ConfigLMM 0.3.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 (250) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -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 +20 -0
  13. data/Plugins/Apps/Authentik/Authentik-Server.container +7 -1
  14. data/Plugins/Apps/Authentik/Authentik-Worker.container +7 -1
  15. data/Plugins/Apps/Authentik/Authentik.conf.erb +18 -6
  16. data/Plugins/Apps/Authentik/Authentik.lmm.rb +232 -45
  17. data/Plugins/Apps/BookStack/BookStack.conf.erb +38 -0
  18. data/Plugins/Apps/BookStack/BookStack.container +20 -0
  19. data/Plugins/Apps/BookStack/BookStack.lmm.rb +91 -0
  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 +22 -0
  28. data/Plugins/Apps/Discourse/Discourse.conf.erb +38 -0
  29. data/Plugins/Apps/Discourse/Discourse.container +21 -0
  30. data/Plugins/Apps/Discourse/Discourse.lmm.rb +156 -0
  31. data/Plugins/Apps/Dovecot/Dovecot.lmm.rb +87 -52
  32. data/Plugins/Apps/ERPNext/ERPNext-Frontend.container +24 -0
  33. data/Plugins/Apps/ERPNext/ERPNext-Queue.container +22 -0
  34. data/Plugins/Apps/ERPNext/ERPNext-Scheduler.container +22 -0
  35. data/Plugins/Apps/ERPNext/ERPNext-Websocket.container +24 -0
  36. data/Plugins/Apps/ERPNext/ERPNext.container +23 -0
  37. data/Plugins/Apps/ERPNext/ERPNext.lmm.rb +204 -0
  38. data/Plugins/Apps/ERPNext/ERPNext.network +12 -0
  39. data/Plugins/Apps/ERPNext/sites/apps.json +10 -0
  40. data/Plugins/Apps/ERPNext/sites/apps.txt +3 -0
  41. data/Plugins/Apps/ERPNext/sites/common_site_config.json +11 -0
  42. data/Plugins/Apps/GitLab/GitLab.container +9 -2
  43. data/Plugins/Apps/GitLab/GitLab.lmm.rb +52 -33
  44. data/Plugins/Apps/Homepage/Homepage.conf.erb +86 -0
  45. data/Plugins/Apps/Homepage/Homepage.container +19 -0
  46. data/Plugins/Apps/Homepage/Homepage.lmm.rb +54 -0
  47. data/Plugins/Apps/IPFS/IPFS.conf.erb +0 -3
  48. data/Plugins/Apps/IPFS/IPFS.lmm.rb +0 -1
  49. data/Plugins/Apps/InfluxDB/InfluxDB.conf.erb +0 -3
  50. data/Plugins/Apps/InfluxDB/InfluxDB.lmm.rb +0 -1
  51. data/Plugins/Apps/Jackett/Jackett.conf.erb +0 -3
  52. data/Plugins/Apps/Jackett/Jackett.lmm.rb +0 -1
  53. data/Plugins/Apps/Jellyfin/Jellyfin.conf.erb +0 -3
  54. data/Plugins/Apps/Jellyfin/Jellyfin.lmm.rb +0 -1
  55. data/Plugins/Apps/LetsEncrypt/LetsEncrypt.lmm.rb +78 -0
  56. data/Plugins/Apps/LetsEncrypt/hooks/dovecot.sh +2 -0
  57. data/Plugins/Apps/LetsEncrypt/hooks/nginx.sh +2 -0
  58. data/Plugins/Apps/LetsEncrypt/hooks/postfix.sh +2 -0
  59. data/Plugins/Apps/LetsEncrypt/renew-certificates.service +7 -0
  60. data/Plugins/Apps/LetsEncrypt/renew-certificates.timer +12 -0
  61. data/Plugins/Apps/LetsEncrypt/rfc2136.ini +11 -0
  62. data/Plugins/Apps/LibreTranslate/LibreTranslate.container +21 -0
  63. data/Plugins/Apps/LibreTranslate/LibreTranslate.lmm.rb +34 -0
  64. data/Plugins/Apps/Lobsters/Containerfile +81 -0
  65. data/Plugins/Apps/Lobsters/Lobsters-Tasks.container +26 -0
  66. data/Plugins/Apps/Lobsters/Lobsters.conf.erb +99 -0
  67. data/Plugins/Apps/Lobsters/Lobsters.container +27 -0
  68. data/Plugins/Apps/Lobsters/Lobsters.lmm.rb +196 -0
  69. data/Plugins/Apps/Lobsters/crontab +3 -0
  70. data/Plugins/Apps/Lobsters/database.yml +26 -0
  71. data/Plugins/Apps/Lobsters/entrypoint.sh +30 -0
  72. data/Plugins/Apps/Lobsters/generateCredentials.rb +19 -0
  73. data/Plugins/Apps/Lobsters/lobsters-cron.sh +25 -0
  74. data/Plugins/Apps/Lobsters/lobsters-daily.sh +23 -0
  75. data/Plugins/Apps/Lobsters/puma.rb +49 -0
  76. data/Plugins/Apps/MariaDB/Connection.rb +55 -0
  77. data/Plugins/Apps/MariaDB/MariaDB.lmm.rb +122 -0
  78. data/Plugins/Apps/Mastodon/Mastodon-Sidekiq.container +22 -0
  79. data/Plugins/Apps/Mastodon/Mastodon-Streaming.container +20 -0
  80. data/Plugins/Apps/Mastodon/Mastodon.conf.erb +34 -45
  81. data/Plugins/Apps/Mastodon/Mastodon.container +28 -0
  82. data/Plugins/Apps/Mastodon/Mastodon.lmm.rb +240 -5
  83. data/Plugins/Apps/Mastodon/configlmm.rake +30 -0
  84. data/Plugins/Apps/Mastodon/entrypoint.sh +16 -0
  85. data/Plugins/Apps/Matrix/Element.container +19 -0
  86. data/Plugins/Apps/Matrix/Matrix.conf.erb +47 -9
  87. data/Plugins/Apps/Matrix/Matrix.lmm.rb +119 -5
  88. data/Plugins/Apps/Matrix/Synapse.container +22 -0
  89. data/Plugins/Apps/Matrix/config.json +50 -0
  90. data/Plugins/Apps/Matrix/homeserver.yaml +70 -0
  91. data/Plugins/Apps/Matrix/log.config +30 -0
  92. data/Plugins/Apps/Netdata/Netdata.conf.erb +0 -3
  93. data/Plugins/Apps/Netdata/Netdata.lmm.rb +0 -1
  94. data/Plugins/Apps/Nextcloud/Nextcloud.conf.erb +3 -4
  95. data/Plugins/Apps/Nextcloud/Nextcloud.lmm.rb +155 -48
  96. data/Plugins/Apps/Nextcloud/autoconfig.php +13 -0
  97. data/Plugins/Apps/Nextcloud/config.php +10 -1
  98. data/Plugins/Apps/Nextcloud/nextcloudcron.service +8 -0
  99. data/Plugins/Apps/Nextcloud/nextcloudcron.timer +10 -0
  100. data/Plugins/Apps/Nginx/Connection.rb +93 -0
  101. data/Plugins/Apps/Nginx/conf.d/configlmm.conf +54 -4
  102. data/Plugins/Apps/Nginx/conf.d/languages.conf +21 -0
  103. data/Plugins/Apps/Nginx/config-lmm/errors.conf +33 -22
  104. data/Plugins/Apps/Nginx/config-lmm/gateway-errors.conf +20 -0
  105. data/Plugins/Apps/Nginx/config-lmm/proxy.conf +6 -2
  106. data/Plugins/Apps/Nginx/main.conf.erb +7 -3
  107. data/Plugins/Apps/Nginx/nginx.conf +2 -2
  108. data/Plugins/Apps/Nginx/nginx.lmm.rb +103 -81
  109. data/Plugins/Apps/Nginx/proxy.conf.erb +24 -6
  110. data/Plugins/Apps/Odoo/Odoo.conf.erb +0 -3
  111. data/Plugins/Apps/Odoo/Odoo.container +7 -1
  112. data/Plugins/Apps/Odoo/Odoo.lmm.rb +4 -5
  113. data/Plugins/Apps/Ollama/Ollama.container +26 -0
  114. data/Plugins/Apps/Ollama/Ollama.lmm.rb +73 -0
  115. data/Plugins/Apps/OpenTelemetry/Config/config.yaml +704 -0
  116. data/Plugins/Apps/OpenTelemetry/OpenTelemetry.lmm.rb +154 -0
  117. data/Plugins/Apps/OpenVidu/Ingress.container +23 -0
  118. data/Plugins/Apps/{GitLab/GitLab.conf.erb → OpenVidu/OpenVidu.conf.erb} +8 -3
  119. data/Plugins/Apps/OpenVidu/OpenVidu.container +21 -0
  120. data/Plugins/Apps/OpenVidu/OpenVidu.lmm.rb +94 -0
  121. data/Plugins/Apps/OpenVidu/OpenViduCall.conf.erb +32 -0
  122. data/Plugins/Apps/OpenVidu/OpenViduCall.container +20 -0
  123. data/Plugins/Apps/OpenVidu/ingress.yaml +10 -0
  124. data/Plugins/Apps/OpenVidu/livekit.yaml +13 -0
  125. data/Plugins/Apps/PHP-FPM/Connection.rb +91 -0
  126. data/Plugins/Apps/PHP-FPM/PHP-FPM.lmm.rb +31 -4
  127. data/Plugins/Apps/Peppermint/Peppermint.conf.erb +2 -9
  128. data/Plugins/Apps/Peppermint/Peppermint.container +7 -1
  129. data/Plugins/Apps/Peppermint/Peppermint.lmm.rb +29 -33
  130. data/Plugins/Apps/Perplexica/Perplexica.container +25 -0
  131. data/Plugins/Apps/Perplexica/Perplexica.lmm.rb +92 -0
  132. data/Plugins/Apps/Perplexica/config.toml +26 -0
  133. data/Plugins/Apps/Podman/Connection.rb +24 -0
  134. data/Plugins/Apps/Podman/Podman.lmm.rb +80 -0
  135. data/Plugins/Apps/Podman/storage.conf +6 -0
  136. data/Plugins/Apps/Postfix/Postfix.lmm.rb +249 -145
  137. data/Plugins/Apps/PostgreSQL/Connection.rb +97 -0
  138. data/Plugins/Apps/PostgreSQL/PostgreSQL.lmm.rb +204 -99
  139. data/Plugins/Apps/Pterodactyl/Pterodactyl.conf.erb +0 -3
  140. data/Plugins/Apps/Pterodactyl/Pterodactyl.lmm.rb +0 -2
  141. data/Plugins/Apps/Pterodactyl/Wings.conf.erb +0 -3
  142. data/Plugins/Apps/RVM/RVM.lmm.rb +57 -0
  143. data/Plugins/Apps/Roundcube/Roundcube.conf.erb +72 -0
  144. data/Plugins/Apps/Roundcube/Roundcube.lmm.rb +141 -0
  145. data/Plugins/Apps/SSH/SSH.lmm.rb +9 -15
  146. data/Plugins/Apps/SearXNG/SearXNG.container +22 -0
  147. data/Plugins/Apps/SearXNG/SearXNG.lmm.rb +79 -0
  148. data/Plugins/Apps/SearXNG/limiter.toml +40 -0
  149. data/Plugins/Apps/SearXNG/settings.yml +2 -0
  150. data/Plugins/Apps/SigNoz/Config/alerts.yml +11 -0
  151. data/Plugins/Apps/SigNoz/Config/otel-collector-config.yaml +110 -0
  152. data/Plugins/Apps/SigNoz/Config/otel-collector-opamp-config.yaml +1 -0
  153. data/Plugins/Apps/SigNoz/Config/prometheus.yml +18 -0
  154. data/Plugins/Apps/SigNoz/SigNoz-Collector.container +23 -0
  155. data/Plugins/Apps/SigNoz/SigNoz-Migrator.container +17 -0
  156. data/Plugins/Apps/SigNoz/SigNoz.conf.erb +61 -0
  157. data/Plugins/Apps/SigNoz/SigNoz.container +26 -0
  158. data/Plugins/Apps/SigNoz/SigNoz.lmm.rb +319 -0
  159. data/Plugins/Apps/Solr/log4j2.xml +89 -0
  160. data/Plugins/Apps/Solr/solr.lmm.rb +82 -0
  161. data/Plugins/Apps/Sunshine/Sunshine.conf.erb +0 -3
  162. data/Plugins/Apps/Sunshine/Sunshine.lmm.rb +0 -1
  163. data/Plugins/Apps/Tunnel/tunnel.lmm.rb +59 -0
  164. data/Plugins/Apps/Tunnel/tunnelTCP.service +9 -0
  165. data/Plugins/Apps/Tunnel/tunnelTCP.socket +9 -0
  166. data/Plugins/Apps/Tunnel/tunnelUDP.service +9 -0
  167. data/Plugins/Apps/Tunnel/tunnelUDP.socket +9 -0
  168. data/Plugins/Apps/UVdesk/UVdesk.conf.erb +0 -3
  169. data/Plugins/Apps/Umami/Umami.container +19 -0
  170. data/Plugins/Apps/Umami/Umami.lmm.rb +108 -0
  171. data/Plugins/Apps/Valkey/Valkey.lmm.rb +64 -20
  172. data/Plugins/Apps/Vaultwarden/Vaultwarden.conf.erb +9 -6
  173. data/Plugins/Apps/Vaultwarden/Vaultwarden.container +7 -1
  174. data/Plugins/Apps/Vaultwarden/Vaultwarden.lmm.rb +67 -28
  175. data/Plugins/Apps/Wiki.js/Wiki.js.conf.erb +39 -0
  176. data/Plugins/Apps/Wiki.js/Wiki.js.container +20 -0
  177. data/Plugins/Apps/Wiki.js/Wiki.js.lmm.rb +55 -0
  178. data/Plugins/Apps/YaCy/YaCy.conf.erb +93 -0
  179. data/Plugins/Apps/YaCy/YaCy.container +21 -0
  180. data/Plugins/Apps/YaCy/YaCy.lmm.rb +160 -0
  181. data/Plugins/Apps/ZooKeeper/ZooKeeper.container +24 -0
  182. data/Plugins/Apps/ZooKeeper/ZooKeeper.lmm.rb +68 -0
  183. data/Plugins/Apps/bitmagnet/bitmagnet.conf.erb +0 -3
  184. data/Plugins/Apps/bitmagnet/bitmagnet.lmm.rb +0 -1
  185. data/Plugins/Apps/gollum/gollum.conf.erb +40 -4
  186. data/Plugins/Apps/gollum/gollum.container +10 -1
  187. data/Plugins/Apps/gollum/gollum.lmm.rb +56 -47
  188. data/Plugins/Apps/llama.cpp/llama.cpp.container +28 -0
  189. data/Plugins/Apps/llama.cpp/llama.cpp.lmm.rb +90 -0
  190. data/Plugins/Apps/vLLM/vLLM.container +32 -0
  191. data/Plugins/Apps/vLLM/vLLM.lmm.rb +89 -0
  192. data/Plugins/OS/General/Utils.lmm.rb +26 -0
  193. data/Plugins/OS/Linux/Connection.rb +472 -0
  194. data/Plugins/OS/Linux/Debian/preseed.cfg.erb +81 -0
  195. data/Plugins/OS/Linux/Distributions.yaml +32 -0
  196. data/Plugins/OS/Linux/Flavours.yaml +24 -0
  197. data/Plugins/OS/Linux/Grub/grub.cfg +10 -0
  198. data/Plugins/OS/Linux/HTTP.rb +32 -0
  199. data/Plugins/OS/Linux/Linux.lmm.rb +708 -174
  200. data/Plugins/OS/Linux/Packages.yaml +67 -3
  201. data/Plugins/OS/Linux/Proxmox/answer.toml.erb +30 -0
  202. data/Plugins/OS/Linux/Services.yaml +8 -0
  203. data/Plugins/OS/Linux/Shell.rb +70 -0
  204. data/Plugins/OS/Linux/Syslinux/default +8 -0
  205. data/Plugins/OS/Linux/WireGuard/WireGuard.lmm.rb +93 -40
  206. data/Plugins/OS/Linux/WireGuard/wg0.conf.erb +3 -0
  207. data/Plugins/OS/Linux/openSUSE/autoinst.xml.erb +29 -3
  208. data/Plugins/OS/Linux/systemd/systemd.lmm.rb +13 -11
  209. data/Plugins/OS/Routers/Aruba/ArubaInstant.lmm.rb +6 -5
  210. data/Plugins/Platforms/GitHub.lmm.rb +73 -28
  211. data/Plugins/Platforms/GoDaddy/GoDaddy.lmm.rb +10 -7
  212. data/Plugins/Platforms/Proxmox/Proxmox.lmm.rb +402 -0
  213. data/Plugins/Platforms/Proxmox/XTerm.rb +321 -0
  214. data/Plugins/Platforms/libvirt/libvirt.lmm.rb +41 -15
  215. data/Plugins/Platforms/porkbun.lmm.rb +12 -2
  216. data/Plugins/Platforms/porkbun_spec.rb +2 -2
  217. data/Plugins/Services/DNS/AmberBit.lmm.rb +1 -1
  218. data/Plugins/Services/DNS/ArubaItDNS.lmm.rb +1 -1
  219. data/Plugins/Services/DNS/NICLV.lmm.rb +1 -1
  220. data/Plugins/Services/DNS/PowerDNS.lmm.rb +130 -41
  221. data/Plugins/Services/DNS/tonic.lmm.rb +22 -12
  222. data/bootstrap.sh +41 -3
  223. data/lib/ConfigLMM/Framework/plugins/dns.rb +4 -3
  224. data/lib/ConfigLMM/Framework/plugins/linuxApp.rb +187 -144
  225. data/lib/ConfigLMM/Framework/plugins/nginxApp.rb +54 -6
  226. data/lib/ConfigLMM/Framework/plugins/plugin.rb +68 -140
  227. data/lib/ConfigLMM/Framework/plugins/store.rb +4 -4
  228. data/lib/ConfigLMM/Framework/variables.rb +75 -0
  229. data/lib/ConfigLMM/Framework.rb +1 -0
  230. data/lib/ConfigLMM/cli.rb +13 -5
  231. data/lib/ConfigLMM/commands/cleanup.rb +1 -0
  232. data/lib/ConfigLMM/commands/configsCommand.rb +38 -5
  233. data/lib/ConfigLMM/commands/diff.rb +33 -9
  234. data/lib/ConfigLMM/context.rb +22 -3
  235. data/lib/ConfigLMM/io/configList.rb +85 -7
  236. data/lib/ConfigLMM/io/connection.rb +143 -0
  237. data/lib/ConfigLMM/io/dhcp.rb +330 -0
  238. data/lib/ConfigLMM/io/http.rb +78 -0
  239. data/lib/ConfigLMM/io/local.rb +207 -0
  240. data/lib/ConfigLMM/io/pxe.rb +92 -0
  241. data/lib/ConfigLMM/io/ssh.rb +156 -0
  242. data/lib/ConfigLMM/io/tftp.rb +105 -0
  243. data/lib/ConfigLMM/io.rb +2 -0
  244. data/lib/ConfigLMM/secrets/envStore.rb +39 -0
  245. data/lib/ConfigLMM/secrets/fileStore.rb +43 -0
  246. data/lib/ConfigLMM/state.rb +12 -3
  247. data/lib/ConfigLMM/version.rb +2 -1
  248. data/lib/ConfigLMM.rb +1 -0
  249. data/{Examples → scripts}/configlmmAuth.sh +7 -5
  250. metadata +257 -9
@@ -1,154 +1,396 @@
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
- buildAutoYaST(id, target, options)
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) + target['Domain'] + ' ' + target['Name'] + "\n"
78
+ hostsLines << getHostsLine(ipAddr, [Addressable::IDNA.to_ascii(target['Domain']), target['Name']]) + "\n"
79
+ end
80
+ end
81
+ target['Hosts'].to_a.each do |ip, entries|
82
+ hostsLines << getHostsLine(ip, entries) + "\n"
83
+ end
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)
102
+ end
103
+ fileLines
104
+ end
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)
117
+ end
118
+ end
119
+ params = '--create-home --user-group'
120
+ badname = '--badname'
121
+ badname = '--badnames' if distroInfo['Name'] == 'openSUSE Leap'
122
+ if info['System']
123
+ params += ' --system'
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
57
146
  end
58
- target['Hosts'].to_a.each do |ip, entries|
59
- hostsLines << ip.ljust(16) + entries.join(' ') + "\n"
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)
60
154
  end
61
- updateRemoteFile(ssh, HOSTS_FILE, options, false) do |fileLines|
62
- fileLines + hostsLines
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)
63
159
  end
64
160
  end
65
- distroInfo = self.class.currentDistroInfo(ssh)
66
- if target['Network']
67
- if distroInfo['Name'] == 'openSUSE Leap'
68
- networkFile = '/etc/sysconfig/network/ifcfg-eth0'
69
- if target['Network'] == 'dhcp'
70
- self.class.sshExec!(ssh, "sed -i \"s|^BOOTPROTO=.*|BOOTPROTO='dhcp'|\" #{networkFile}")
161
+ end
162
+ if target['Firewall'] && target['Firewall'] != 'no'
163
+ connection.ensurePackage(FIREWALL_PACKAGE, options)
164
+ connection.ensureServiceAutoStart(FIREWALL_SERVICE, options)
165
+ connection.startService(FIREWALL_SERVICE, options)
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)
175
+ end
176
+
177
+ def convertFlavour(distroInfo, target, connection, options)
178
+ if target['Flavour']
179
+ if target['Flavour'] == PROXMOXVE_NAME
180
+ if distroInfo['Name'] != DEBIAN_NAME
181
+ raise 'Can\'t convert flavour!'
182
+ end
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?
185
+ if needInstall
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)
189
+ end
190
+ else
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)
192
+ File.write(options['output'] + 'proxmox-release-bookworm.gpg', HTTP.follow.get('https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg').body)
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)
197
+ end
198
+ target['Network'] = {} unless target['Network'].is_a?(Hash)
199
+ target['Network']['Interfaces'] = {} unless target['Network']['Interfaces'].is_a?(Hash)
200
+ if !target['Network']['Interfaces'].key?('vmbr0')
201
+ if target['Network']['IP']
202
+ target['Network']['Interfaces']['vmbr0'] = {}
203
+ target['Network']['Interfaces']['vmbr0']['Type'] = 'Bridge'
204
+ target['Network']['Interfaces']['vmbr0']['IP'] = target['Network']['IP']
205
+ target['Network']['Interfaces']['vmbr0']['Gateway'] = target['Network']['Gateway']
206
+ target['Network']['Interfaces']['vmbr0']['DNS'] = target['Network']['DNS']
71
207
  else
72
- self.class.sshExec!(ssh, "sed -i \"s|^BOOTPROTO=.*|BOOTPROTO='static'|\" #{networkFile}")
73
- updateRemoteFile(ssh, networkFile, options, false) do |fileLines|
74
- fileLines << "\n"
75
- if target['Network']['IP']
76
- if target['Network']['IP'].is_a?(Array)
77
- # TODO
208
+ target['Network']['Interfaces']['vmbr0'] = 'dhcp'
209
+ end
210
+ end
211
+ else
212
+ raise 'Unimplemented flavour!'
213
+ end
214
+ end
215
+ end
216
+
217
+ def configureNetwork(distroInfo, target, connection, options)
218
+ if target['Network']
219
+ if distroInfo['Name'] == 'openSUSE Leap'
220
+ updateNetworkInterface(target['Network'], 'eth0', connection, options)
221
+ if target['Network']['Interfaces']
222
+ target['Network']['Interfaces'].each do |interface, config|
223
+ updateNetworkInterface(config, interface, connection, options)
224
+ end
225
+ end
226
+ if target['Network']['DNS']
227
+ configFile = '/etc/sysconfig/network/config'
228
+ dns = target['Network']['DNS']
229
+ dns = [dns] unless dns.is_a?(Array)
230
+ connection.exec("sed -i 's|^NETCONFIG_DNS_STATIC_SERVERS=.*|NETCONFIG_DNS_STATIC_SERVERS=\"#{dns.join(' ')}\"|' #{configFile}", false, options)
231
+ end
232
+ if target['Network']['Gateway']
233
+ routesFile = '/etc/sysconfig/network/routes'
234
+ connection.exec("sed -i 's|^default |#default |' #{routesFile}", false, options)
235
+ connection.updateFile(routesFile, options) do |fileLines|
236
+ fileLines << "default #{target['Network']['Gateway']}\n"
237
+ end
238
+ end
239
+ elsif distroInfo['Name'] == 'Debian'
240
+ links = self.networkLinks(connection)
241
+ raise 'Didn\'t find network links!' if links.empty?
242
+ linkType = nil
243
+ dnsSearch = connection.exec('cat /etc/resolv.conf | grep search', false, { **options, 'dry' => false }).strip.split(' ').last
244
+ if target['Network'].is_a?(String)
245
+ linkType = target['Network']
246
+ target['Network'] = {}
247
+ end
248
+ links.each do |link|
249
+ target['Network']['Interfaces'][link] = 'manual' unless target['Network']['Interfaces'].key?(link)
250
+ end
251
+ if !target['Network'].key?('Interfaces') ||
252
+ target['Network']['Interfaces'].to_h.empty? ||
253
+ !target['Network']['Interfaces'].key?(links.first)
254
+ target['Network']['Interfaces'] ||= {}
255
+ if !linkType.nil?
256
+ target['Network']['Interfaces'][links.first] = linkType
257
+ elsif target['Network']['IP']
258
+ target['Network']['Interfaces'][links.first] = {}
259
+ target['Network']['Interfaces'][links.first]['IP'] = target['Network']['IP']
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']
262
+ end
263
+ end
264
+ if target['Network']['Interfaces'].key?('vmbr0')
265
+ if !target['Network']['Interfaces']['vmbr0'].is_a?(Hash)
266
+ target['Network']['Interfaces']['vmbr0'] = { }
267
+ end
268
+ if target['Network']['Interfaces']['vmbr0']['Ports'].nil?
269
+ target['Network']['Interfaces']['vmbr0']['Ports'] = [links.first]
270
+ target['Network']['Interfaces'][links.first] = 'manual'
271
+ end
272
+ end
273
+ interfacesFile = '/etc/network/interfaces'
274
+ localFile = options['output'] + '/' + SecureRandom.alphanumeric(10)
275
+ connection.download(interfacesFile, localFile)
276
+ fileLines = File.read(localFile).lines
277
+ if fileLines.index(IO::Local::CONFIGLMM_SECTION_BEGIN).nil?
278
+ lines = []
279
+ iface = false
280
+ fileLines.each do |line|
281
+ if line.start_with?('iface')
282
+ if line.strip.split(' ')[1].start_with?('enp')
283
+ iface = true
284
+ else
285
+ lines << line
286
+ end
287
+ elsif iface && (line.start_with?(' ') || line.start_with?("\t"))
288
+ # Drop line
289
+ else
290
+ iface = false
291
+ lines << line
292
+ end
293
+ end
294
+ fileWrite(localFile, lines.join(), options[:dry])
295
+ connection.upload(localFile, interfacesFile)
296
+ end
297
+ connection.updateFile(interfacesFile, options) do |fileLines|
298
+ target['Network']['Interfaces'].each do |name, data|
299
+ fileLines << "auto #{name}\n"
300
+ data = 'manual' if data.nil?
301
+ if data.is_a?(String)
302
+ fileLines << "iface #{name} inet #{data}\n"
303
+ else
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"
78
314
  else
79
- self.class.sshExec!(ssh, "sed -i 's|^IPADDR=|#IPADDR=|' #{networkFile}")
80
- fileLines << "IPADDR=#{target['Network']['IP']}\n"
315
+ fileLines << " bridge-ports none\n"
316
+ end
317
+ fileLines << " bridge-stp off\n"
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"
81
324
  end
82
325
  end
83
- fileLines
84
- end
85
- if target['Network']['DNS']
86
- configFile = '/etc/sysconfig/network/config'
87
- dns = target['Network']['DNS']
88
- dns = [dns] unless dns.is_a?(Array)
89
- self.class.sshExec!(ssh, "sed -i 's|^NETCONFIG_DNS_STATIC_SERVERS=.*|NETCONFIG_DNS_STATIC_SERVERS=\"#{dns.join(' ')}\"|' #{configFile}")
90
- end
91
- if target['Network']['Gateway']
92
- routesFile = '/etc/sysconfig/network/routes'
93
- self.class.sshExec!(ssh, "sed -i 's|^default |#default |' #{routesFile}")
94
- updateRemoteFile(ssh, routesFile, options) do |fileLines|
95
- fileLines << "default #{target['Network']['Gateway']}\n"
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
330
+ end
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"
96
340
  end
97
341
  end
98
- end
99
- else
100
- # TODO
101
- raise 'Not Unimplemented!'
102
- end
103
- end
104
- if target['Tmpfs']
105
- self.class.sshExec!(ssh, "sed -i '/ \\/tmp /d' #{FSTAB_FILE}")
106
- updateRemoteFile(ssh, FSTAB_FILE, options, false) do |fileLines|
107
- fileLines << "tmpfs /tmp tmpfs nodev,nosuid,size=#{target['Tmpfs']} 0 0\n"
108
- end
109
- end
110
- if target['Sysctl']
111
- updateRemoteFile(ssh, SYSCTL_FILE, options, false) do |fileLines|
112
- target['Sysctl'].each do |name, value|
113
- fileLines << "#{name} = #{value}\n"
114
- self.class.sshExec!(ssh, "sysctl #{name}=#{value}")
342
+ fileLines << "\n"
115
343
  end
116
344
  fileLines
117
345
  end
346
+ else
347
+ # TODO
348
+ raise 'Not Unimplemented!'
118
349
  end
119
- if target['Users']
120
- target['Users'].each do |name, info|
121
- userId = ssh.exec!("id -u #{name} 2>/dev/null").strip
122
- if userId.empty?
123
- shell = ''
124
- if info['Shell']
125
- shell = "--shell '/usr/bin/#{info['Shell']}'"
350
+ end
351
+ end
352
+
353
+ def updateNetworkInterface(config, interface, connection, options)
354
+ baseFile = '/etc/sysconfig/network/ifcfg-'
355
+ networkFile = baseFile + interface
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|
364
+ fileLines << "STARTMODE=auto\n"
365
+ fileLines << "ZONE=public\n"
366
+ if config == 'dhcp'
367
+ fileLines << "BOOTPROTO=dhcp\n"
368
+ else
369
+ fileLines << "BOOTPROTO=static\n"
370
+ fileLines << "\n"
371
+ if config['IP']
372
+ if config['IP'].is_a?(Array)
373
+ config['IP'].each_with_index do |ip, i|
374
+ c = "_#{i}"
375
+ c = '' if i.zero?
376
+ fileLines << "IPADDR#{c}=#{ip}\n"
126
377
  end
127
- badname = '--badname'
128
- badname = '--badnames' if distroInfo['Name'] == 'openSUSE Leap'
129
- self.class.sshExec!(ssh, "useradd #{badname} --create-home --user-group #{shell} #{name}")
130
- end
131
- homeDir = self.class.sshExec!(ssh, "getent passwd #{name} | cut -d ':' -f 6").strip
132
- keyFile = homeDir + "/.ssh/id_ed25519"
133
- if info['SSHKey'] && !self.class.remoteFilePresent?(keyFile, ssh)
134
- self.class.sshExec!(ssh, "mkdir -p #{homeDir}/.ssh")
135
- self.class.sshExec!(ssh, "ssh-keygen -t ed25519 -f #{keyFile} -P ''")
136
- self.class.sshExec!(ssh, "chown -R #{name}:#{name} #{homeDir}/.ssh")
378
+ else
379
+ fileLines << "IPADDR=#{config['IP']}\n"
137
380
  end
138
381
  end
139
382
  end
140
- self.executeCommands(target['Execute'], ssh)
141
- end
142
- if target['Firewall'] && target['Firewall'] != 'no'
143
- self.ensurePackage(FIREWALL_PACKAGE, locationUri)
144
- self.ensureServiceAutoStart(FIREWALL_SERVICE, locationUri)
145
- self.startService(FIREWALL_SERVICE, locationUri)
383
+ fileLines
146
384
  end
147
385
  end
148
386
 
149
- def deployLocal(target, options)
387
+ def networkLinks(connection)
388
+ connection.exec("ls /sys/class/net/").strip.split("\n").select { |name| name.start_with?('enp') }
389
+ end
390
+
391
+ def deployLocal(connection, target, options)
150
392
  deployLocalHostsFile(target, options)
151
- deployLocalSSHConfig(target, options)
393
+ deployLocalSSHConfig(connection, target, options)
152
394
  if target['Sysctl']
153
395
  updateLocalFile(SYSCTL_FILE, options) do |fileLines|
154
396
  target['Sysctl'].each do |name, value|
@@ -160,35 +402,34 @@ module ConfigLMM
160
402
  end
161
403
  if target['Users']
162
404
  target['Users'].each do |name, info|
163
- 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
164
406
  if userId.empty?
165
407
  shell = ''
166
408
  if info['Shell']
167
409
  shell = "--shell '/usr/bin/#{info['Shell']}'"
168
410
  end
169
- distroInfo = self.class.currentDistroInfo(nil)
170
411
  badname = '--badname'
171
- badname = '--badnames' if distroInfo['Name'] == 'openSUSE Leap'
172
- 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)
173
414
  end
174
- 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
175
416
  keyFile = homeDir + "/.ssh/id_ed25519"
176
- if info['SSHKey'] && !self.class.filePresent?(keyFile)
177
- self.class.exec("mkdir -p #{homeDir}/.ssh")
178
- self.class.exec("ssh-keygen -t ed25519 -f #{keyFile} -P ''")
179
- 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)
180
421
  end
181
422
  end
182
423
  end
183
424
  if target['Firewall'] && target['Firewall'] != 'no'
184
- self.ensurePackage(FIREWALL_PACKAGE, locationUri)
185
- self.ensureServiceAutoStart(FIREWALL_SERVICE, locationUri)
186
- self.startService(FIREWALL_SERVICE, locationUri)
425
+ connection.ensurePackage(FIREWALL_PACKAGE, options)
426
+ connection.ensureServiceAutoStart(FIREWALL_SERVICE, options)
427
+ connection.startService(FIREWALL_SERVICE, options)
187
428
  end
188
429
  self.executeCommands(target['Execute'])
189
430
  end
190
431
 
191
- def executeCommands(commands, ssh = nil)
432
+ def executeCommands(commands, connection)
192
433
  return unless commands
193
434
 
194
435
  commands.each do |type, data|
@@ -196,7 +437,7 @@ module ConfigLMM
196
437
  when 'sh'
197
438
  data = [data] unless data.is_a?(Array)
198
439
  data.each do |cmd|
199
- self.class.exec(cmd, ssh)
440
+ connection.exec(cmd)
200
441
  end
201
442
  else
202
443
  raise 'Unimplemented!'
@@ -206,9 +447,71 @@ module ConfigLMM
206
447
 
207
448
  def deployOverLibvirt(id, target, activeState, context, options)
208
449
  location = Libvirt.getLocation(target['Location'])
209
- iso = installationISO(target['Distro'], location)
210
- iso = buildISOAutoYaST(id, iso, target, options) if target['Distro'] == SUSE_NAME
211
- plugins[:Libvirt].createVM(target['Name'], target, target['Location'], iso, activeState)
450
+ iso = installationISO(target['Distro'], target['Flavour'], location)
451
+ iso = buildAutoInstallISO(id, iso, target, options)
452
+ if plugins[:Libvirt].createVM(target['Name'], target, target['Location'], iso, activeState)
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)
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)
212
515
  end
213
516
 
214
517
  def buildHostsFile(id, target, options)
@@ -219,11 +522,11 @@ module ConfigLMM
219
522
  hosts += "#<ip-address> <hostname.domain.org> <hostname>\n"
220
523
  hosts += "127.0.0.1 localhost\n"
221
524
  hosts += "::1 localhost\n\n"
222
- hosts += CONFIGLMM_SECTION_BEGIN
525
+ hosts += IO::Local::CONFIGLMM_SECTION_BEGIN
223
526
  target['Hosts'].each do |ip, entries|
224
- hosts += ip.ljust(16) + entries.join(' ') + "\n"
527
+ hosts += getHostsLine(ip, entries) + "\n"
225
528
  end
226
- hosts += CONFIGLMM_SECTION_END
529
+ hosts += IO::Local::CONFIGLMM_SECTION_END
227
530
 
228
531
  path = options['output'] + '/' + id
229
532
  mkdir(path + '/etc', options[:dry])
@@ -234,16 +537,16 @@ module ConfigLMM
234
537
  def buildSSHConfig(id, target, options)
235
538
  if !target['SSH']['Config'].empty?
236
539
  sshConfig = "\n"
237
- sshConfig += CONFIGLMM_SECTION_BEGIN
540
+ sshConfig += IO::Local::CONFIGLMM_SECTION_BEGIN
238
541
  target['SSH']['Config'].each do |name, info|
239
542
  sshConfig += "Host #{name} #{info['HostName']}\n"
240
- sshConfig += " HostName " + info['HostName'] + "\n" if info['HostName']
241
- 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']
242
545
  sshConfig += " User " + info['User'] + "\n" if info['User']
243
546
  sshConfig += " IdentityFile " + info['IdentityFile'] + "\n" if info['IdentityFile']
244
547
  sshConfig += "\n"
245
548
  end
246
- sshConfig += CONFIGLMM_SECTION_END
549
+ sshConfig += IO::Local::CONFIGLMM_SECTION_END
247
550
  sshConfig += "\n"
248
551
 
249
552
  configPath = options['output'] + '/' + id
@@ -252,66 +555,192 @@ module ConfigLMM
252
555
  end
253
556
  end
254
557
 
255
- def buildAutoYaST(id, target, options)
256
- if target['Distro'] == SUSE_NAME
558
+ def buildAutoInstall(id, target, options)
559
+ config = prepareAutoInstallConfig(target)
560
+ if config['Flavour'] == PROXMOXVE_NAME
561
+ outputFolder = options['output'] + '/' + id + '/'
562
+ template = ERB.new(File.read(__dir__ + '/Proxmox/answer.toml.erb'))
563
+ renderTemplate(template, config, outputFolder + 'answer.toml', options)
564
+ File.write("#{outputFolder}/auto-installer-mode.toml", 'mode = "iso"')
565
+ elsif config['Distro'] == SUSE_NAME
257
566
  outputFolder = options['output'] + '/' + id + '/'
258
567
  template = ERB.new(File.read(__dir__ + '/openSUSE/autoinst.xml.erb'))
259
- renderTemplate(template, target, outputFolder + 'autoinst.xml', options)
568
+ renderTemplate(template, config, outputFolder + 'autoinst.xml', options)
569
+ elsif config['Distro'] == DEBIAN_NAME
570
+ variables = prepareDebianStorage(config, options)
571
+ outputFolder = options['output'] + '/' + id + '/'
572
+ template = ERB.new(File.read(__dir__ + '/Debian/preseed.cfg.erb'))
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
604
+ end
605
+ if !variables['AutoPartition']
606
+ logger.warn('Specified disk/partition configuration is not implemented! You will have to configure it manually!')
260
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(' ')
261
617
  end
262
618
 
263
619
  def deployLocalHostsFile(target, options)
264
620
  if target['Hosts']
265
621
  updateLocalFile(HOSTS_FILE, options) do |hostsLines|
266
622
  target['Hosts'].each do |ip, entries|
267
- hostsLines << ip.ljust(16) + entries.join(' ') + "\n"
623
+ hostsLines << getHostsLine(ip, entries) + "\n"
268
624
  end
269
625
  hostsLines
270
626
  end
271
627
  end
272
628
  end
273
629
 
274
- def deployLocalSSHConfig(target, options)
275
- if !target['SSH']['Config'].empty?
276
- updateLocalFile(File.expand_path(SSH_CONFIG), options) do |configLines|
277
- target['SSH']['Config'].each do |name, info|
278
- configLines << "Host #{name} #{info['HostName']}\n"
279
- configLines << " HostName " + info['HostName'] + "\n" if info['HostName']
280
- configLines << " Port " + info['Port'] + "\n" if info['Port']
281
- configLines << " User " + info['User'] + "\n" if info['User']
282
- configLines << " IdentityFile " + info['IdentityFile'] + "\n" if info['IdentityFile']
283
- end
284
- 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)
285
638
  end
286
639
  end
287
640
  end
288
641
 
289
- def installationISO(distro, 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)
290
654
  url = nil
291
- case distro
292
- when SUSE_NAME
293
- if location.empty?
294
- # TODO automatically fetch latest version from website
295
- url = 'https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso'
296
- else
297
- raise Framework::PluginProcessError.new("#{id}: Unimplemented!")
655
+ flavour = distro unless flavour
656
+ flavourInfo = YAML.load_file(__dir__ + '/Flavours.yaml')[flavour]
657
+ if flavourInfo.nil?
658
+ raise Framework::PluginProcessError.new("#{id}: Unknown Linux Distro: #{flavour}!")
659
+ 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)
298
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)
299
731
  else
300
- raise Framework::PluginProcessError.new("#{id}: Unknown Linux Distro: #{distro}!")
732
+ raise 'Not implemented!'
301
733
  end
734
+ outputFolder
735
+ end
302
736
 
303
- filename = File.basename(Addressable::URI.parse(url).path)
304
- iso = File.expand_path(ISO_LOCATION + filename)
305
- if !File.exist?(iso)
306
- mkdir(File.expand_path(ISO_LOCATION), false)
307
- prompt.say('Downloading... ' + url)
308
- response = HTTP.follow.get(url)
309
- raise "Failed to download file: #{response.status}" unless response.status.success?
310
- File.open(iso, 'wb') do |file|
311
- response.body.each do |chunk|
312
- file.write(chunk)
313
- end
314
- end
737
+ def buildAutoInstallISO(id, iso, target, options)
738
+ if target['Flavour'] == PROXMOXVE_NAME
739
+ iso = buildISOAutoProxmox(id, iso, target, options)
740
+ elsif target['Distro'] == SUSE_NAME
741
+ iso = buildISOAutoYaST(id, iso, target, options)
742
+ elsif target['Distro'] == DEBIAN_NAME
743
+ iso = buildISOPreseed(id, iso, target, options)
315
744
  end
316
745
  iso
317
746
  end
@@ -319,40 +748,92 @@ module ConfigLMM
319
748
  def buildISOAutoYaST(id, iso, target, options)
320
749
  outputFolder = options['output'] + '/iso/'
321
750
  mkdir(outputFolder, false)
322
- `xorriso -osirrox on -indev #{iso} -extract / #{outputFolder} 2>&1 >/dev/null`
751
+ local.exec("xorriso -osirrox on -indev #{iso} -extract / #{outputFolder}")
323
752
  FileUtils.chmod_R(0750, outputFolder) # Need to make it writeable so it can be deleted
324
753
  copy(options['output'] + '/' + id + '/autoinst.xml', outputFolder, false)
325
754
 
326
755
  cfg = outputFolder + "boot/x86_64/loader/isolinux.cfg"
327
- `sed -i 's|default harddisk|default linux|' #{cfg}`
328
- `sed -i 's|append initrd=initrd splash=silent showopts|append initrd=initrd splash=silent autoyast=device://sr0/autoinst.xml|' #{cfg}`
329
- `sed -i 's|prompt 1|prompt 0|' #{cfg}`
330
- `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}")
331
777
 
332
778
  patchedIso = File.dirname(iso) + '/patched.iso'
333
- `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`
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}")
334
780
  patchedIso
335
781
  end
336
782
 
337
- def prepareConfig(target)
783
+ def buildISOPreseed(id, iso, target, options)
784
+ outputFolder = options['output'] + '/iso/'
785
+ mkdir(outputFolder, false)
786
+ local.exec("xorriso -osirrox on -indev #{iso} -extract / #{outputFolder}", false)
787
+ FileUtils.chmod_R(0750, outputFolder) # Need to make it writeable so it can be deleted
788
+ local.copy(options['output'] + '/' + id + '/preseed.cfg', outputFolder, false)
789
+
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"}")
795
+
796
+ patchedIso = File.dirname(iso) + '/patched.iso'
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}")
798
+ patchedIso
799
+ end
800
+
801
+ def buildISOAutoProxmox(id, iso, target, options)
802
+ outputFolder = options['output'] + '/iso/'
803
+ patchedIso = File.dirname(iso) + '/patched.iso'
804
+
805
+ copy(iso, patchedIso, false)
806
+
807
+ self.class.exec("xorriso -boot_image any keep -dev #{patchedIso} -map #{options['output'] + '/' + id + '/auto-installer-mode.toml'} /auto-installer-mode.toml")
808
+ self.class.exec("xorriso -boot_image any keep -dev #{patchedIso} -map #{options['output'] + '/' + id + '/answer.toml'} /answer.toml")
809
+ patchedIso
810
+ end
811
+
812
+ def self.withConnection(connection)
813
+ yield(LinuxConnection.new(connection))
814
+ end
815
+
816
+ def prepareConfig(target, context)
338
817
  target['SSH'] ||= {}
339
818
  target['SSH']['Config'] ||= {}
340
819
  target['Users'] ||= {}
341
820
  target['HostName'] = target['Name'] unless target['HostName']
342
821
 
343
- if ENV['LINUX_ROOT_PASSWORD_HASH']
822
+ if target['SecretId'] && context.secrets.load(target['SecretId'], 'ROOT_PASSWORD_HASH')
344
823
  target['Users']['root'] ||= {}
345
- target['Users']['root']['PasswordHash'] = ENV['LINUX_ROOT_PASSWORD_HASH']
346
- 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')
347
826
  target['Users']['root'] ||= {}
348
- 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'])
349
829
  elsif target['Users'].key?('root')
350
- if !target['Users']['root']['Password'] &&
351
- !target['Users']['root']['PasswordHash']
352
- rootPassword = SecureRandom.urlsafe_base64(12)
353
- prompt.say("Root password: #{rootPassword}", :color => :magenta)
354
- target['Users']['root']['PasswordHash'] = self.class.linuxPasswordHash(rootPassword)
355
- 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
356
837
  target['Users']['root'].delete('Password')
357
838
  end
358
839
  end
@@ -360,7 +841,7 @@ module ConfigLMM
360
841
  target['Users'].each do |user, info|
361
842
  newKeys = []
362
843
  info['AuthorizedKeys'].to_a.each do |key|
363
- if key.start_with?('/') || key.start_with?('~')
844
+ if key.start_with?('/') || key.start_with?('~') || key.start_with?('.') || key.end_with?('.pub')
364
845
  newKeys << File.read(File.expand_path(key)).strip
365
846
  else
366
847
  newKeys << key
@@ -369,14 +850,67 @@ module ConfigLMM
369
850
  info['AuthorizedKeys'] = newKeys
370
851
  end
371
852
 
372
- packages = YAML.load_file(__dir__ + '/Packages.yaml')
373
853
  newApps = []
374
854
  target['Services'] ||= []
375
- if target['Apps'].to_a.include?('sshd')
376
- target['Services'] << 'sshd'
377
- 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
378
913
  end
379
- target['Apps'] = self.class.mapPackages(target['Apps'], target['Distro'])
380
914
  end
381
915
 
382
916
  def self.linuxPasswordHash(password)