ConfigLMM 0.4.0 → 0.5.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 (227) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/CNAME +1 -0
  4. data/Examples/.lmm.state.yaml +159 -0
  5. data/Examples/ConfigLMM.mm.yaml +32 -0
  6. data/Examples/Implemented.mm.yaml +252 -4
  7. data/Examples/SmallBusiness.mm.yaml +492 -0
  8. data/Plugins/Apps/Answer/answer.lmm.rb +165 -0
  9. data/Plugins/Apps/Answer/answer@.service +40 -0
  10. data/Plugins/Apps/ArchiSteamFarm/ArchiSteamFarm.conf.erb +0 -3
  11. data/Plugins/Apps/ArchiSteamFarm/ArchiSteamFarm.lmm.rb +0 -1
  12. data/Plugins/Apps/Authentik/Authentik-ProxyOutpost.container +7 -1
  13. data/Plugins/Apps/Authentik/Authentik-Server.container +6 -1
  14. data/Plugins/Apps/Authentik/Authentik-Worker.container +6 -1
  15. data/Plugins/Apps/Authentik/Authentik.conf.erb +12 -7
  16. data/Plugins/Apps/Authentik/Authentik.lmm.rb +226 -61
  17. data/Plugins/Apps/BookStack/BookStack.conf.erb +0 -3
  18. data/Plugins/Apps/BookStack/BookStack.container +5 -0
  19. data/Plugins/Apps/BookStack/BookStack.lmm.rb +14 -3
  20. data/Plugins/Apps/Cassandra/Cassandra.lmm.rb +9 -19
  21. data/Plugins/Apps/ClickHouse/ClickHouse.container +28 -0
  22. data/Plugins/Apps/ClickHouse/ClickHouse.lmm.rb +113 -0
  23. data/Plugins/Apps/ClickHouse/Config/listen.yaml +2 -0
  24. data/Plugins/Apps/ClickHouse/Config/logger.yaml +8 -0
  25. data/Plugins/Apps/ClickHouse/Config/zookeepers.yaml +5 -0
  26. data/Plugins/Apps/ClickHouse/Connection.rb +96 -0
  27. data/Plugins/Apps/Discourse/Discourse-Sidekiq.container +5 -0
  28. data/Plugins/Apps/Discourse/Discourse.conf.erb +1 -4
  29. data/Plugins/Apps/Discourse/Discourse.container +4 -0
  30. data/Plugins/Apps/Discourse/Discourse.lmm.rb +116 -55
  31. data/Plugins/Apps/Dovecot/Dovecot.lmm.rb +74 -62
  32. data/Plugins/Apps/ERPNext/ERPNext-Frontend.container +6 -1
  33. data/Plugins/Apps/ERPNext/ERPNext-Queue.container +5 -0
  34. data/Plugins/Apps/ERPNext/ERPNext-Scheduler.container +5 -0
  35. data/Plugins/Apps/ERPNext/ERPNext-Websocket.container +6 -1
  36. data/Plugins/Apps/ERPNext/ERPNext.container +6 -1
  37. data/Plugins/Apps/ERPNext/ERPNext.lmm.rb +138 -127
  38. data/Plugins/Apps/GitLab/GitLab.container +6 -0
  39. data/Plugins/Apps/GitLab/GitLab.lmm.rb +43 -49
  40. data/Plugins/Apps/Homepage/Homepage.conf.erb +86 -0
  41. data/Plugins/Apps/Homepage/Homepage.container +19 -0
  42. data/Plugins/Apps/Homepage/Homepage.lmm.rb +54 -0
  43. data/Plugins/Apps/IPFS/IPFS.conf.erb +0 -3
  44. data/Plugins/Apps/IPFS/IPFS.lmm.rb +0 -1
  45. data/Plugins/Apps/InfluxDB/InfluxDB.conf.erb +0 -3
  46. data/Plugins/Apps/InfluxDB/InfluxDB.lmm.rb +0 -1
  47. data/Plugins/Apps/Jackett/Jackett.conf.erb +0 -3
  48. data/Plugins/Apps/Jackett/Jackett.lmm.rb +0 -1
  49. data/Plugins/Apps/Jellyfin/Jellyfin.conf.erb +0 -3
  50. data/Plugins/Apps/Jellyfin/Jellyfin.lmm.rb +0 -1
  51. data/Plugins/Apps/LetsEncrypt/LetsEncrypt.lmm.rb +49 -28
  52. data/Plugins/Apps/LibreTranslate/LibreTranslate.container +21 -0
  53. data/Plugins/Apps/LibreTranslate/LibreTranslate.lmm.rb +34 -0
  54. data/Plugins/Apps/Lobsters/Containerfile +81 -0
  55. data/Plugins/Apps/Lobsters/Lobsters-Tasks.container +26 -0
  56. data/Plugins/Apps/Lobsters/Lobsters.conf.erb +99 -0
  57. data/Plugins/Apps/Lobsters/Lobsters.container +27 -0
  58. data/Plugins/Apps/Lobsters/Lobsters.lmm.rb +196 -0
  59. data/Plugins/Apps/Lobsters/crontab +3 -0
  60. data/Plugins/Apps/Lobsters/database.yml +26 -0
  61. data/Plugins/Apps/Lobsters/entrypoint.sh +30 -0
  62. data/Plugins/Apps/Lobsters/generateCredentials.rb +19 -0
  63. data/Plugins/Apps/Lobsters/lobsters-cron.sh +25 -0
  64. data/Plugins/Apps/Lobsters/lobsters-daily.sh +23 -0
  65. data/Plugins/Apps/Lobsters/puma.rb +49 -0
  66. data/Plugins/Apps/MariaDB/Connection.rb +55 -0
  67. data/Plugins/Apps/MariaDB/MariaDB.lmm.rb +60 -53
  68. data/Plugins/Apps/Mastodon/Mastodon-Sidekiq.container +22 -0
  69. data/Plugins/Apps/Mastodon/Mastodon-Streaming.container +20 -0
  70. data/Plugins/Apps/Mastodon/Mastodon.conf.erb +34 -45
  71. data/Plugins/Apps/Mastodon/Mastodon.container +28 -0
  72. data/Plugins/Apps/Mastodon/Mastodon.lmm.rb +240 -5
  73. data/Plugins/Apps/Mastodon/configlmm.rake +30 -0
  74. data/Plugins/Apps/Mastodon/entrypoint.sh +16 -0
  75. data/Plugins/Apps/Matrix/Element.container +5 -0
  76. data/Plugins/Apps/Matrix/Matrix.conf.erb +2 -8
  77. data/Plugins/Apps/Matrix/Matrix.lmm.rb +100 -71
  78. data/Plugins/Apps/Matrix/Synapse.container +5 -0
  79. data/Plugins/Apps/Netdata/Netdata.conf.erb +0 -3
  80. data/Plugins/Apps/Netdata/Netdata.lmm.rb +0 -1
  81. data/Plugins/Apps/Nextcloud/Nextcloud.conf.erb +3 -4
  82. data/Plugins/Apps/Nextcloud/Nextcloud.lmm.rb +150 -68
  83. data/Plugins/Apps/Nextcloud/autoconfig.php +13 -0
  84. data/Plugins/Apps/Nextcloud/config.php +10 -1
  85. data/Plugins/Apps/Nextcloud/nextcloudcron.service +8 -0
  86. data/Plugins/Apps/Nextcloud/nextcloudcron.timer +10 -0
  87. data/Plugins/Apps/Nginx/Connection.rb +93 -0
  88. data/Plugins/Apps/Nginx/conf.d/configlmm.conf +50 -9
  89. data/Plugins/Apps/Nginx/conf.d/languages.conf +21 -0
  90. data/Plugins/Apps/Nginx/config-lmm/errors.conf +25 -20
  91. data/Plugins/Apps/Nginx/config-lmm/gateway-errors.conf +20 -0
  92. data/Plugins/Apps/Nginx/config-lmm/proxy.conf +1 -1
  93. data/Plugins/Apps/Nginx/main.conf.erb +7 -3
  94. data/Plugins/Apps/Nginx/nginx.conf +2 -2
  95. data/Plugins/Apps/Nginx/nginx.lmm.rb +99 -81
  96. data/Plugins/Apps/Nginx/proxy.conf.erb +11 -3
  97. data/Plugins/Apps/Odoo/Odoo.conf.erb +0 -3
  98. data/Plugins/Apps/Odoo/Odoo.container +5 -0
  99. data/Plugins/Apps/Odoo/Odoo.lmm.rb +4 -5
  100. data/Plugins/Apps/Ollama/Ollama.container +26 -0
  101. data/Plugins/Apps/Ollama/Ollama.lmm.rb +73 -0
  102. data/Plugins/Apps/OpenTelemetry/Config/config.yaml +704 -0
  103. data/Plugins/Apps/OpenTelemetry/OpenTelemetry.lmm.rb +154 -0
  104. data/Plugins/Apps/OpenVidu/Ingress.container +5 -0
  105. data/Plugins/Apps/OpenVidu/OpenVidu.conf.erb +0 -3
  106. data/Plugins/Apps/OpenVidu/OpenVidu.container +5 -0
  107. data/Plugins/Apps/OpenVidu/OpenVidu.lmm.rb +7 -3
  108. data/Plugins/Apps/OpenVidu/OpenViduCall.conf.erb +0 -3
  109. data/Plugins/Apps/OpenVidu/OpenViduCall.container +5 -0
  110. data/Plugins/Apps/PHP-FPM/Connection.rb +91 -0
  111. data/Plugins/Apps/PHP-FPM/PHP-FPM.lmm.rb +31 -4
  112. data/Plugins/Apps/Peppermint/Peppermint.conf.erb +2 -5
  113. data/Plugins/Apps/Peppermint/Peppermint.container +5 -0
  114. data/Plugins/Apps/Peppermint/Peppermint.lmm.rb +29 -33
  115. data/Plugins/Apps/Perplexica/Perplexica.container +25 -0
  116. data/Plugins/Apps/Perplexica/Perplexica.lmm.rb +92 -0
  117. data/Plugins/Apps/Perplexica/config.toml +26 -0
  118. data/Plugins/Apps/Podman/Connection.rb +24 -0
  119. data/Plugins/Apps/Podman/Podman.lmm.rb +80 -0
  120. data/Plugins/Apps/Podman/storage.conf +6 -0
  121. data/Plugins/Apps/Postfix/Postfix.lmm.rb +242 -164
  122. data/Plugins/Apps/PostgreSQL/Connection.rb +97 -0
  123. data/Plugins/Apps/PostgreSQL/PostgreSQL.lmm.rb +184 -148
  124. data/Plugins/Apps/Pterodactyl/Pterodactyl.conf.erb +0 -3
  125. data/Plugins/Apps/Pterodactyl/Pterodactyl.lmm.rb +0 -2
  126. data/Plugins/Apps/Pterodactyl/Wings.conf.erb +0 -3
  127. data/Plugins/Apps/RVM/RVM.lmm.rb +57 -0
  128. data/Plugins/Apps/Roundcube/Roundcube.conf.erb +0 -3
  129. data/Plugins/Apps/Roundcube/Roundcube.lmm.rb +15 -19
  130. data/Plugins/Apps/SSH/SSH.lmm.rb +9 -15
  131. data/Plugins/Apps/SearXNG/SearXNG.container +22 -0
  132. data/Plugins/Apps/SearXNG/SearXNG.lmm.rb +79 -0
  133. data/Plugins/Apps/SearXNG/limiter.toml +40 -0
  134. data/Plugins/Apps/SearXNG/settings.yml +2 -0
  135. data/Plugins/Apps/SigNoz/Config/alerts.yml +11 -0
  136. data/Plugins/Apps/SigNoz/Config/otel-collector-config.yaml +110 -0
  137. data/Plugins/Apps/SigNoz/Config/otel-collector-opamp-config.yaml +1 -0
  138. data/Plugins/Apps/SigNoz/Config/prometheus.yml +18 -0
  139. data/Plugins/Apps/SigNoz/SigNoz-Collector.container +23 -0
  140. data/Plugins/Apps/SigNoz/SigNoz-Migrator.container +17 -0
  141. data/Plugins/Apps/SigNoz/SigNoz.conf.erb +61 -0
  142. data/Plugins/Apps/SigNoz/SigNoz.container +26 -0
  143. data/Plugins/Apps/SigNoz/SigNoz.lmm.rb +319 -0
  144. data/Plugins/Apps/Solr/log4j2.xml +89 -0
  145. data/Plugins/Apps/Solr/solr.lmm.rb +82 -0
  146. data/Plugins/Apps/Sunshine/Sunshine.conf.erb +0 -3
  147. data/Plugins/Apps/Sunshine/Sunshine.lmm.rb +0 -1
  148. data/Plugins/Apps/Tunnel/tunnel.lmm.rb +33 -37
  149. data/Plugins/Apps/UVdesk/UVdesk.conf.erb +0 -3
  150. data/Plugins/Apps/Umami/Umami.container +19 -0
  151. data/Plugins/Apps/Umami/Umami.lmm.rb +108 -0
  152. data/Plugins/Apps/Valkey/Valkey.lmm.rb +54 -42
  153. data/Plugins/Apps/Vaultwarden/Vaultwarden.conf.erb +9 -6
  154. data/Plugins/Apps/Vaultwarden/Vaultwarden.container +7 -1
  155. data/Plugins/Apps/Vaultwarden/Vaultwarden.lmm.rb +64 -29
  156. data/Plugins/Apps/Wiki.js/Wiki.js.conf.erb +1 -4
  157. data/Plugins/Apps/Wiki.js/Wiki.js.container +5 -0
  158. data/Plugins/Apps/Wiki.js/Wiki.js.lmm.rb +31 -37
  159. data/Plugins/Apps/YaCy/YaCy.conf.erb +93 -0
  160. data/Plugins/Apps/YaCy/YaCy.container +21 -0
  161. data/Plugins/Apps/YaCy/YaCy.lmm.rb +160 -0
  162. data/Plugins/Apps/ZooKeeper/ZooKeeper.container +24 -0
  163. data/Plugins/Apps/ZooKeeper/ZooKeeper.lmm.rb +68 -0
  164. data/Plugins/Apps/bitmagnet/bitmagnet.conf.erb +0 -3
  165. data/Plugins/Apps/bitmagnet/bitmagnet.lmm.rb +0 -1
  166. data/Plugins/Apps/gollum/gollum.conf.erb +2 -4
  167. data/Plugins/Apps/gollum/gollum.container +6 -0
  168. data/Plugins/Apps/gollum/gollum.lmm.rb +51 -50
  169. data/Plugins/Apps/llama.cpp/llama.cpp.container +28 -0
  170. data/Plugins/Apps/llama.cpp/llama.cpp.lmm.rb +90 -0
  171. data/Plugins/Apps/vLLM/vLLM.container +32 -0
  172. data/Plugins/Apps/vLLM/vLLM.lmm.rb +89 -0
  173. data/Plugins/OS/General/Utils.lmm.rb +26 -0
  174. data/Plugins/OS/Linux/Connection.rb +472 -0
  175. data/Plugins/OS/Linux/Debian/preseed.cfg.erb +25 -6
  176. data/Plugins/OS/Linux/Flavours.yaml +13 -0
  177. data/Plugins/OS/Linux/Grub/grub.cfg +10 -0
  178. data/Plugins/OS/Linux/HTTP.rb +32 -0
  179. data/Plugins/OS/Linux/Linux.lmm.rb +533 -187
  180. data/Plugins/OS/Linux/Packages.yaml +20 -1
  181. data/Plugins/OS/Linux/Services.yaml +8 -0
  182. data/Plugins/OS/Linux/Shell.rb +70 -0
  183. data/Plugins/OS/Linux/Syslinux/default +8 -0
  184. data/Plugins/OS/Linux/WireGuard/WireGuard.lmm.rb +83 -59
  185. data/Plugins/OS/Linux/WireGuard/wg0.conf.erb +3 -0
  186. data/Plugins/OS/Linux/openSUSE/autoinst.xml.erb +29 -3
  187. data/Plugins/OS/Linux/systemd/systemd.lmm.rb +13 -11
  188. data/Plugins/OS/Routers/Aruba/ArubaInstant.lmm.rb +6 -5
  189. data/Plugins/Platforms/GitHub.lmm.rb +73 -28
  190. data/Plugins/Platforms/GoDaddy/GoDaddy.lmm.rb +9 -6
  191. data/Plugins/Platforms/Proxmox/Proxmox.lmm.rb +402 -0
  192. data/Plugins/Platforms/Proxmox/XTerm.rb +321 -0
  193. data/Plugins/Platforms/libvirt/libvirt.lmm.rb +38 -13
  194. data/Plugins/Platforms/porkbun.lmm.rb +12 -2
  195. data/Plugins/Platforms/porkbun_spec.rb +2 -2
  196. data/Plugins/Services/DNS/AmberBit.lmm.rb +1 -1
  197. data/Plugins/Services/DNS/ArubaItDNS.lmm.rb +1 -1
  198. data/Plugins/Services/DNS/NICLV.lmm.rb +1 -1
  199. data/Plugins/Services/DNS/PowerDNS.lmm.rb +70 -68
  200. data/Plugins/Services/DNS/tonic.lmm.rb +22 -12
  201. data/lib/ConfigLMM/Framework/plugins/dns.rb +4 -3
  202. data/lib/ConfigLMM/Framework/plugins/linuxApp.rb +145 -184
  203. data/lib/ConfigLMM/Framework/plugins/nginxApp.rb +34 -17
  204. data/lib/ConfigLMM/Framework/plugins/plugin.rb +53 -181
  205. data/lib/ConfigLMM/Framework/plugins/store.rb +4 -4
  206. data/lib/ConfigLMM/Framework/variables.rb +75 -0
  207. data/lib/ConfigLMM/Framework.rb +1 -0
  208. data/lib/ConfigLMM/cli.rb +12 -6
  209. data/lib/ConfigLMM/commands/configsCommand.rb +37 -6
  210. data/lib/ConfigLMM/commands/diff.rb +33 -9
  211. data/lib/ConfigLMM/context.rb +22 -3
  212. data/lib/ConfigLMM/io/configList.rb +82 -6
  213. data/lib/ConfigLMM/io/connection.rb +143 -0
  214. data/lib/ConfigLMM/io/dhcp.rb +330 -0
  215. data/lib/ConfigLMM/io/http.rb +78 -0
  216. data/lib/ConfigLMM/io/local.rb +207 -0
  217. data/lib/ConfigLMM/io/pxe.rb +92 -0
  218. data/lib/ConfigLMM/io/ssh.rb +156 -0
  219. data/lib/ConfigLMM/io/tftp.rb +105 -0
  220. data/lib/ConfigLMM/io.rb +2 -0
  221. data/lib/ConfigLMM/secrets/envStore.rb +39 -0
  222. data/lib/ConfigLMM/secrets/fileStore.rb +43 -0
  223. data/lib/ConfigLMM/state.rb +2 -1
  224. data/lib/ConfigLMM/version.rb +2 -1
  225. data/lib/ConfigLMM.rb +1 -0
  226. data/{Examples → scripts}/configlmmAuth.sh +7 -5
  227. metadata +205 -8
@@ -1,135 +1,199 @@
1
1
 
2
+ require_relative 'Connection'
3
+
2
4
  require 'addressable/uri'
3
5
  require 'http'
4
6
  require 'securerandom'
5
7
  require 'shellwords'
8
+ require 'ipaddr'
6
9
 
7
10
  module ConfigLMM
8
11
  module LMM
9
- class Linux < Framework::LinuxApp
12
+ class Linux < Framework::Plugin
10
13
 
11
- ISO_LOCATION = '~/.cache/configlmm/images/'
14
+ IMAGE_LOCATION = '~/.cache/configlmm/images/'
12
15
  HOSTS_FILE = '/etc/hosts'
13
16
  FSTAB_FILE = '/etc/fstab'
17
+ SUBUID_FILE = '/etc/subuid'
18
+ SUBGID_FILE = '/etc/subgid'
14
19
  SSH_CONFIG = '~/.ssh/config'
15
20
  SYSCTL_FILE = '/etc/sysctl.d/90-configlmm.conf'
16
21
  FIREWALL_PACKAGE = 'firewalld'
17
22
  FIREWALL_SERVICE = 'firewalld'
18
23
 
24
+ ARCH_NAME = 'Arch Linux'
25
+ SUSE_NAME = 'openSUSE Leap'
26
+ PROXMOXVE_NAME = 'Proxmox VE'
27
+ DEBIAN_NAME = 'Debian'
28
+
19
29
  def actionLinuxBuild(id, target, activeState, context, options)
20
- prepareConfig(target)
30
+ prepareConfig(target, context)
21
31
  buildHostsFile(id, target, options)
22
32
  buildSSHConfig(id, target, options)
23
33
  buildAutoInstall(id, target, options)
24
34
  end
25
35
 
26
36
  def actionLinuxDeploy(id, target, activeState, context, options)
27
- prepareConfig(target)
37
+ prepareConfig(target, context)
28
38
  if target['Location'] && target['Location'] != '@me'
29
39
  uri = Addressable::URI.parse(target['Location'])
30
40
  case uri.scheme
31
41
  when 'qemu'
32
42
  deployOverLibvirt(id, target, activeState, context, options)
43
+ when 'proxmox'
44
+ deployOverProxmox(id, target, activeState, context, options)
45
+ when 'pxe', 'pxe+http'
46
+ deployOverPXE(uri, id, target, activeState, context, options)
33
47
  when 'ssh'
34
- deployOverSSH(uri, id, target, activeState, context, options)
48
+ self.withConnection(uri, target) do |connection|
49
+ self.class.withConnection(connection) do |connection|
50
+ deployOverConnection(connection, id, target, activeState, context, options)
51
+ end
52
+ end
35
53
  else
36
54
  raise Framework::PluginProcessError.new("#{id}: Unknown protocol: #{uri.scheme}!")
37
55
  end
38
56
  else
39
- deployLocal(target, options)
57
+ self.class.withConnection(Local.new(prompt, logger)) do |connection|
58
+ deployLocal(connection, target, options)
59
+ end
40
60
  end
41
61
  if target['AlternativeLocation']
42
- uri = Addressable::URI.parse(target['AlternativeLocation'])
43
- raise Framework::PluginProcessError.new("#{id}: Unsupported protocol: #{uri.scheme}!") if uri.scheme != 'ssh'
44
- deployOverSSH(uri, id, target, activeState, context, options)
62
+ self.withConnection(target['AlternativeLocation'], target) do |connection|
63
+ self.class.withConnection(connection) do |connection|
64
+ deployOverConnection(connection, id, target, activeState, context, options)
65
+ end
66
+ end
45
67
  end
46
68
  end
47
69
 
48
- def deployOverSSH(locationUri, id, target, activeState, context, options)
49
- self.class.sshStart(locationUri) do |ssh|
50
- if target['Domain'] || target['Hosts']
51
- hostsLines = []
52
- if target['Domain']
53
- envs = self.class.sshExec!(ssh, "env").split("\n")
54
- envVars = Hash[envs.map { |vars| vars.split('=', 2) }]
70
+ def deployOverConnection(connection, id, target, activeState, context, options)
71
+ if target['Domain'] || target['Hosts']
72
+ hostsLines = []
73
+ if target['Domain']
74
+ envs = connection.exec("env", false, { **options, 'dry' => false }).split("\n")
75
+ envVars = Hash[envs.map { |vars| vars.split('=', 2) }]
76
+ if envVars['SSH_CONNECTION']
55
77
  ipAddr = envVars['SSH_CONNECTION'].split[-2]
56
- hostsLines << ipAddr.ljust(16) + Addressable::IDNA.to_ascii(target['Domain']) + ' ' + target['Name'] + "\n"
57
- end
58
- target['Hosts'].to_a.each do |ip, entries|
59
- hostsLines << ip.ljust(16) + entries.join(' ') + "\n"
60
- end
61
- updateRemoteFile(ssh, HOSTS_FILE, options, false) do |fileLines|
62
- fileLines + hostsLines
78
+ hostsLines << getHostsLine(ipAddr, [Addressable::IDNA.to_ascii(target['Domain']), target['Name']]) + "\n"
63
79
  end
64
80
  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
81
+ target['Hosts'].to_a.each do |ip, entries|
82
+ hostsLines << getHostsLine(ip, entries) + "\n"
73
83
  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
84
+ connection.updateFile(HOSTS_FILE, options, false) do |fileLines|
85
+ fileLines + hostsLines
86
+ end
87
+ end
88
+ distroInfo = connection.distroInfo
89
+ convertFlavour(distroInfo, target, connection, options)
90
+ configureNetwork(distroInfo, target, connection, options)
91
+ if target['Tmpfs']
92
+ connection.exec("sed -i '/ \\/tmp /d' #{FSTAB_FILE}", false, options)
93
+ connection.updateFile(FSTAB_FILE, options, false) do |fileLines|
94
+ fileLines << "tmpfs /tmp tmpfs nodev,nosuid,size=#{target['Tmpfs']} 0 0\n"
95
+ end
96
+ end
97
+ if target['Sysctl']
98
+ connection.updateFile(SYSCTL_FILE, options, false) do |fileLines|
99
+ target['Sysctl'].each do |name, value|
100
+ fileLines << "#{name} = #{value}\n"
101
+ connection.exec("sysctl #{name}=#{value}", false, options)
81
102
  end
103
+ fileLines
82
104
  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']}'"
105
+ end
106
+ if target['Users']
107
+ target['Users'].each do |name, info|
108
+ userId = connection.exec("id -u #{name} 2>/dev/null", true, { **options, 'dry' => false }).strip
109
+ if userId.empty?
110
+ shell = ''
111
+ if info['Shell']
112
+ result = connection.exec("which #{info['Shell']}", true, { **options, 'dry' => false }).strip
113
+ if !result.empty? && !result.include?("no #{info['Shell']}")
114
+ shell = "--shell '#{result}'"
115
+ else
116
+ prompt.say("Shell '#{info['Shell']}' not found! Skipping setting!", :color => :red)
90
117
  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
118
  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")
119
+ params = '--create-home --user-group'
120
+ badname = '--badname'
121
+ badname = '--badnames' if distroInfo['Name'] == 'openSUSE Leap'
122
+ if info['System']
123
+ params += ' --system'
101
124
  end
125
+ connection.exec("useradd #{badname} #{params} #{shell} #{name}", false, options)
126
+ elsif info['Shell']
127
+ result = connection.exec("which #{info['Shell']}", true, { **options, 'dry' => false }).strip
128
+ if !result.empty? && !result.include?("no #{info['Shell']}")
129
+ shell = "--shell '#{result}'"
130
+ connection.exec("chsh #{shell} #{name}")
131
+ else
132
+ prompt.say("Shell '#{info['Shell']}' not found! Skipping setting!", :color => :red)
133
+ end
134
+ end
135
+ if info['Subuids']
136
+ connection.exec("sed -i '/^#{name}:.*/d' #{SUBUID_FILE}", false, options)
137
+ info['Subuids'].each do |id|
138
+ connection.exec("#{distroInfo['ModifyUser']} --add-subuids #{id} #{name}", false, options)
139
+ end
140
+ end
141
+ if info['Subgids']
142
+ connection.exec("sed -i '/^#{name}:.*/d' #{SUBGID_FILE}", false, options)
143
+ info['Subgids'].each do |id|
144
+ connection.exec("#{distroInfo['ModifyUser']} --add-subgids #{id} #{name}", false, options)
145
+ end
146
+ end
147
+ homeDir = connection.exec("getent passwd #{name} | cut -d ':' -f 6", false, { **options, 'dry' => false }).strip
148
+ hostname = connection.exec("hostname", false, { **options, 'dry' => false }).strip
149
+ keyFile = homeDir + "/.ssh/id_ed25519"
150
+ if info['SSH'].to_h['Key'] && !connection.filePresent?(keyFile, options)
151
+ connection.exec("mkdir -p #{homeDir}/.ssh", false, options)
152
+ connection.exec("ssh-keygen -t ed25519 -f #{keyFile} -P '' -C '#{name}@#{hostname}'", false, options)
153
+ connection.exec("chown -R #{name}:#{name} #{homeDir}/.ssh", false, options)
154
+ end
155
+ if !info['SSH'].to_h['Config'].to_h.empty?
156
+ connection.exec("mkdir -p #{homeDir}/.ssh", false, options)
157
+ deploySSHConfig(connection, info['SSH']['Config'], "#{homeDir}/.ssh/config", target, options)
158
+ connection.exec("chown -R #{name}:#{name} #{homeDir}/.ssh", false, options)
102
159
  end
103
160
  end
104
- self.executeCommands(target['Execute'], ssh)
105
161
  end
106
162
  if target['Firewall'] && target['Firewall'] != 'no'
107
- self.ensurePackage(FIREWALL_PACKAGE, locationUri)
108
- self.ensureServiceAutoStart(FIREWALL_SERVICE, locationUri)
109
- self.startService(FIREWALL_SERVICE, locationUri)
163
+ connection.ensurePackage(FIREWALL_PACKAGE, options)
164
+ connection.ensureServiceAutoStart(FIREWALL_SERVICE, options)
165
+ connection.startService(FIREWALL_SERVICE, options)
110
166
  end
167
+ if !target['Packages'].to_a.empty?
168
+ connection.ensurePackages(target['Packages'], options)
169
+ end
170
+ target['Services'].to_a.each do |service|
171
+ connection.ensureServiceAutoStart(service, options)
172
+ connection.startService(service, options)
173
+ end
174
+ self.executeCommands(target['Execute'], connection)
111
175
  end
112
176
 
113
- def convertFlavour(distroInfo, target, ssh, options)
177
+ def convertFlavour(distroInfo, target, connection, options)
114
178
  if target['Flavour']
115
179
  if target['Flavour'] == PROXMOXVE_NAME
116
180
  if distroInfo['Name'] != DEBIAN_NAME
117
181
  raise 'Can\'t convert flavour!'
118
182
  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?
183
+ if connection.filePresent?('/etc/apt/sources.list.d/pve-install-repo.list', { **options, 'dry' => false })
184
+ needInstall = connection.exec('dpkg --status proxmox-ve 2>/dev/null | grep Status | grep installed | wc -l', false, { **options, 'dry' => false }).strip.to_i.zero?
121
185
  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)
186
+ connection.exec('DEBIAN_FRONTEND=noninteractive apt install --assume-yes proxmox-ve postfix open-iscsi chrony', false, options)
187
+ connection.exec("apt remove --assume-yes os-prober linux-image-amd64 'linux-image-*'", false, options)
188
+ connection.exec('update-grub', false, options)
125
189
  end
126
190
  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)
191
+ connection.exec('echo "deb [arch=amd64] http://download.proxmox.com/debian/pve bookworm pve-no-subscription" > /etc/apt/sources.list.d/pve-install-repo.list', false, options)
128
192
  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)
193
+ connection.upload(options['output'] + 'proxmox-release-bookworm.gpg', '/etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg', options)
194
+ connection.exec('apt update && apt full-upgrade --assume-yes', false, options)
195
+ connection.exec('apt install --assume-yes proxmox-default-kernel', false, options)
196
+ connection.exec('systemctl reboot', false, options)
133
197
  end
134
198
  target['Network'] = {} unless target['Network'].is_a?(Hash)
135
199
  target['Network']['Interfaces'] = {} unless target['Network']['Interfaces'].is_a?(Hash)
@@ -150,51 +214,57 @@ module ConfigLMM
150
214
  end
151
215
  end
152
216
 
153
- def configureNetwork(distroInfo, target, ssh, options)
217
+ def configureNetwork(distroInfo, target, connection, options)
154
218
  if target['Network']
155
219
  if distroInfo['Name'] == 'openSUSE Leap'
156
- updateNetworkInterface(target['Network'], 'eth0', ssh, options)
220
+ updateNetworkInterface(target['Network'], 'eth0', connection, options)
157
221
  if target['Network']['Interfaces']
158
222
  target['Network']['Interfaces'].each do |interface, config|
159
- updateNetworkInterface(config, interface, ssh, options)
223
+ updateNetworkInterface(config, interface, connection, options)
160
224
  end
161
225
  end
162
226
  if target['Network']['DNS']
163
227
  configFile = '/etc/sysconfig/network/config'
164
228
  dns = target['Network']['DNS']
165
229
  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}")
230
+ connection.exec("sed -i 's|^NETCONFIG_DNS_STATIC_SERVERS=.*|NETCONFIG_DNS_STATIC_SERVERS=\"#{dns.join(' ')}\"|' #{configFile}", false, options)
167
231
  end
168
232
  if target['Network']['Gateway']
169
233
  routesFile = '/etc/sysconfig/network/routes'
170
- self.class.sshExec!(ssh, "sed -i 's|^default |#default |' #{routesFile}")
171
- updateRemoteFile(ssh, routesFile, options) do |fileLines|
234
+ connection.exec("sed -i 's|^default |#default |' #{routesFile}", false, options)
235
+ connection.updateFile(routesFile, options) do |fileLines|
172
236
  fileLines << "default #{target['Network']['Gateway']}\n"
173
237
  end
174
238
  end
175
239
  elsif distroInfo['Name'] == 'Debian'
176
- links = self.networkLinks(ssh)
240
+ links = self.networkLinks(connection)
177
241
  raise 'Didn\'t find network links!' if links.empty?
178
242
  linkType = nil
179
- dnsSearch = self.class.exec('cat /etc/resolv.conf | grep search', ssh).strip.split(' ').last
243
+ dnsSearch = connection.exec('cat /etc/resolv.conf | grep search', false, { **options, 'dry' => false }).strip.split(' ').last
180
244
  if target['Network'].is_a?(String)
181
245
  linkType = target['Network']
182
246
  target['Network'] = {}
183
247
  end
248
+ links.each do |link|
249
+ target['Network']['Interfaces'][link] = 'manual' unless target['Network']['Interfaces'].key?(link)
250
+ end
184
251
  if !target['Network'].key?('Interfaces') ||
185
252
  target['Network']['Interfaces'].to_h.empty? ||
186
253
  !target['Network']['Interfaces'].key?(links.first)
187
254
  target['Network']['Interfaces'] ||= {}
188
255
  if !linkType.nil?
189
256
  target['Network']['Interfaces'][links.first] = linkType
190
- else
257
+ elsif target['Network']['IP']
191
258
  target['Network']['Interfaces'][links.first] = {}
192
259
  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']
260
+ target['Network']['Interfaces'][links.first]['Gateway'] = target['Network']['Gateway'] if target['Network']['Gateway']
261
+ target['Network']['Interfaces'][links.first]['DNS'] = target['Network']['DNS'] if target['Network']['DNS']
195
262
  end
196
263
  end
197
264
  if target['Network']['Interfaces'].key?('vmbr0')
265
+ if !target['Network']['Interfaces']['vmbr0'].is_a?(Hash)
266
+ target['Network']['Interfaces']['vmbr0'] = { }
267
+ end
198
268
  if target['Network']['Interfaces']['vmbr0']['Ports'].nil?
199
269
  target['Network']['Interfaces']['vmbr0']['Ports'] = [links.first]
200
270
  target['Network']['Interfaces'][links.first] = 'manual'
@@ -202,9 +272,9 @@ module ConfigLMM
202
272
  end
203
273
  interfacesFile = '/etc/network/interfaces'
204
274
  localFile = options['output'] + '/' + SecureRandom.alphanumeric(10)
205
- ssh.scp.download!(interfacesFile, localFile)
275
+ connection.download(interfacesFile, localFile)
206
276
  fileLines = File.read(localFile).lines
207
- if fileLines.index(CONFIGLMM_SECTION_BEGIN).nil?
277
+ if fileLines.index(IO::Local::CONFIGLMM_SECTION_BEGIN).nil?
208
278
  lines = []
209
279
  iface = false
210
280
  fileLines.each do |line|
@@ -222,26 +292,51 @@ module ConfigLMM
222
292
  end
223
293
  end
224
294
  fileWrite(localFile, lines.join(), options[:dry])
225
- ssh.scp.upload!(localFile, interfacesFile)
295
+ connection.upload(localFile, interfacesFile)
226
296
  end
227
- self.updateRemoteFile(ssh, interfacesFile, options) do |fileLines|
297
+ connection.updateFile(interfacesFile, options) do |fileLines|
228
298
  target['Network']['Interfaces'].each do |name, data|
229
299
  fileLines << "auto #{name}\n"
300
+ data = 'manual' if data.nil?
230
301
  if data.is_a?(String)
231
302
  fileLines << "iface #{name} inet #{data}\n"
232
303
  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"
304
+ if data['IP']
305
+ fileLines << "iface #{name} inet static\n"
306
+ fileLines << " address #{data['IP']}\n"
307
+ fileLines << " gateway #{data['Gateway']}\n" if data['Gateway']
308
+ else
309
+ fileLines << "iface #{name} inet manual\n"
310
+ end
311
+ if data.key?('Ports')
312
+ if data['Ports']
313
+ fileLines << " bridge-ports #{data['Ports'].join(' ')}\n"
314
+ else
315
+ fileLines << " bridge-ports none\n"
316
+ end
238
317
  fileLines << " bridge-stp off\n"
239
318
  fileLines << " bridge-fd 0\n"
319
+ if data['VLANFiltering']
320
+ fileLines << " bridge-vlan-aware yes\n"
321
+ end
322
+ if data['VLANS']
323
+ fileLines << " bridge-vids #{data['VLANS']}\n"
324
+ end
325
+ end
326
+ if data['DNS']
327
+ fileLines << " # dns-* options are implemented by the resolvconf package, if installed\n"
328
+ fileLines << " dns-nameservers #{data['DNS']}\n"
329
+ fileLines << " dns-search #{dnsSearch}\n" if dnsSearch
240
330
  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"
331
+ if data['NAT']
332
+ addr = IPAddr.new(data['IP'])
333
+ sourceAddr = "#{addr.to_s}/#{addr.prefix}"
334
+ outputInterface = ''
335
+ if data['NAT'].is_a?(String)
336
+ outputInterface = " -o #{data['NAT']}"
337
+ end
338
+ fileLines << " post-up iptables -t nat -A POSTROUTING -s #{sourceAddr}#{outputInterface} -j MASQUERADE\n"
339
+ fileLines << " post-down iptables -t nat -D POSTROUTING -s #{sourceAddr}#{outputInterface} -j MASQUERADE\n"
245
340
  end
246
341
  end
247
342
  fileLines << "\n"
@@ -255,14 +350,17 @@ module ConfigLMM
255
350
  end
256
351
  end
257
352
 
258
- def updateNetworkInterface(config, interface, ssh, options)
353
+ def updateNetworkInterface(config, interface, connection, options)
259
354
  baseFile = '/etc/sysconfig/network/ifcfg-'
260
355
  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|
356
+ connection.exec("touch #{networkFile}", false, options)
357
+ connection.exec("sed -i \"/^BOOTPROTO=.*/d\" #{networkFile}", false, options)
358
+ connection.exec("sed -i \"/^STARTMODE=.*/d\" #{networkFile}", false, options)
359
+ connection.exec("sed -i \"/^ZONE=.*/d\" #{networkFile}", false, options)
360
+ if config['IP']
361
+ connection.exec("sed -i 's|^IPADDR=|#IPADDR=|' #{networkFile}", false, options)
362
+ end
363
+ connection.updateFile(networkFile, options, false) do |fileLines|
266
364
  fileLines << "STARTMODE=auto\n"
267
365
  fileLines << "ZONE=public\n"
268
366
  if config == 'dhcp'
@@ -271,7 +369,6 @@ module ConfigLMM
271
369
  fileLines << "BOOTPROTO=static\n"
272
370
  fileLines << "\n"
273
371
  if config['IP']
274
- self.class.sshExec!(ssh, "sed -i 's|^IPADDR=|#IPADDR=|' #{networkFile}")
275
372
  if config['IP'].is_a?(Array)
276
373
  config['IP'].each_with_index do |ip, i|
277
374
  c = "_#{i}"
@@ -287,13 +384,13 @@ module ConfigLMM
287
384
  end
288
385
  end
289
386
 
290
- def networkLinks(ssh)
291
- self.class.exec("ls /sys/class/net/", ssh).strip.split("\n").select { |name| name.start_with?('enp') }
387
+ def networkLinks(connection)
388
+ connection.exec("ls /sys/class/net/").strip.split("\n").select { |name| name.start_with?('enp') }
292
389
  end
293
390
 
294
- def deployLocal(target, options)
391
+ def deployLocal(connection, target, options)
295
392
  deployLocalHostsFile(target, options)
296
- deployLocalSSHConfig(target, options)
393
+ deployLocalSSHConfig(connection, target, options)
297
394
  if target['Sysctl']
298
395
  updateLocalFile(SYSCTL_FILE, options) do |fileLines|
299
396
  target['Sysctl'].each do |name, value|
@@ -305,35 +402,34 @@ module ConfigLMM
305
402
  end
306
403
  if target['Users']
307
404
  target['Users'].each do |name, info|
308
- userId = self.class.exec("id -u #{name} 2>/dev/null", nil, true).strip
405
+ userId = connection.exec("id -u #{name} 2>/dev/null", true, { **options, 'dry' => false }).strip
309
406
  if userId.empty?
310
407
  shell = ''
311
408
  if info['Shell']
312
409
  shell = "--shell '/usr/bin/#{info['Shell']}'"
313
410
  end
314
- distroInfo = self.class.currentDistroInfo(nil)
315
411
  badname = '--badname'
316
- badname = '--badnames' if distroInfo['Name'] == 'openSUSE Leap'
317
- self.class.exec("useradd #{badname} --create-home --user-group #{shell} #{name}")
412
+ badname = '--badnames' if connection.distroName == 'openSUSE Leap'
413
+ connection.exec("useradd #{badname} --create-home --user-group #{shell} #{name}", false, options)
318
414
  end
319
- homeDir = self.class.exec("getent passwd #{name} | cut -d ':' -f 6").strip
415
+ homeDir = connection.exec("getent passwd #{name} | cut -d ':' -f 6", false, options).strip
320
416
  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")
417
+ if info['SSH'].to_h['Key'] && !connection.filePresent?(keyFile, options)
418
+ connection.exec("mkdir -p #{homeDir}/.ssh", false, options)
419
+ connection.exec("ssh-keygen -t ed25519 -f #{keyFile} -P ''", false, options)
420
+ connection.exec("chown -R #{name}:#{name} #{homeDir}/.ssh", false, options)
325
421
  end
326
422
  end
327
423
  end
328
424
  if target['Firewall'] && target['Firewall'] != 'no'
329
- self.ensurePackage(FIREWALL_PACKAGE, locationUri)
330
- self.ensureServiceAutoStart(FIREWALL_SERVICE, locationUri)
331
- self.startService(FIREWALL_SERVICE, locationUri)
425
+ connection.ensurePackage(FIREWALL_PACKAGE, options)
426
+ connection.ensureServiceAutoStart(FIREWALL_SERVICE, options)
427
+ connection.startService(FIREWALL_SERVICE, options)
332
428
  end
333
429
  self.executeCommands(target['Execute'])
334
430
  end
335
431
 
336
- def executeCommands(commands, ssh = nil)
432
+ def executeCommands(commands, connection)
337
433
  return unless commands
338
434
 
339
435
  commands.each do |type, data|
@@ -341,7 +437,7 @@ module ConfigLMM
341
437
  when 'sh'
342
438
  data = [data] unless data.is_a?(Array)
343
439
  data.each do |cmd|
344
- self.class.exec(cmd, ssh)
440
+ connection.exec(cmd)
345
441
  end
346
442
  else
347
443
  raise 'Unimplemented!'
@@ -354,8 +450,68 @@ module ConfigLMM
354
450
  iso = installationISO(target['Distro'], target['Flavour'], location)
355
451
  iso = buildAutoInstallISO(id, iso, target, options)
356
452
  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')
453
+ context.secrets.print('Root password', target['Users']['root']['Password']) if target['Users']['root'].key?('Password')
454
+ end
455
+ end
456
+
457
+ def deployOverProxmox(id, target, activeState, context, options)
458
+ if target['LXC']
459
+ info = flavourInfo(target['Distro'], target['Flavour'])
460
+ if plugins[:Proxmox].createContainer(target, target['Location'], info, activeState, context)
461
+ context.secrets.print('Root password', target['Users']['root']['Password']) if target['Users']['root'].key?('Password')
462
+ end
463
+ else
464
+ location = Proxmox.getLocation(target['Location'])
465
+ iso = installationISO(target['Distro'], target['Flavour'], location)
466
+ iso = buildAutoInstallISO(id, iso, target, options)
467
+ if plugins[:Proxmox].createVM(target['Name'], target, target['Location'], iso, activeState, context)
468
+ context.secrets.print('Root password', target['Users']['root']['Password']) if target['Users']['root'].key?('Password')
469
+ end
470
+ end
471
+ end
472
+
473
+ def findNetworkIP(ipaddr)
474
+ addrs = Socket.getifaddrs.select { |ifaddr| ifaddr.addr.ipv4? && !ifaddr.addr.ipv4_loopback? }
475
+ addrs.each do |addr|
476
+ ip = addr.addr.ip_unpack.first
477
+ netmask = addr.netmask.ip_unpack.first
478
+ prefix = netmask.split('.').map(&:to_i).map { |octet| octet.to_s(2).count('1') }.sum
479
+ if IPAddr.new("#{ip}/#{prefix}") == IPAddr.new(ipaddr)
480
+ return ip
481
+ end
482
+ end
483
+ nil
484
+ end
485
+
486
+ def deployOverPXE(uri, id, target, activeState, context, options)
487
+ if target['AlternativeLocation']
488
+ return if self.ping(target['AlternativeLocation'], target)
489
+ end
490
+ networkOptions = target['DefaultNetwork'].dup
491
+ networkOptions['ID'] = id
492
+ clientIp = networkOptions['IP']
493
+ if clientIp == 'dhcp'
494
+ networkOptions['IP'] = nil
495
+ networkOptions['ClientIP'] = nil
496
+ else
497
+ networkOptions['ClientIP'] = clientIp.split('/').first
498
+ networkOptions['IP'] = findNetworkIP(clientIp)
358
499
  end
500
+ dir = preparePXE(id, target['Distro'], target['Flavour'], options)
501
+ bootFileResolver = Proc.new do |clientArch|
502
+ bootFile = 'lpxelinux.0'
503
+ bootFile = 'pxelinux.0' unless File.exist?(dir + bootFile)
504
+ if [0x0007, 0x0010].include?(clientArch) # EFI x64 and x64 UEFI HTTP
505
+ if target['Distro'] == SUSE_NAME
506
+ # Because we reuse Debian netboot archive...
507
+ bootFile = 'debian-installer/amd64/bootnetx64.efi'
508
+ elsif target['Distro'] == DEBIAN_NAME
509
+ bootFile = 'debian-installer/amd64/bootnetx64.efi'
510
+ end
511
+ end
512
+ bootFile
513
+ end
514
+ IO::PXE.boot(dir, uri, networkOptions, bootFileResolver, options, logger)
359
515
  end
360
516
 
361
517
  def buildHostsFile(id, target, options)
@@ -366,11 +522,11 @@ module ConfigLMM
366
522
  hosts += "#<ip-address> <hostname.domain.org> <hostname>\n"
367
523
  hosts += "127.0.0.1 localhost\n"
368
524
  hosts += "::1 localhost\n\n"
369
- hosts += CONFIGLMM_SECTION_BEGIN
525
+ hosts += IO::Local::CONFIGLMM_SECTION_BEGIN
370
526
  target['Hosts'].each do |ip, entries|
371
- hosts += ip.ljust(16) + entries.join(' ') + "\n"
527
+ hosts += getHostsLine(ip, entries) + "\n"
372
528
  end
373
- hosts += CONFIGLMM_SECTION_END
529
+ hosts += IO::Local::CONFIGLMM_SECTION_END
374
530
 
375
531
  path = options['output'] + '/' + id
376
532
  mkdir(path + '/etc', options[:dry])
@@ -381,16 +537,16 @@ module ConfigLMM
381
537
  def buildSSHConfig(id, target, options)
382
538
  if !target['SSH']['Config'].empty?
383
539
  sshConfig = "\n"
384
- sshConfig += CONFIGLMM_SECTION_BEGIN
540
+ sshConfig += IO::Local::CONFIGLMM_SECTION_BEGIN
385
541
  target['SSH']['Config'].each do |name, info|
386
542
  sshConfig += "Host #{name} #{info['HostName']}\n"
387
- sshConfig += " HostName " + info['HostName'] + "\n" if info['HostName']
388
- sshConfig += " Port " + info['Port'] + "\n" if info['Port']
543
+ sshConfig += " HostName " + Addressable::IDNA.to_ascii(info['HostName']) + "\n" if info['HostName']
544
+ sshConfig += " Port " + info['Port'].to_s + "\n" if info['Port']
389
545
  sshConfig += " User " + info['User'] + "\n" if info['User']
390
546
  sshConfig += " IdentityFile " + info['IdentityFile'] + "\n" if info['IdentityFile']
391
547
  sshConfig += "\n"
392
548
  end
393
- sshConfig += CONFIGLMM_SECTION_END
549
+ sshConfig += IO::Local::CONFIGLMM_SECTION_END
394
550
  sshConfig += "\n"
395
551
 
396
552
  configPath = options['output'] + '/' + id
@@ -400,70 +556,182 @@ module ConfigLMM
400
556
  end
401
557
 
402
558
  def buildAutoInstall(id, target, options)
403
- if target['Flavour'] == PROXMOXVE_NAME
559
+ config = prepareAutoInstallConfig(target)
560
+ if config['Flavour'] == PROXMOXVE_NAME
404
561
  outputFolder = options['output'] + '/' + id + '/'
405
562
  template = ERB.new(File.read(__dir__ + '/Proxmox/answer.toml.erb'))
406
- renderTemplate(template, target, outputFolder + 'answer.toml', options)
563
+ renderTemplate(template, config, outputFolder + 'answer.toml', options)
407
564
  File.write("#{outputFolder}/auto-installer-mode.toml", 'mode = "iso"')
408
- elsif target['Distro'] == SUSE_NAME
565
+ elsif config['Distro'] == SUSE_NAME
409
566
  outputFolder = options['output'] + '/' + id + '/'
410
567
  template = ERB.new(File.read(__dir__ + '/openSUSE/autoinst.xml.erb'))
411
- renderTemplate(template, target, outputFolder + 'autoinst.xml', options)
412
- elsif target['Distro'] == DEBIAN_NAME
568
+ renderTemplate(template, config, outputFolder + 'autoinst.xml', options)
569
+ elsif config['Distro'] == DEBIAN_NAME
570
+ variables = prepareDebianStorage(config, options)
413
571
  outputFolder = options['output'] + '/' + id + '/'
414
572
  template = ERB.new(File.read(__dir__ + '/Debian/preseed.cfg.erb'))
415
- renderTemplate(template, target, outputFolder + 'preseed.cfg', options)
573
+ renderTemplate(template, variables, outputFolder + 'preseed.cfg', options)
574
+ end
575
+ end
576
+
577
+ def prepareAutoInstallConfig(target)
578
+ config = target.dup
579
+ if config['Apps'].to_a.include?('sshd')
580
+ config['Services'] << :sshd
581
+ config['Services'].uniq!
582
+ end
583
+ config['Apps'] = Framework::LinuxApp.mapPackages(config['Apps'], config['Distro']) if config['Distro']
584
+ config['Apps'].delete_if { |app| app.include?('|') }
585
+ config
586
+ end
587
+
588
+ def prepareDebianStorage(target, options)
589
+ variables = target.dup
590
+ variables['AutoPartition'] = true
591
+ variables['Disks'] = []
592
+ if target.key?('StorageDevices')
593
+ if target['StorageDevices'].length == 1
594
+ variables['Disks'] = [target['StorageDevices'].first['Device']]
595
+ if !target['StorageDevices'].first['Partitions'].to_a.empty?
596
+ variables['AutoPartition'] = false
597
+ end
598
+ elsif !target['StorageDevices'].empty?
599
+ variables['AutoPartition'] = false
600
+ end
601
+ end
602
+ if target.key?('Mounts') && !target['Mounts'].empty?
603
+ variables['AutoPartition'] = false
416
604
  end
605
+ if !variables['AutoPartition']
606
+ logger.warn('Specified disk/partition configuration is not implemented! You will have to configure it manually!')
607
+ end
608
+ variables
609
+ end
610
+
611
+ def getHostsLine(ip, entries)
612
+ entries = entries.map { |entry| Addressable::IDNA.to_ascii(entry) }
613
+ # Hostnames should be case-insensitive but some implementations like in Alpine aren't
614
+ # so in case someone specified UpperCase hostname we also add lowercase one so that both would resolve
615
+ entries = (entries + entries.map(&:downcase)).uniq
616
+ ip.ljust(16) + entries.join(' ')
417
617
  end
418
618
 
419
619
  def deployLocalHostsFile(target, options)
420
620
  if target['Hosts']
421
621
  updateLocalFile(HOSTS_FILE, options) do |hostsLines|
422
622
  target['Hosts'].each do |ip, entries|
423
- hostsLines << ip.ljust(16) + entries.join(' ') + "\n"
623
+ hostsLines << getHostsLine(ip, entries) + "\n"
424
624
  end
425
625
  hostsLines
426
626
  end
427
627
  end
428
628
  end
429
629
 
430
- def deployLocalSSHConfig(target, options)
431
- if !target['SSH']['Config'].empty?
432
- updateLocalFile(File.expand_path(SSH_CONFIG), options) do |configLines|
433
- target['SSH']['Config'].each do |name, info|
434
- configLines << "Host #{name} #{info['HostName']}\n"
435
- configLines << " HostName " + info['HostName'] + "\n" if info['HostName']
436
- configLines << " Port " + info['Port'] + "\n" if info['Port']
437
- configLines << " User " + info['User'] + "\n" if info['User']
438
- configLines << " IdentityFile " + info['IdentityFile'] + "\n" if info['IdentityFile']
439
- end
440
- configLines
630
+ def deployLocalSSHConfig(connection, target, options)
631
+ deploySSHConfig(connection, target['SSH']['Config'], File.expand_path(SSH_CONFIG), target, options)
632
+ end
633
+
634
+ def deploySSHConfig(connection, config, path, target, options)
635
+ if !config.to_h.empty?
636
+ connection.updateFile(path, options) do |configLines|
637
+ processSSHConfig(config, configLines)
441
638
  end
442
639
  end
443
640
  end
444
641
 
445
- def installationISO(distro, flavour, location)
642
+ def processSSHConfig(config, lines)
643
+ config.each do |name, info|
644
+ lines << "Host #{name} #{info['HostName']}\n"
645
+ lines << " HostName " + Addressable::IDNA.to_ascii(info['HostName']) + "\n" if info['HostName']
646
+ lines << " Port " + info['Port'].to_s + "\n" if info['Port']
647
+ lines << " User " + info['User'] + "\n" if info['User']
648
+ lines << " IdentityFile " + info['IdentityFile'] + "\n" if info['IdentityFile']
649
+ end
650
+ lines
651
+ end
652
+
653
+ def flavourInfo(distro, flavour)
446
654
  url = nil
447
655
  flavour = distro unless flavour
448
656
  flavourInfo = YAML.load_file(__dir__ + '/Flavours.yaml')[flavour]
449
657
  if flavourInfo.nil?
450
658
  raise Framework::PluginProcessError.new("#{id}: Unknown Linux Distro: #{flavour}!")
451
659
  end
452
- url = flavourInfo['ISO']
453
- filename = File.basename(Addressable::URI.parse(url).path)
454
- iso = File.expand_path(ISO_LOCATION + filename)
455
- if !File.exist?(iso)
456
- mkdir(File.expand_path(ISO_LOCATION), false)
457
- prompt.say('Downloading... ' + url)
458
- response = HTTP.follow.get(url)
459
- raise "Failed to download file: #{response.status}" unless response.status.success?
460
- File.open(iso, 'wb') do |file|
461
- response.body.each do |chunk|
462
- file.write(chunk)
463
- end
660
+ flavourInfo
661
+ end
662
+
663
+ def downloadImage(url)
664
+ local.remoteDownload(url, IMAGE_LOCATION)
665
+ end
666
+
667
+ def installationISO(distro, flavour, location)
668
+ info = flavourInfo(distro, flavour)
669
+ downloadImage(info['ISO'])
670
+ end
671
+
672
+ def preparePXE(id, distro, flavour, options)
673
+ outputFolder = options['output'] + '/pxe/'
674
+ local.mkdir(outputFolder, false)
675
+ info = flavourInfo(distro, flavour)
676
+ image = nil
677
+ if info['PXE']
678
+ image = downloadImage(info['PXE'])
679
+ local.exec("tar --extract --file=#{image.shellescape} --directory=#{outputFolder}", false)
680
+ if distro == DEBIAN_NAME
681
+ local.copy(options['output'] + '/' + id + '/preseed.cfg', outputFolder, false)
682
+ local.exec("sed -i 's|default .*|default auto|' #{outputFolder}debian-installer/amd64/boot-screens/syslinux.cfg", false)
683
+ local.exec("sed -i 's|--- quiet|file=/preseed.cfg --- quiet|' #{outputFolder}debian-installer/amd64/boot-screens/adtxt.cfg", false)
684
+ local.exec("echo \"set default='... Automated install'\" >> #{outputFolder}debian-installer/amd64/grub/grub.cfg", false)
685
+ local.exec("echo 'set timeout=1' >> #{outputFolder}debian-installer/amd64/grub/grub.cfg", false)
686
+ local.exec("gunzip #{outputFolder}debian-installer/amd64/initrd.gz", false)
687
+ local.exec("cd #{options['output'] + '/' + id} && echo preseed.cfg | cpio -H newc -o -O #{outputFolder}debian-installer/amd64/initrd --append", false)
688
+ local.exec("gzip #{outputFolder}debian-installer/amd64/initrd", false)
464
689
  end
690
+ elsif distro == SUSE_NAME
691
+ # openSUSE doesn't provide netboot archive
692
+ # and grub.efi from it's ISO doesn't work
693
+ # so let's just reuse Debian archive
694
+ debianInfo = flavourInfo('Debian', nil)
695
+ debianImage = downloadImage(debianInfo['PXE'])
696
+ local.exec("tar --extract --file=#{debianImage.shellescape} --directory=#{outputFolder}", false)
697
+ local.exec("rm -rf #{outputFolder}pxelinux.cfg", false)
698
+
699
+ syslinux = downloadImage(flavourInfo('Syslinux', nil)['Archive'])
700
+ syslinuxFolder = options['output'] + '/syslinux'
701
+ local.mkdir(syslinuxFolder, false)
702
+ local.mkdir(outputFolder + 'pxelinux.cfg', false)
703
+ local.copy(options['output'] + '/' + id + '/autoinst.xml', outputFolder, false)
704
+ local.exec("tar --extract --file=#{syslinux.shellescape} --directory=#{syslinuxFolder}", false)
705
+ local.exec("cp #{syslinuxFolder}/*/bios/core/lpxelinux.0 #{outputFolder}", false)
706
+ local.exec("cp #{syslinuxFolder}/*/bios/com32/elflink/ldlinux/ldlinux.c32 #{outputFolder}", false)
707
+ local.exec("cp #{__dir__ + '/Syslinux/default'} #{outputFolder}pxelinux.cfg/", false)
708
+ local.exec("sed -i 's|$OPTIONS|install=#{info['FILES']} autoyast=/autoinst.xml|' #{outputFolder}pxelinux.cfg/default", false)
709
+
710
+ # If you have Network/IP configured in UEFI firmware which is different than what we use
711
+ # then Syslinux will use that IP instead of ours so you need to reset those settings
712
+ #
713
+ # Not using Syslinux.efi because for some reason it doesn't work - reboots after loading initrd
714
+ #local.exec("cp #{syslinuxFolder}/*/efi64/efi/syslinux.efi #{outputFolder}", false)
715
+ #local.exec("cp #{syslinuxFolder}/*/efi64/com32/elflink/ldlinux/ldlinux.e64 #{outputFolder}", false)
716
+
717
+ local.remoteDownload(info['FILES'] + 'boot/x86_64/loader/initrd', outputFolder)
718
+ local.remoteDownload(info['FILES'] + 'boot/x86_64/loader/linux', outputFolder)
719
+
720
+ # Not using grub.efi because UEFI Firemware says "Unsupported"
721
+ #local.remoteDownload(info['FILES'] + 'EFI/BOOT/bootx64.efi', outputFolder)
722
+ #local.remoteDownload(info['FILES'] + 'EFI/BOOT/grub.efi', outputFolder)
723
+ #local.remoteDownload(info['FILES'] + 'EFI/BOOT/grub.cfg', outputFolder)
724
+
725
+ local.exec("cp #{__dir__ + '/Grub/grub.cfg'} #{outputFolder}debian-installer/amd64/grub/", false)
726
+ local.exec("sed -i 's|$OPTIONS|install=#{info['FILES']} autoyast=/autoinst.xml|' #{outputFolder}debian-installer/amd64/grub/grub.cfg", false)
727
+
728
+ local.exec("xz --decompress --stdout #{outputFolder}initrd > #{outputFolder}initrd.decompressed", false)
729
+ local.exec("cd #{options['output'] + '/' + id} && echo autoinst.xml | cpio -H newc -o -O #{outputFolder}initrd.decompressed --append", false)
730
+ local.exec("xz --compress --stdout --check=crc32 #{outputFolder}initrd.decompressed > #{outputFolder}initrd", false)
731
+ else
732
+ raise 'Not implemented!'
465
733
  end
466
- iso
734
+ outputFolder
467
735
  end
468
736
 
469
737
  def buildAutoInstallISO(id, iso, target, options)
@@ -480,34 +748,53 @@ module ConfigLMM
480
748
  def buildISOAutoYaST(id, iso, target, options)
481
749
  outputFolder = options['output'] + '/iso/'
482
750
  mkdir(outputFolder, false)
483
- self.class.exec("xorriso -osirrox on -indev #{iso} -extract / #{outputFolder}")
751
+ local.exec("xorriso -osirrox on -indev #{iso} -extract / #{outputFolder}")
484
752
  FileUtils.chmod_R(0750, outputFolder) # Need to make it writeable so it can be deleted
485
753
  copy(options['output'] + '/' + id + '/autoinst.xml', outputFolder, false)
486
754
 
487
755
  cfg = outputFolder + "boot/x86_64/loader/isolinux.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}")
756
+ local.exec("sed -i 's|default harddisk|default linux|' #{cfg}")
757
+ local.exec("sed -i 's|append initrd=initrd splash=silent showopts|append initrd=initrd splash=silent autoyast=device://sr0/autoinst.xml|' #{cfg}")
758
+ local.exec("sed -i 's|prompt 1|prompt 0|' #{cfg}")
759
+ local.exec("sed -i 's|timeout 600|timeout 1|' #{cfg}")
760
+
761
+ ifcfg = ''
762
+ if target['DefaultNetwork']['IP'] != 'dhcp'
763
+ ifcfg = "ifcfg=\"eth*=#{target['DefaultNetwork']['IP']}"
764
+ if target['DefaultNetwork']['Gateway'] || target['DefaultNetwork']['DNS']
765
+ ifcfg += ',' + target['DefaultNetwork']['Gateway'].to_s
766
+ if target['DefaultNetwork']['DNS']
767
+ ifcfg += ',' + target['DefaultNetwork']['DNS']
768
+ ifcfg += ',' + Addressable::IDNA.to_ascii(target['Domain']) if target['Domain']
769
+ end
770
+ end
771
+ ifcfg += '"'
772
+ end
773
+
774
+ cfg = outputFolder + "EFI/BOOT/grub.cfg"
775
+ local.exec("sed -i 's|timeout=.*|timeout=1|' #{cfg}")
776
+ local.exec("sed -i 's|linux splash=silent|linux splash=silent #{ifcfg} autoyast=device://sr0/autoinst.xml|' #{cfg}")
492
777
 
493
778
  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}")
779
+ local.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
780
  patchedIso
496
781
  end
497
782
 
498
783
  def buildISOPreseed(id, iso, target, options)
499
784
  outputFolder = options['output'] + '/iso/'
500
785
  mkdir(outputFolder, false)
501
- self.class.exec("xorriso -osirrox on -indev #{iso} -extract / #{outputFolder}")
786
+ local.exec("xorriso -osirrox on -indev #{iso} -extract / #{outputFolder}", false)
502
787
  FileUtils.chmod_R(0750, outputFolder) # Need to make it writeable so it can be deleted
503
- copy(options['output'] + '/' + id + '/preseed.cfg', outputFolder, false)
788
+ local.copy(options['output'] + '/' + id + '/preseed.cfg', outputFolder, false)
504
789
 
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"}")
790
+ local.exec("sed -i 's|vga=788 --- quiet|auto=true file=/cdrom/preseed.cfg vga=788 --- quiet|' #{outputFolder}boot/grub/grub.cfg")
791
+ local.exec("echo \"set default='... Automated install'\" >> #{outputFolder}boot/grub/grub.cfg", false)
792
+ local.exec("echo 'set timeout=1' >> #{outputFolder}boot/grub/grub.cfg", false)
793
+ local.exec("sed -i 's|--- quiet|file=/cdrom/preseed.cfg --- quiet|' #{outputFolder + "isolinux/adgtk.cfg"}")
794
+ local.exec("sed -i 's|default .*|default autogui|' #{outputFolder + "isolinux/isolinux.cfg"}")
508
795
 
509
796
  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}")
797
+ local.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 -e boot/grub/efi.img -no-emul-boot -o #{patchedIso} #{outputFolder}")
511
798
  patchedIso
512
799
  end
513
800
 
@@ -522,25 +809,31 @@ module ConfigLMM
522
809
  patchedIso
523
810
  end
524
811
 
525
- def prepareConfig(target)
812
+ def self.withConnection(connection)
813
+ yield(LinuxConnection.new(connection))
814
+ end
815
+
816
+ def prepareConfig(target, context)
526
817
  target['SSH'] ||= {}
527
818
  target['SSH']['Config'] ||= {}
528
819
  target['Users'] ||= {}
529
820
  target['HostName'] = target['Name'] unless target['HostName']
530
821
 
531
- if ENV['LINUX_ROOT_PASSWORD_HASH']
822
+ if target['SecretId'] && context.secrets.load(target['SecretId'], 'ROOT_PASSWORD_HASH')
532
823
  target['Users']['root'] ||= {}
533
- target['Users']['root']['PasswordHash'] = ENV['LINUX_ROOT_PASSWORD_HASH']
534
- elsif ENV['LINUX_ROOT_PASSWORD']
824
+ target['Users']['root']['PasswordHash'] = context.secrets.load(target['SecretId'], 'ROOT_PASSWORD_HASH')
825
+ elsif target['SecretId'] && context.secrets.load(target['SecretId'], 'ROOT_PASSWORD')
535
826
  target['Users']['root'] ||= {}
536
- target['Users']['root']['Password'] = ENV['LINUX_ROOT_PASSWORD']
537
- target['Users']['root']['PasswordHash'] = self.class.linuxPasswordHash(ENV['LINUX_ROOT_PASSWORD'])
827
+ target['Users']['root']['Password'] = context.secrets.load(target['SecretId'], 'ROOT_PASSWORD')
828
+ target['Users']['root']['PasswordHash'] = self.class.linuxPasswordHash(target['Users']['root']['Password'])
538
829
  elsif target['Users'].key?('root')
539
- if !target['Users']['root']['Password'] &&
540
- !target['Users']['root']['PasswordHash']
541
- target['Users']['root']['Password'] = SecureRandom.urlsafe_base64(12)
542
- target['Users']['root']['PasswordHash'] = self.class.linuxPasswordHash(target['Users']['root']['Password'])
543
- elsif target['Users']['root']['Password'] == 'no'
830
+ if !target['Users']['root'].key?('Password') &&
831
+ !target['Users']['root'].key?('PasswordHash')
832
+ password = SecureRandom.urlsafe_base64(20)
833
+ context.secrets.store(target['SecretId'], 'ROOT_PASSWORD', password) if target['SecretId']
834
+ target['Users']['root']['Password'] = password
835
+ target['Users']['root']['PasswordHash'] = self.class.linuxPasswordHash(password)
836
+ elsif target['Users']['root']['Password'] == false
544
837
  target['Users']['root'].delete('Password')
545
838
  end
546
839
  end
@@ -548,7 +841,7 @@ module ConfigLMM
548
841
  target['Users'].each do |user, info|
549
842
  newKeys = []
550
843
  info['AuthorizedKeys'].to_a.each do |key|
551
- if key.start_with?('/') || key.start_with?('~')
844
+ if key.start_with?('/') || key.start_with?('~') || key.start_with?('.') || key.end_with?('.pub')
552
845
  newKeys << File.read(File.expand_path(key)).strip
553
846
  else
554
847
  newKeys << key
@@ -557,14 +850,67 @@ module ConfigLMM
557
850
  info['AuthorizedKeys'] = newKeys
558
851
  end
559
852
 
560
- packages = YAML.load_file(__dir__ + '/Packages.yaml')
561
853
  newApps = []
562
854
  target['Services'] ||= []
563
- if target['Apps'].to_a.include?('sshd')
564
- target['Services'] << 'sshd'
565
- target['Services'].uniq!
855
+ target['Packages'] = target['Apps'].dup
856
+ prepareDefaultNetwork(target)
857
+ end
858
+
859
+ def prepareDefaultNetwork(target)
860
+ target['DefaultNetwork'] = {}
861
+ target['DefaultNetwork']['IP'] = 'dhcp'
862
+ target['DefaultNetwork']['Interface'] = 'enp1s0'
863
+ target['DefaultNetwork']['VLAN'] = nil
864
+
865
+ if target['Network'].is_a?(Hash)
866
+ ipaddr = nil
867
+ if target['Network']['IP']
868
+ ipaddr = target['Network']['IP']
869
+ end
870
+ gateway = nil
871
+ if target['Network']['Gateway']
872
+ gateway = target['Network']['Gateway']
873
+ end
874
+ dns = nil
875
+ if target['Network']['DNS']
876
+ dns = target['Network']['DNS']
877
+ end
878
+ interface = nil
879
+ vlan = nil
880
+ if gateway.nil? && target['Network']['Interfaces'].is_a?(Hash)
881
+ cadidates = target['Network']['Interfaces'].select { |name, data| name[0] == 'e' }
882
+ target['DefaultNetwork']['Interface'] = cadidates.first unless cadidates.empty?
883
+ target['Network']['Interfaces'].each do |name, data|
884
+ if data.is_a?(Hash) && data['Gateway']
885
+ ipaddr = data['IP']
886
+ gateway = data['Gateway']
887
+ dns = data['DNS']
888
+ interface = name
889
+ if data['Ports']
890
+ interface = data['Ports'].first
891
+ end
892
+ if name.split('.').length == 2
893
+ interface, vlan = name.split('.')
894
+ if target['Network']['Interfaces'][interface].is_a?(Hash) &&
895
+ target['Network']['Interfaces']['Ports']
896
+ interface = target['Network']['Interfaces']['Ports'].first
897
+ end
898
+ end
899
+ break
900
+ end
901
+ end
902
+ end
903
+ if ipaddr
904
+ target['DefaultNetwork']['IP'] = ipaddr
905
+ addr = IPAddr.new(ipaddr)
906
+ target['DefaultNetwork']['Subnet'] = [((1 << 32) - 1) << (32 - addr.prefix)].pack('N').bytes.join('.')
907
+ target['DefaultNetwork']['Broadcast'] = addr.to_range.last.to_s
908
+ end
909
+ target['DefaultNetwork']['Gateway'] = gateway if gateway
910
+ target['DefaultNetwork']['DNS'] = dns if dns
911
+ target['DefaultNetwork']['Interface'] = interface if interface
912
+ target['DefaultNetwork']['VLAN'] = vlan if vlan
566
913
  end
567
- target['Apps'] = self.class.mapPackages(target['Apps'], target['Distro']) if target['Distro']
568
914
  end
569
915
 
570
916
  def self.linuxPasswordHash(password)