ConfigLMM 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/Examples/Implemented.mm.yaml +75 -1
  4. data/Plugins/Apps/Authentik/Authentik-ProxyOutpost.container +14 -0
  5. data/Plugins/Apps/Authentik/Authentik-Server.container +19 -0
  6. data/Plugins/Apps/Authentik/Authentik-Worker.container +18 -0
  7. data/Plugins/Apps/Authentik/Authentik.conf.erb +42 -0
  8. data/Plugins/Apps/Authentik/Authentik.lmm.rb +95 -0
  9. data/Plugins/Apps/BookStack/BookStack.conf.erb +41 -0
  10. data/Plugins/Apps/BookStack/BookStack.container +15 -0
  11. data/Plugins/Apps/BookStack/BookStack.lmm.rb +80 -0
  12. data/Plugins/Apps/Cassandra/Cassandra.lmm.rb +41 -0
  13. data/Plugins/Apps/Discourse/Discourse-Sidekiq.container +17 -0
  14. data/Plugins/Apps/Discourse/Discourse.conf.erb +41 -0
  15. data/Plugins/Apps/Discourse/Discourse.container +17 -0
  16. data/Plugins/Apps/Discourse/Discourse.lmm.rb +95 -0
  17. data/Plugins/Apps/Dovecot/Dovecot.lmm.rb +171 -0
  18. data/Plugins/Apps/ERPNext/ERPNext-Frontend.container +19 -0
  19. data/Plugins/Apps/ERPNext/ERPNext-Queue.container +17 -0
  20. data/Plugins/Apps/ERPNext/ERPNext-Scheduler.container +17 -0
  21. data/Plugins/Apps/ERPNext/ERPNext-Websocket.container +19 -0
  22. data/Plugins/Apps/ERPNext/ERPNext.container +18 -0
  23. data/Plugins/Apps/ERPNext/ERPNext.lmm.rb +193 -0
  24. data/Plugins/Apps/ERPNext/ERPNext.network +12 -0
  25. data/Plugins/Apps/ERPNext/sites/apps.json +10 -0
  26. data/Plugins/Apps/ERPNext/sites/apps.txt +3 -0
  27. data/Plugins/Apps/ERPNext/sites/common_site_config.json +11 -0
  28. data/Plugins/Apps/GitLab/GitLab.container +18 -0
  29. data/Plugins/Apps/GitLab/GitLab.lmm.rb +100 -0
  30. data/Plugins/Apps/LetsEncrypt/LetsEncrypt.lmm.rb +57 -0
  31. data/Plugins/Apps/LetsEncrypt/hooks/dovecot.sh +2 -0
  32. data/Plugins/Apps/LetsEncrypt/hooks/nginx.sh +2 -0
  33. data/Plugins/Apps/LetsEncrypt/hooks/postfix.sh +2 -0
  34. data/Plugins/Apps/LetsEncrypt/renew-certificates.service +7 -0
  35. data/Plugins/Apps/LetsEncrypt/renew-certificates.timer +12 -0
  36. data/Plugins/Apps/LetsEncrypt/rfc2136.ini +11 -0
  37. data/Plugins/Apps/MariaDB/MariaDB.lmm.rb +115 -0
  38. data/Plugins/Apps/Matrix/Element.container +14 -0
  39. data/Plugins/Apps/Matrix/Matrix.conf.erb +49 -5
  40. data/Plugins/Apps/Matrix/Matrix.lmm.rb +86 -1
  41. data/Plugins/Apps/Matrix/Synapse.container +17 -0
  42. data/Plugins/Apps/Matrix/config.json +50 -0
  43. data/Plugins/Apps/Matrix/homeserver.yaml +70 -0
  44. data/Plugins/Apps/Matrix/log.config +30 -0
  45. data/Plugins/Apps/Nextcloud/Nextcloud.conf.erb +48 -10
  46. data/Plugins/Apps/Nextcloud/Nextcloud.lmm.rb +83 -1
  47. data/Plugins/Apps/Nextcloud/config.php +18 -0
  48. data/Plugins/Apps/Nginx/conf.d/configlmm.conf +71 -0
  49. data/Plugins/Apps/Nginx/config-lmm/errors.conf +11 -5
  50. data/Plugins/Apps/Nginx/config-lmm/proxy.conf +5 -1
  51. data/Plugins/Apps/Nginx/main.conf.erb +31 -0
  52. data/Plugins/Apps/Nginx/nginx.conf +3 -68
  53. data/Plugins/Apps/Nginx/nginx.lmm.rb +83 -22
  54. data/Plugins/Apps/Nginx/proxy.conf.erb +13 -3
  55. data/Plugins/Apps/Odoo/Odoo.conf.erb +30 -13
  56. data/Plugins/Apps/Odoo/Odoo.container +18 -0
  57. data/Plugins/Apps/Odoo/Odoo.lmm.rb +62 -2
  58. data/Plugins/Apps/Odoo/odoo.conf +37 -0
  59. data/Plugins/Apps/OpenVidu/Ingress.container +18 -0
  60. data/Plugins/Apps/OpenVidu/OpenVidu.conf.erb +34 -0
  61. data/Plugins/Apps/OpenVidu/OpenVidu.container +16 -0
  62. data/Plugins/Apps/OpenVidu/OpenVidu.lmm.rb +90 -0
  63. data/Plugins/Apps/OpenVidu/OpenViduCall.conf.erb +35 -0
  64. data/Plugins/Apps/OpenVidu/OpenViduCall.container +15 -0
  65. data/Plugins/Apps/OpenVidu/ingress.yaml +10 -0
  66. data/Plugins/Apps/OpenVidu/livekit.yaml +13 -0
  67. data/Plugins/Apps/PHP-FPM/PHP-FPM.lmm.rb +95 -0
  68. data/Plugins/Apps/Peppermint/Peppermint.conf.erb +60 -0
  69. data/Plugins/Apps/Peppermint/Peppermint.container +15 -0
  70. data/Plugins/Apps/Peppermint/Peppermint.lmm.rb +58 -0
  71. data/Plugins/Apps/Postfix/Postfix.lmm.rb +165 -31
  72. data/Plugins/Apps/Postfix/smtpd.conf +3 -0
  73. data/Plugins/Apps/PostgreSQL/PostgreSQL.lmm.rb +242 -24
  74. data/Plugins/Apps/Roundcube/Roundcube.conf.erb +75 -0
  75. data/Plugins/Apps/Roundcube/Roundcube.lmm.rb +145 -0
  76. data/Plugins/Apps/SSH/SSH.lmm.rb +51 -0
  77. data/Plugins/Apps/Tunnel/tunnel.lmm.rb +63 -0
  78. data/Plugins/Apps/Tunnel/tunnelTCP.service +9 -0
  79. data/Plugins/Apps/Tunnel/tunnelTCP.socket +9 -0
  80. data/Plugins/Apps/Tunnel/tunnelUDP.service +9 -0
  81. data/Plugins/Apps/Tunnel/tunnelUDP.socket +9 -0
  82. data/Plugins/Apps/UVdesk/UVdesk.conf.erb +52 -0
  83. data/Plugins/Apps/UVdesk/UVdesk.lmm.rb +85 -0
  84. data/Plugins/Apps/Valkey/Valkey.lmm.rb +34 -1
  85. data/Plugins/Apps/Vaultwarden/Vaultwarden.conf.erb +35 -18
  86. data/Plugins/Apps/Vaultwarden/Vaultwarden.container +16 -0
  87. data/Plugins/Apps/Vaultwarden/Vaultwarden.lmm.rb +46 -3
  88. data/Plugins/Apps/Wiki.js/Wiki.js.conf.erb +42 -0
  89. data/Plugins/Apps/Wiki.js/Wiki.js.container +15 -0
  90. data/Plugins/Apps/Wiki.js/Wiki.js.lmm.rb +61 -0
  91. data/Plugins/Apps/gollum/gollum.conf.erb +84 -19
  92. data/Plugins/Apps/gollum/gollum.container +15 -0
  93. data/Plugins/Apps/gollum/gollum.lmm.rb +48 -11
  94. data/Plugins/OS/Linux/Debian/preseed.cfg.erb +62 -0
  95. data/Plugins/OS/Linux/Distributions.yaml +42 -0
  96. data/Plugins/OS/Linux/Flavours.yaml +11 -0
  97. data/Plugins/OS/Linux/Linux.lmm.rb +362 -41
  98. data/Plugins/OS/Linux/Packages.yaml +88 -5
  99. data/Plugins/OS/Linux/Proxmox/answer.toml.erb +30 -0
  100. data/Plugins/OS/Linux/WireGuard/WireGuard.lmm.rb +137 -0
  101. data/Plugins/OS/Linux/WireGuard/wg0.conf.erb +15 -0
  102. data/Plugins/OS/Linux/systemd/systemd.lmm.rb +28 -0
  103. data/Plugins/OS/Linux/systemd/user-0.slice +9 -0
  104. data/Plugins/OS/Linux/systemd/user@.service.d/delegate.conf +3 -0
  105. data/Plugins/Platforms/GoDaddy/GoDaddy.lmm.rb +7 -3
  106. data/Plugins/Platforms/libvirt/libvirt.lmm.rb +3 -2
  107. data/Plugins/Services/DNS/PowerDNS.lmm.rb +158 -8
  108. data/README.md +6 -0
  109. data/bootstrap.sh +92 -0
  110. data/lib/ConfigLMM/Framework/plugins/dns.rb +1 -2
  111. data/lib/ConfigLMM/Framework/plugins/linuxApp.rb +249 -45
  112. data/lib/ConfigLMM/Framework/plugins/nginxApp.rb +56 -7
  113. data/lib/ConfigLMM/Framework/plugins/plugin.rb +112 -16
  114. data/lib/ConfigLMM/cli.rb +3 -1
  115. data/lib/ConfigLMM/commands/cleanup.rb +1 -0
  116. data/lib/ConfigLMM/commands/configsCommand.rb +3 -1
  117. data/lib/ConfigLMM/io/configList.rb +3 -1
  118. data/lib/ConfigLMM/state.rb +10 -2
  119. data/lib/ConfigLMM/version.rb +1 -1
  120. metadata +82 -3
  121. data/Plugins/Apps/Nginx/main.conf +0 -30
@@ -10,14 +10,17 @@ module ConfigLMM
10
10
 
11
11
  ISO_LOCATION = '~/.cache/configlmm/images/'
12
12
  HOSTS_FILE = '/etc/hosts'
13
+ FSTAB_FILE = '/etc/fstab'
13
14
  SSH_CONFIG = '~/.ssh/config'
14
- SYSCTL_FILE = '/etc/sysctl.d/10-configlmm.conf'
15
+ SYSCTL_FILE = '/etc/sysctl.d/90-configlmm.conf'
16
+ FIREWALL_PACKAGE = 'firewalld'
17
+ FIREWALL_SERVICE = 'firewalld'
15
18
 
16
19
  def actionLinuxBuild(id, target, activeState, context, options)
17
20
  prepareConfig(target)
18
21
  buildHostsFile(id, target, options)
19
22
  buildSSHConfig(id, target, options)
20
- buildAutoYaST(id, target, options)
23
+ buildAutoInstall(id, target, options)
21
24
  end
22
25
 
23
26
  def actionLinuxDeploy(id, target, activeState, context, options)
@@ -33,8 +36,7 @@ module ConfigLMM
33
36
  raise Framework::PluginProcessError.new("#{id}: Unknown protocol: #{uri.scheme}!")
34
37
  end
35
38
  else
36
- deployLocalHostsFile(target, options)
37
- deployLocalSSHConfig(target, options)
39
+ deployLocal(target, options)
38
40
  end
39
41
  if target['AlternativeLocation']
40
42
  uri = Addressable::URI.parse(target['AlternativeLocation'])
@@ -44,38 +46,316 @@ module ConfigLMM
44
46
  end
45
47
 
46
48
  def deployOverSSH(locationUri, id, target, activeState, context, options)
47
- if target['Domain'] || target['Hosts']
48
- hostsLines = []
49
- if target['Domain']
50
- self.class.sshStart(locationUri) do |ssh|
49
+ self.class.sshStart(locationUri) do |ssh|
50
+ if target['Domain'] || target['Hosts']
51
+ hostsLines = []
52
+ if target['Domain']
51
53
  envs = self.class.sshExec!(ssh, "env").split("\n")
52
54
  envVars = Hash[envs.map { |vars| vars.split('=', 2) }]
53
55
  ipAddr = envVars['SSH_CONNECTION'].split[-2]
54
- hostsLines << ipAddr.ljust(16) + target['Domain'] + ' ' + target['Name'] + "\n"
56
+ hostsLines << ipAddr.ljust(16) + Addressable::IDNA.to_ascii(target['Domain']) + ' ' + target['Name'] + "\n"
55
57
  end
56
- end
57
- target['Hosts'].to_a.each do |ip, entries|
58
+ target['Hosts'].to_a.each do |ip, entries|
58
59
  hostsLines << ip.ljust(16) + entries.join(' ') + "\n"
60
+ end
61
+ updateRemoteFile(ssh, HOSTS_FILE, options, false) do |fileLines|
62
+ fileLines + hostsLines
63
+ end
64
+ end
65
+ distroInfo = self.class.currentDistroInfo(ssh)
66
+ convertFlavour(distroInfo, target, ssh, options)
67
+ configureNetwork(distroInfo, target, ssh, options)
68
+ if target['Tmpfs']
69
+ self.class.sshExec!(ssh, "sed -i '/ \\/tmp /d' #{FSTAB_FILE}")
70
+ updateRemoteFile(ssh, FSTAB_FILE, options, false) do |fileLines|
71
+ fileLines << "tmpfs /tmp tmpfs nodev,nosuid,size=#{target['Tmpfs']} 0 0\n"
72
+ end
73
+ end
74
+ if target['Sysctl']
75
+ updateRemoteFile(ssh, SYSCTL_FILE, options, false) do |fileLines|
76
+ target['Sysctl'].each do |name, value|
77
+ fileLines << "#{name} = #{value}\n"
78
+ self.class.sshExec!(ssh, "sysctl #{name}=#{value}")
79
+ end
80
+ fileLines
81
+ end
82
+ end
83
+ if target['Users']
84
+ target['Users'].each do |name, info|
85
+ userId = ssh.exec!("id -u #{name} 2>/dev/null").strip
86
+ if userId.empty?
87
+ shell = ''
88
+ if info['Shell']
89
+ shell = "--shell '/usr/bin/#{info['Shell']}'"
90
+ end
91
+ badname = '--badname'
92
+ badname = '--badnames' if distroInfo['Name'] == 'openSUSE Leap'
93
+ self.class.sshExec!(ssh, "useradd #{badname} --create-home --user-group #{shell} #{name}")
94
+ end
95
+ homeDir = self.class.sshExec!(ssh, "getent passwd #{name} | cut -d ':' -f 6").strip
96
+ keyFile = homeDir + "/.ssh/id_ed25519"
97
+ if info['SSHKey'] && !self.class.remoteFilePresent?(keyFile, ssh)
98
+ self.class.sshExec!(ssh, "mkdir -p #{homeDir}/.ssh")
99
+ self.class.sshExec!(ssh, "ssh-keygen -t ed25519 -f #{keyFile} -P ''")
100
+ self.class.sshExec!(ssh, "chown -R #{name}:#{name} #{homeDir}/.ssh")
101
+ end
102
+ end
103
+ end
104
+ self.executeCommands(target['Execute'], ssh)
105
+ end
106
+ if target['Firewall'] && target['Firewall'] != 'no'
107
+ self.ensurePackage(FIREWALL_PACKAGE, locationUri)
108
+ self.ensureServiceAutoStart(FIREWALL_SERVICE, locationUri)
109
+ self.startService(FIREWALL_SERVICE, locationUri)
110
+ end
111
+ end
112
+
113
+ def convertFlavour(distroInfo, target, ssh, options)
114
+ if target['Flavour']
115
+ if target['Flavour'] == PROXMOXVE_NAME
116
+ if distroInfo['Name'] != DEBIAN_NAME
117
+ raise 'Can\'t convert flavour!'
118
+ end
119
+ if self.class.filePresent?('/etc/apt/sources.list.d/pve-install-repo.list', ssh)
120
+ needInstall = self.class.exec('dpkg --status proxmox-ve 2>/dev/null | grep Status | grep installed | wc -l', ssh).strip.to_i.zero?
121
+ if needInstall
122
+ self.class.exec('DEBIAN_FRONTEND=noninteractive apt install --assume-yes proxmox-ve postfix open-iscsi chrony', ssh)
123
+ self.class.exec("apt remove --assume-yes os-prober linux-image-amd64 'linux-image-*'", ssh)
124
+ self.class.exec('update-grub', ssh)
125
+ end
126
+ else
127
+ self.class.exec('echo "deb [arch=amd64] http://download.proxmox.com/debian/pve bookworm pve-no-subscription" > /etc/apt/sources.list.d/pve-install-repo.list', ssh)
128
+ File.write(options['output'] + 'proxmox-release-bookworm.gpg', HTTP.follow.get('https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg').body)
129
+ ssh.scp.upload!(options['output'] + 'proxmox-release-bookworm.gpg', '/etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg')
130
+ self.class.exec('apt update && apt full-upgrade --assume-yes', ssh)
131
+ self.class.exec('apt install --assume-yes proxmox-default-kernel', ssh)
132
+ self.class.exec('systemctl reboot', ssh)
133
+ end
134
+ target['Network'] = {} unless target['Network'].is_a?(Hash)
135
+ target['Network']['Interfaces'] = {} unless target['Network']['Interfaces'].is_a?(Hash)
136
+ if !target['Network']['Interfaces'].key?('vmbr0')
137
+ if target['Network']['IP']
138
+ target['Network']['Interfaces']['vmbr0'] = {}
139
+ target['Network']['Interfaces']['vmbr0']['Type'] = 'Bridge'
140
+ target['Network']['Interfaces']['vmbr0']['IP'] = target['Network']['IP']
141
+ target['Network']['Interfaces']['vmbr0']['Gateway'] = target['Network']['Gateway']
142
+ target['Network']['Interfaces']['vmbr0']['DNS'] = target['Network']['DNS']
143
+ else
144
+ target['Network']['Interfaces']['vmbr0'] = 'dhcp'
145
+ end
146
+ end
147
+ else
148
+ raise 'Unimplemented flavour!'
149
+ end
150
+ end
151
+ end
152
+
153
+ def configureNetwork(distroInfo, target, ssh, options)
154
+ if target['Network']
155
+ if distroInfo['Name'] == 'openSUSE Leap'
156
+ updateNetworkInterface(target['Network'], 'eth0', ssh, options)
157
+ if target['Network']['Interfaces']
158
+ target['Network']['Interfaces'].each do |interface, config|
159
+ updateNetworkInterface(config, interface, ssh, options)
160
+ end
161
+ end
162
+ if target['Network']['DNS']
163
+ configFile = '/etc/sysconfig/network/config'
164
+ dns = target['Network']['DNS']
165
+ dns = [dns] unless dns.is_a?(Array)
166
+ self.class.sshExec!(ssh, "sed -i 's|^NETCONFIG_DNS_STATIC_SERVERS=.*|NETCONFIG_DNS_STATIC_SERVERS=\"#{dns.join(' ')}\"|' #{configFile}")
167
+ end
168
+ if target['Network']['Gateway']
169
+ routesFile = '/etc/sysconfig/network/routes'
170
+ self.class.sshExec!(ssh, "sed -i 's|^default |#default |' #{routesFile}")
171
+ updateRemoteFile(ssh, routesFile, options) do |fileLines|
172
+ fileLines << "default #{target['Network']['Gateway']}\n"
173
+ end
174
+ end
175
+ elsif distroInfo['Name'] == 'Debian'
176
+ links = self.networkLinks(ssh)
177
+ raise 'Didn\'t find network links!' if links.empty?
178
+ linkType = nil
179
+ dnsSearch = self.class.exec('cat /etc/resolv.conf | grep search', ssh).strip.split(' ').last
180
+ if target['Network'].is_a?(String)
181
+ linkType = target['Network']
182
+ target['Network'] = {}
183
+ end
184
+ if !target['Network'].key?('Interfaces') ||
185
+ target['Network']['Interfaces'].to_h.empty? ||
186
+ !target['Network']['Interfaces'].key?(links.first)
187
+ target['Network']['Interfaces'] ||= {}
188
+ if !linkType.nil?
189
+ target['Network']['Interfaces'][links.first] = linkType
190
+ else
191
+ target['Network']['Interfaces'][links.first] = {}
192
+ target['Network']['Interfaces'][links.first]['IP'] = target['Network']['IP']
193
+ target['Network']['Interfaces'][links.first]['Gateway'] = target['Network']['Gateway']
194
+ target['Network']['Interfaces'][links.first]['DNS'] = target['Network']['DNS']
195
+ end
196
+ end
197
+ if target['Network']['Interfaces'].key?('vmbr0')
198
+ if target['Network']['Interfaces']['vmbr0']['Ports'].nil?
199
+ target['Network']['Interfaces']['vmbr0']['Ports'] = [links.first]
200
+ target['Network']['Interfaces'][links.first] = 'manual'
201
+ end
202
+ end
203
+ interfacesFile = '/etc/network/interfaces'
204
+ localFile = options['output'] + '/' + SecureRandom.alphanumeric(10)
205
+ ssh.scp.download!(interfacesFile, localFile)
206
+ fileLines = File.read(localFile).lines
207
+ if fileLines.index(CONFIGLMM_SECTION_BEGIN).nil?
208
+ lines = []
209
+ iface = false
210
+ fileLines.each do |line|
211
+ if line.start_with?('iface')
212
+ if line.strip.split(' ')[1].start_with?('enp')
213
+ iface = true
214
+ else
215
+ lines << line
216
+ end
217
+ elsif iface && (line.start_with?(' ') || line.start_with?("\t"))
218
+ # Drop line
219
+ else
220
+ iface = false
221
+ lines << line
222
+ end
223
+ end
224
+ fileWrite(localFile, lines.join(), options[:dry])
225
+ ssh.scp.upload!(localFile, interfacesFile)
226
+ end
227
+ self.updateRemoteFile(ssh, interfacesFile, options) do |fileLines|
228
+ target['Network']['Interfaces'].each do |name, data|
229
+ fileLines << "auto #{name}\n"
230
+ if data.is_a?(String)
231
+ fileLines << "iface #{name} inet #{data}\n"
232
+ else
233
+ fileLines << "iface #{name} inet static\n"
234
+ fileLines << " address #{data['IP']}\n"
235
+ fileLines << " gateway #{data['Gateway']}\n"
236
+ if data['Ports']
237
+ fileLines << " bridge-ports #{data['Ports'].join(' ')}\n"
238
+ fileLines << " bridge-stp off\n"
239
+ fileLines << " bridge-fd 0\n"
240
+ end
241
+ fileLines << " # dns-* options are implemented by the resolvconf package, if installed\n"
242
+ fileLines << " dns-nameservers #{data['DNS']}\n"
243
+ if dnsSearch
244
+ fileLines << " dns-search #{dnsSearch}\n"
245
+ end
246
+ end
247
+ fileLines << "\n"
248
+ end
249
+ fileLines
250
+ end
251
+ else
252
+ # TODO
253
+ raise 'Not Unimplemented!'
59
254
  end
60
- updateRemoteFile(locationUri, HOSTS_FILE, options, false) do |fileLines|
61
- fileLines + hostsLines
255
+ end
256
+ end
257
+
258
+ def updateNetworkInterface(config, interface, ssh, options)
259
+ baseFile = '/etc/sysconfig/network/ifcfg-'
260
+ networkFile = baseFile + interface
261
+ self.class.sshExec!(ssh, "touch #{networkFile}")
262
+ self.class.sshExec!(ssh, "sed -i \"/^BOOTPROTO=.*/d\" #{networkFile}")
263
+ self.class.sshExec!(ssh, "sed -i \"/^STARTMODE=.*/d\" #{networkFile}")
264
+ self.class.sshExec!(ssh, "sed -i \"/^ZONE=.*/d\" #{networkFile}")
265
+ updateRemoteFile(ssh, networkFile, options, false) do |fileLines|
266
+ fileLines << "STARTMODE=auto\n"
267
+ fileLines << "ZONE=public\n"
268
+ if config == 'dhcp'
269
+ fileLines << "BOOTPROTO=dhcp\n"
270
+ else
271
+ fileLines << "BOOTPROTO=static\n"
272
+ fileLines << "\n"
273
+ if config['IP']
274
+ self.class.sshExec!(ssh, "sed -i 's|^IPADDR=|#IPADDR=|' #{networkFile}")
275
+ if config['IP'].is_a?(Array)
276
+ config['IP'].each_with_index do |ip, i|
277
+ c = "_#{i}"
278
+ c = '' if i.zero?
279
+ fileLines << "IPADDR#{c}=#{ip}\n"
280
+ end
281
+ else
282
+ fileLines << "IPADDR=#{config['IP']}\n"
283
+ end
284
+ end
62
285
  end
286
+ fileLines
63
287
  end
288
+ end
289
+
290
+ def networkLinks(ssh)
291
+ self.class.exec("ls /sys/class/net/", ssh).strip.split("\n").select { |name| name.start_with?('enp') }
292
+ end
293
+
294
+ def deployLocal(target, options)
295
+ deployLocalHostsFile(target, options)
296
+ deployLocalSSHConfig(target, options)
64
297
  if target['Sysctl']
65
- updateRemoteFile(locationUri, SYSCTL_FILE, options, false) do |fileLines|
298
+ updateLocalFile(SYSCTL_FILE, options) do |fileLines|
66
299
  target['Sysctl'].each do |name, value|
67
300
  fileLines << "#{name} = #{value}\n"
301
+ `sysctl #{name}=#{value}`
68
302
  end
69
303
  fileLines
70
304
  end
71
305
  end
306
+ if target['Users']
307
+ target['Users'].each do |name, info|
308
+ userId = self.class.exec("id -u #{name} 2>/dev/null", nil, true).strip
309
+ if userId.empty?
310
+ shell = ''
311
+ if info['Shell']
312
+ shell = "--shell '/usr/bin/#{info['Shell']}'"
313
+ end
314
+ distroInfo = self.class.currentDistroInfo(nil)
315
+ badname = '--badname'
316
+ badname = '--badnames' if distroInfo['Name'] == 'openSUSE Leap'
317
+ self.class.exec("useradd #{badname} --create-home --user-group #{shell} #{name}")
318
+ end
319
+ homeDir = self.class.exec("getent passwd #{name} | cut -d ':' -f 6").strip
320
+ keyFile = homeDir + "/.ssh/id_ed25519"
321
+ if info['SSHKey'] && !self.class.filePresent?(keyFile)
322
+ self.class.exec("mkdir -p #{homeDir}/.ssh")
323
+ self.class.exec("ssh-keygen -t ed25519 -f #{keyFile} -P ''")
324
+ self.class.exec("chown -R #{name}:#{name} #{homeDir}/.ssh")
325
+ end
326
+ end
327
+ end
328
+ if target['Firewall'] && target['Firewall'] != 'no'
329
+ self.ensurePackage(FIREWALL_PACKAGE, locationUri)
330
+ self.ensureServiceAutoStart(FIREWALL_SERVICE, locationUri)
331
+ self.startService(FIREWALL_SERVICE, locationUri)
332
+ end
333
+ self.executeCommands(target['Execute'])
334
+ end
335
+
336
+ def executeCommands(commands, ssh = nil)
337
+ return unless commands
338
+
339
+ commands.each do |type, data|
340
+ case type
341
+ when 'sh'
342
+ data = [data] unless data.is_a?(Array)
343
+ data.each do |cmd|
344
+ self.class.exec(cmd, ssh)
345
+ end
346
+ else
347
+ raise 'Unimplemented!'
348
+ end
349
+ end
72
350
  end
73
351
 
74
352
  def deployOverLibvirt(id, target, activeState, context, options)
75
353
  location = Libvirt.getLocation(target['Location'])
76
- iso = installationISO(target['Distro'], location)
77
- iso = buildISOAutoYaST(id, iso, target, options) if target['Distro'] == SUSE_NAME
78
- plugins[:Libvirt].createVM(target['Name'], target, target['Location'], iso, activeState)
354
+ iso = installationISO(target['Distro'], target['Flavour'], location)
355
+ iso = buildAutoInstallISO(id, iso, target, options)
356
+ if plugins[:Libvirt].createVM(target['Name'], target, target['Location'], iso, activeState)
357
+ prompt.say("Root password: #{target['Users']['root']['Password']}", :color => :magenta) if target['Users']['root'].key?('Password')
358
+ end
79
359
  end
80
360
 
81
361
  def buildHostsFile(id, target, options)
@@ -119,11 +399,20 @@ module ConfigLMM
119
399
  end
120
400
  end
121
401
 
122
- def buildAutoYaST(id, target, options)
123
- if target['Distro'] == SUSE_NAME
402
+ def buildAutoInstall(id, target, options)
403
+ if target['Flavour'] == PROXMOXVE_NAME
404
+ outputFolder = options['output'] + '/' + id + '/'
405
+ template = ERB.new(File.read(__dir__ + '/Proxmox/answer.toml.erb'))
406
+ renderTemplate(template, target, outputFolder + 'answer.toml', options)
407
+ File.write("#{outputFolder}/auto-installer-mode.toml", 'mode = "iso"')
408
+ elsif target['Distro'] == SUSE_NAME
124
409
  outputFolder = options['output'] + '/' + id + '/'
125
410
  template = ERB.new(File.read(__dir__ + '/openSUSE/autoinst.xml.erb'))
126
411
  renderTemplate(template, target, outputFolder + 'autoinst.xml', options)
412
+ elsif target['Distro'] == DEBIAN_NAME
413
+ outputFolder = options['output'] + '/' + id + '/'
414
+ template = ERB.new(File.read(__dir__ + '/Debian/preseed.cfg.erb'))
415
+ renderTemplate(template, target, outputFolder + 'preseed.cfg', options)
127
416
  end
128
417
  end
129
418
 
@@ -153,20 +442,14 @@ module ConfigLMM
153
442
  end
154
443
  end
155
444
 
156
- def installationISO(distro, location)
445
+ def installationISO(distro, flavour, location)
157
446
  url = nil
158
- case distro
159
- when SUSE_NAME
160
- if location.empty?
161
- # TODO automatically fetch latest version from website
162
- url = 'https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso'
163
- else
164
- raise Framework::PluginProcessError.new("#{id}: Unimplemented!")
165
- end
166
- else
167
- raise Framework::PluginProcessError.new("#{id}: Unknown Linux Distro: #{distro}!")
447
+ flavour = distro unless flavour
448
+ flavourInfo = YAML.load_file(__dir__ + '/Flavours.yaml')[flavour]
449
+ if flavourInfo.nil?
450
+ raise Framework::PluginProcessError.new("#{id}: Unknown Linux Distro: #{flavour}!")
168
451
  end
169
-
452
+ url = flavourInfo['ISO']
170
453
  filename = File.basename(Addressable::URI.parse(url).path)
171
454
  iso = File.expand_path(ISO_LOCATION + filename)
172
455
  if !File.exist?(iso)
@@ -183,21 +466,59 @@ module ConfigLMM
183
466
  iso
184
467
  end
185
468
 
469
+ def buildAutoInstallISO(id, iso, target, options)
470
+ if target['Flavour'] == PROXMOXVE_NAME
471
+ iso = buildISOAutoProxmox(id, iso, target, options)
472
+ elsif target['Distro'] == SUSE_NAME
473
+ iso = buildISOAutoYaST(id, iso, target, options)
474
+ elsif target['Distro'] == DEBIAN_NAME
475
+ iso = buildISOPreseed(id, iso, target, options)
476
+ end
477
+ iso
478
+ end
479
+
186
480
  def buildISOAutoYaST(id, iso, target, options)
187
481
  outputFolder = options['output'] + '/iso/'
188
482
  mkdir(outputFolder, false)
189
- `xorriso -osirrox on -indev #{iso} -extract / #{outputFolder} 2>&1 >/dev/null`
483
+ self.class.exec("xorriso -osirrox on -indev #{iso} -extract / #{outputFolder}")
190
484
  FileUtils.chmod_R(0750, outputFolder) # Need to make it writeable so it can be deleted
191
485
  copy(options['output'] + '/' + id + '/autoinst.xml', outputFolder, false)
192
486
 
193
487
  cfg = outputFolder + "boot/x86_64/loader/isolinux.cfg"
194
- `sed -i 's|default harddisk|default linux|' #{cfg}`
195
- `sed -i 's|append initrd=initrd splash=silent showopts|append initrd=initrd splash=silent autoyast=device://sr0/autoinst.xml|' #{cfg}`
196
- `sed -i 's|prompt 1|prompt 0|' #{cfg}`
197
- `sed -i 's|timeout 600|timeout 1|' #{cfg}`
488
+ self.class.exec("sed -i 's|default harddisk|default linux|' #{cfg}")
489
+ self.class.exec("sed -i 's|append initrd=initrd splash=silent showopts|append initrd=initrd splash=silent autoyast=device://sr0/autoinst.xml|' #{cfg}")
490
+ self.class.exec("sed -i 's|prompt 1|prompt 0|' #{cfg}")
491
+ self.class.exec("sed -i 's|timeout 600|timeout 1|' #{cfg}")
492
+
493
+ patchedIso = File.dirname(iso) + '/patched.iso'
494
+ self.class.exec("xorriso -as mkisofs -no-emul-boot -boot-info-table -boot-load-size 4 -iso-level 4 -b boot/x86_64/loader/isolinux.bin -c boot/x86_64/loader/boot.cat -eltorito-alt-boot -no-emul-boot -e boot/x86_64/efi -o #{patchedIso} #{outputFolder}")
495
+ patchedIso
496
+ end
497
+
498
+ def buildISOPreseed(id, iso, target, options)
499
+ outputFolder = options['output'] + '/iso/'
500
+ mkdir(outputFolder, false)
501
+ self.class.exec("xorriso -osirrox on -indev #{iso} -extract / #{outputFolder}")
502
+ FileUtils.chmod_R(0750, outputFolder) # Need to make it writeable so it can be deleted
503
+ copy(options['output'] + '/' + id + '/preseed.cfg', outputFolder, false)
504
+
505
+ self.class.exec("sed -i 's|vga=788 --- quiet|auto=true file=/cdrom/preseed.cfg vga=788 --- quiet|' #{outputFolder + "boot/grub/grub.cfg"}")
506
+ self.class.exec("sed -i 's|--- quiet|file=/cdrom/preseed.cfg --- quiet|' #{outputFolder + "isolinux/adgtk.cfg"}")
507
+ self.class.exec("sed -i 's|default .*|default autogui|' #{outputFolder + "isolinux/isolinux.cfg"}")
508
+
509
+ patchedIso = File.dirname(iso) + '/patched.iso'
510
+ self.class.exec("xorriso -as mkisofs -no-emul-boot -boot-info-table -boot-load-size 4 -iso-level 4 -b isolinux/isolinux.bin -c isolinux/boot.cat -eltorito-alt-boot -o #{patchedIso} #{outputFolder}")
511
+ patchedIso
512
+ end
198
513
 
514
+ def buildISOAutoProxmox(id, iso, target, options)
515
+ outputFolder = options['output'] + '/iso/'
199
516
  patchedIso = File.dirname(iso) + '/patched.iso'
200
- `xorriso -as mkisofs -no-emul-boot -boot-load-size 4 -boot-info-table -iso-level 4 -b boot/x86_64/loader/isolinux.bin -c boot/x86_64/loader/boot.cat -eltorito-alt-boot -e boot/x86_64/efi -no-emul-boot -o #{patchedIso} #{outputFolder} 2>&1 >/dev/null`
517
+
518
+ copy(iso, patchedIso, false)
519
+
520
+ self.class.exec("xorriso -boot_image any keep -dev #{patchedIso} -map #{options['output'] + '/' + id + '/auto-installer-mode.toml'} /auto-installer-mode.toml")
521
+ self.class.exec("xorriso -boot_image any keep -dev #{patchedIso} -map #{options['output'] + '/' + id + '/answer.toml'} /answer.toml")
201
522
  patchedIso
202
523
  end
203
524
 
@@ -212,13 +533,13 @@ module ConfigLMM
212
533
  target['Users']['root']['PasswordHash'] = ENV['LINUX_ROOT_PASSWORD_HASH']
213
534
  elsif ENV['LINUX_ROOT_PASSWORD']
214
535
  target['Users']['root'] ||= {}
536
+ target['Users']['root']['Password'] = ENV['LINUX_ROOT_PASSWORD']
215
537
  target['Users']['root']['PasswordHash'] = self.class.linuxPasswordHash(ENV['LINUX_ROOT_PASSWORD'])
216
538
  elsif target['Users'].key?('root')
217
539
  if !target['Users']['root']['Password'] &&
218
540
  !target['Users']['root']['PasswordHash']
219
- rootPassword = SecureRandom.urlsafe_base64(12)
220
- prompt.say("Root password: #{rootPassword}", :color => :magenta)
221
- target['Users']['root']['PasswordHash'] = self.class.linuxPasswordHash(rootPassword)
541
+ target['Users']['root']['Password'] = SecureRandom.urlsafe_base64(12)
542
+ target['Users']['root']['PasswordHash'] = self.class.linuxPasswordHash(target['Users']['root']['Password'])
222
543
  elsif target['Users']['root']['Password'] == 'no'
223
544
  target['Users']['root'].delete('Password')
224
545
  end
@@ -243,7 +564,7 @@ module ConfigLMM
243
564
  target['Services'] << 'sshd'
244
565
  target['Services'].uniq!
245
566
  end
246
- target['Apps'] = self.class.mapPackages(target['Apps'], target['Distro'])
567
+ target['Apps'] = self.class.mapPackages(target['Apps'], target['Distro']) if target['Distro']
247
568
  end
248
569
 
249
570
  def self.linuxPasswordHash(password)
@@ -1,13 +1,96 @@
1
1
  Arch Linux:
2
- sshd: openssh
3
- Postfix: postfix
2
+ Cassandra: AUR|cassandra
3
+ CertBotNginx:
4
+ - certbot-nginx
5
+ - certbot-dns-rfc2136
6
+ CyrusSASL: cyrus-sasl
4
7
  Dovecot: dovecot
8
+ firewalld: firewalld
9
+ MariaDB: mariadb
10
+ Nextcloud: nextcloud
11
+ nginx: nginx
12
+ PHP-FPM: php-fpm
13
+ php-pecl: php-pecl
14
+ Podman: podman
5
15
  PostgreSQL: postgresql
16
+ Postfix: postfix
17
+ PowerDNS: powerdns
18
+ Roundcube: roundcubemail
19
+ socat: socat
20
+ sshd: openssh
6
21
  Valkey: redis
22
+ WireGuard: wireguard-tools
23
+ Yarn: yarn
7
24
 
8
25
  openSUSE Leap:
9
- sshd: openssh
10
- Postfix: postfix
26
+ Cassandra: server:database|cassandra
27
+ CertBotNginx:
28
+ - python3-certbot-nginx
29
+ - certbot-systemd-timer
30
+ - python3-certbot-dns-rfc2136
31
+ CyrusSASL: cyrus-sasl-plain
11
32
  Dovecot: dovecot
12
- PostgreSQL: postgresql-server
33
+ firewalld: firewalld
34
+ MariaDB: mariadb
35
+ Nextcloud: server:php:applications|nextcloud
36
+ nginx: nginx
37
+ PHP-FPM:
38
+ - php8-devel
39
+ - php8-mbstring
40
+ - php8-fpm
41
+ - php8-redis
42
+ - php8-pgsql
43
+ - php8-mysql
44
+ php-pecl: php8-pecl
45
+ Podman: podman
46
+ PostgreSQL:
47
+ - postgresql-server
48
+ - postgresql-contrib
49
+ Postfix: postfix
50
+ PowerDNS:
51
+ - pdns
52
+ - pdns-backend-geoip
53
+ - pdns-backend-sqlite3
54
+ - pdns-backend-postgresql
55
+ Roundcube: roundcubemail
56
+ socat: socat
57
+ sshd: openssh
58
+ Valkey: redis
59
+ WireGuard: wireguard-tools
60
+ Yarn: yarn
61
+
62
+ Debian:
63
+ Cassandra: https://debian.cassandra.apache.org 41x main|cassandra
64
+ CertBotNginx:
65
+ - certbot
66
+ - python3-certbot-dns-rfc2136
67
+ CyrusSASL: libsasl2-modules
68
+ Dovecot:
69
+ - dovecot-imapd
70
+ - dovecot-lmtpd
71
+ - dovecot-submissiond
72
+ firewalld: firewalld
73
+ MariaDB: mariadb-server
74
+ Nextcloud:
75
+ nginx: nginx
76
+ PHP-FPM:
77
+ - php-mbstring
78
+ - php-fpm
79
+ - php-redis
80
+ - php-pgsql
81
+ - php-mysql
82
+ php-pecl:
83
+ Podman: podman
84
+ PostgreSQL: postgresql
85
+ Postfix: postfix-lmdb
86
+ PowerDNS:
87
+ - pdns-server
88
+ - pdns-backend-geoip
89
+ - pdns-backend-sqlite3
90
+ - pdns-backend-pgsql
91
+ Roundcube: roundcube-pgsql
92
+ socat: socat
93
+ sshd: openssh-server
13
94
  Valkey: redis
95
+ WireGuard: wireguard
96
+ Yarn: yarnpkg
@@ -0,0 +1,30 @@
1
+ [global]
2
+ keyboard = "en-us"
3
+ country = "us"
4
+ fqdn = "<%= Addressable::IDNA.to_ascii(config['Domain']) %>"
5
+ mailto = "<%= config['EMail'] %>"
6
+ timezone = "UTC"
7
+ root_password = "<%= config['Users'].to_h['root'].to_h['Password'] %>"
8
+ <% if !config['Users'].to_h['root'].to_h['AuthorizedKeys'].to_a.empty? %>
9
+ root_ssh_keys = [
10
+ <% config['Users']['root']['AuthorizedKeys'].each do |entry| %>
11
+ "<%= entry %>"
12
+ <% end %>
13
+ ]
14
+ <% end %>
15
+
16
+ [network]
17
+ <% if config['Network'].is_a?(Hash) %>
18
+ source = "from-answer"
19
+ cidr = "<%= config['Network']['IP'] %>"
20
+ dns = "<%= config['Network']['DNS'] %>"
21
+ gateway = "<%= config['Network']['Gateway'] %>"
22
+ filter.IFINDEX = "2"
23
+ <% else %>
24
+ source = "from-dhcp"
25
+ <% end %>
26
+
27
+ [disk-setup]
28
+ filesystem = "btrfs"
29
+ btrfs.raid = "raid1"
30
+ disk_list = ["vda"]