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
@@ -0,0 +1,78 @@
1
+
2
+ require 'forwardable'
3
+ require 'webrick'
4
+
5
+ module ConfigLMM
6
+ module IO
7
+
8
+ class HTTPLogger
9
+ extend Forwardable
10
+
11
+ def_delegators :@Logger, :debug, :info, :warn, :error, :fatal
12
+
13
+ def initialize(logger, level)
14
+ @Logger = logger
15
+ @Level = level
16
+ end
17
+
18
+ def debug?
19
+ @Level == 'debug'
20
+ end
21
+
22
+ def <<(message)
23
+ @Logger.info(message)
24
+ end
25
+ end
26
+
27
+ class HTTP
28
+ PORT = 6582
29
+
30
+ def initialize(dir, ip, options, logger)
31
+ @IP = ip
32
+ @Logger = HTTPLogger.new(logger, options[:level])
33
+ @LastReadTime = nil
34
+ requestCallback = Proc.new do |request, response|
35
+ @LastReadTime = Time.now
36
+ response
37
+ end
38
+ @Server = WEBrick::HTTPServer.new(BindAddress: @IP,
39
+ Port: PORT,
40
+ DocumentRoot: dir,
41
+ RequestCallback: requestCallback,
42
+ Logger: @Logger,
43
+ AccessLog: { @Logger => WEBrick::AccessLog::COMMON_LOG_FORMAT })
44
+ end
45
+
46
+ def url(path)
47
+ "http://#{@IP}:#{PORT}/#{path}"
48
+ end
49
+
50
+ def start()
51
+ Thread.new do
52
+ @Server.start
53
+ end
54
+ end
55
+
56
+ def stop()
57
+ @Server.stop
58
+ end
59
+
60
+ def wait(timeout)
61
+ timeoutTime = Time.now + timeout
62
+ previousReadTime = @LastReadTime
63
+ loop do
64
+ if previousReadTime != @LastReadTime
65
+ previousReadTime = @LastReadTime
66
+ timeoutTime = Time.now + timeout
67
+ end
68
+ return if Time.now >= timeoutTime
69
+ sleep(0.1)
70
+ end
71
+ end
72
+
73
+ def hadRead?
74
+ !@LastReadTime.nil?
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'open3'
5
+
6
+ module ConfigLMM
7
+ module IO
8
+ class Local
9
+
10
+ attr_reader :prompt
11
+ attr_reader :logger
12
+ attr_reader :local
13
+
14
+ def initialize(prompt, logger)
15
+ @prompt = prompt
16
+ @logger = logger
17
+ @local = self
18
+ end
19
+
20
+ def fileWrite(target, data, dry)
21
+ if dry
22
+ prompt.say('Would write file ' + target)
23
+ else
24
+ File.write(target, data)
25
+ end
26
+ end
27
+
28
+ def copy(source, target, dry)
29
+ if dry
30
+ prompt.say('Would copy ' + source + ' to ' + target)
31
+ else
32
+ FileUtils.cp_r(source, target, noop: dry)
33
+ end
34
+ end
35
+
36
+ def copyNotPresent(source, target, dry)
37
+ if !File.exist?(target + File.basename(source))
38
+ if dry
39
+ prompt.say('Would copy ' + source + ' to ' + target)
40
+ else
41
+ FileUtils.cp_r(source, target, noop: dry)
42
+ end
43
+ end
44
+ end
45
+
46
+ def rm(path, dry)
47
+ if dry
48
+ prompt.say('Would remove ' + path)
49
+ else
50
+ FileUtils.rm_r(path, noop: dry)
51
+ end
52
+ end
53
+
54
+ def mkdir(target, dry)
55
+ if dry
56
+ prompt.say('Would create ' + target)
57
+ else
58
+ FileUtils.mkdir_p(target)
59
+ end
60
+ end
61
+
62
+ def chown(user, group, target, dry)
63
+ if dry
64
+ prompt.say("Would chown #{target} as #{user}:#{group}")
65
+ else
66
+ FileUtils.chown_R(user, group, target)
67
+ end
68
+ end
69
+
70
+ CONFIGLMM_SECTION_BEGIN = "# -----BEGIN CONFIGLMM-----\n"
71
+ CONFIGLMM_SECTION_END = "# -----END CONFIGLMM-----\n"
72
+
73
+ def updateFile(file, options, atTop = false, comment = '#')
74
+ File.write(file, '') unless File.exist?(file)
75
+ sectionBegin = CONFIGLMM_SECTION_BEGIN.gsub('#', comment)
76
+ sectionEnd = CONFIGLMM_SECTION_END.gsub('#', comment)
77
+ fileLines = File.read(file).lines
78
+ sectionBeginIndex = fileLines.index(sectionBegin)
79
+ sectionEndIndex = fileLines.index(sectionEnd)
80
+ if sectionBeginIndex.nil?
81
+ linesBefore = []
82
+ linesBefore = fileLines unless atTop
83
+ linesBefore << "\n"
84
+ linesBefore << sectionBegin
85
+ linesAfter = [sectionEnd]
86
+ linesAfter << "\n"
87
+ linesAfter += fileLines if atTop
88
+ else
89
+ linesBefore = fileLines[0..sectionBeginIndex]
90
+ if sectionEndIndex.nil?
91
+ linesAfter = [sectionEnd]
92
+ linesAfter << "\n"
93
+ else
94
+ linesAfter = fileLines[sectionEndIndex..fileLines.length]
95
+ end
96
+ end
97
+
98
+ fileLines = linesBefore
99
+ fileLines = yield(fileLines)
100
+ fileLines += linesAfter
101
+
102
+ fileWrite(file, fileLines.join(), options[:dry])
103
+ end
104
+
105
+ def exec(command, allowFailure = false, options = {})
106
+ self.class.exec(command, allowFailure, options, self.prompt, self.logger)
107
+ end
108
+
109
+ def filePresent?(file, options = {})
110
+ result = self.exec("stat #{file}", true, options)
111
+ !result.start_with?('stat: cannot')
112
+ end
113
+
114
+ def adminExec(command, allowFailure = false, options = {})
115
+ if `echo $EUID`.strip == '0'
116
+ self.exec(command, allowFailure, options)
117
+ else
118
+ if options['dry']
119
+ prompt.say("Would execute: sudo #{command} >/dev/null")
120
+ else
121
+ self.exec('sudo ' + command, false, options)
122
+ end
123
+ end
124
+ end
125
+
126
+ def download(target, source, options = {})
127
+ copy(source, target, options[:dry])
128
+ end
129
+
130
+ def upload(source, target, options = {})
131
+ copy(source, target, options[:dry])
132
+ end
133
+
134
+ def uploadFolder(folder, target, options = {})
135
+ upload(folder, target, options)
136
+ end
137
+
138
+ def remoteDownload(url, targetDir, options = {})
139
+ filename = File.basename(Addressable::URI.parse(url).path)
140
+ targetFile = File.expand_path(targetDir + filename)
141
+ if !File.exist?(targetFile)
142
+ mkdir(File.expand_path(targetDir), false)
143
+ prompt.say('Downloading... ' + url)
144
+ response = ::HTTP.follow.get(url)
145
+ raise "Failed to download file: #{response.status}" unless response.status.success?
146
+ File.open(targetFile, 'wb') do |file|
147
+ response.body.each do |chunk|
148
+ file.write(chunk)
149
+ end
150
+ end
151
+ end
152
+ targetFile
153
+ end
154
+
155
+ def renderTemplate(template, target, outputPath, options)
156
+ variables = {
157
+ config: target,
158
+ }
159
+ result = template.result_with_hash(variables)
160
+ mkdir(File.dirname(outputPath), options['dry'])
161
+ if options['dry']
162
+ prompt.say('Would write to ' + outputPath)
163
+ else
164
+ File.write(outputPath, result)
165
+ end
166
+ end
167
+
168
+ def self.exec(command, allowFailure = false, options = {}, prompt = nil, logger = nil)
169
+ if options['dry']
170
+ message = "Would execute: #{command}"
171
+ if prompt
172
+ prompt.say(message)
173
+ else
174
+ puts message
175
+ end
176
+ return ''
177
+ end
178
+ if options[:hide]
179
+ command = ' ' + command
180
+ if logger
181
+ logger.debug("# **HIDDEN**")
182
+ end
183
+ else
184
+ if logger
185
+ logger.debug("# #{command}")
186
+ end
187
+ end
188
+ stdout, stdeerr, status = Open3.capture3(command)
189
+ if !allowFailure && !status.success?
190
+ $stderr.puts(stdout)
191
+ $stderr.puts(stdeerr)
192
+ raise ExecError.new("Failed '#{command}'", command, stdout, stdeerr, status)
193
+ end
194
+ if logger
195
+ logger.debug("(#{status.exitstatus})> #{stdout + stdeerr}")
196
+ end
197
+ stdout + stdeerr
198
+ rescue Errno::ENOENT => error
199
+ if !allowFailure
200
+ raise ExecError.new("Failed '#{command}'", command, error, nil, nil)
201
+ end
202
+ ''
203
+ end
204
+
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,92 @@
1
+
2
+ require_relative 'dhcp'
3
+ require_relative 'http'
4
+ require_relative 'tftp'
5
+
6
+ require 'socket'
7
+
8
+ module ConfigLMM
9
+ module IO
10
+ class PXE
11
+
12
+ def self.createMac(id)
13
+ '000101' + Zlib.crc32(id).to_s(16).ljust(8, '0')[2, 6]
14
+ end
15
+
16
+ def self.fillNetworkInfo(networkOptions, dhcp)
17
+ discoverMessage = dhcp.sendDiscover(self.createMac(networkOptions['ID']))
18
+ offerMessage = dhcp.waitOffer(discoverMessage, 10)
19
+ raise 'Didn\'t receive DHCP Offer' unless offerMessage
20
+ requestMessage = dhcp.sendRequest(discoverMessage, offerMessage)
21
+ ipinfo, ackMessage = dhcp.waitACK(discoverMessage, requestMessage, 5)
22
+ raise 'Didn\'t receive DHCP ACK' unless ackMessage
23
+
24
+ networkOptions['IP'] = ipinfo.last.ip_address
25
+ networkOptions['ClientIP'] = [ackMessage.yiaddr].pack('N').unpack('C4').join('.')
26
+
27
+ networkOptions['Subnet'] = '255.255.255.0'
28
+ subnet = ackMessage.options.find { |opt| opt.is_a?(::DHCP::SubnetMaskOption) }
29
+ networkOptions['Subnet'] = subnet.payload.join('.') if subnet
30
+
31
+ broadcast = ackMessage.options.find { |opt| opt.is_a?(::DHCP::BroadcastAddressOption) }
32
+ networkOptions['Broadcast'] = broadcast.payload.join('.') if broadcast
33
+
34
+ dns = ackMessage.options.find { |opt| opt.is_a?(::DHCP::DomainNameServerOption) }
35
+ networkOptions['DNS'] = dns.payload.join('.') if dns
36
+
37
+ router = ackMessage.options.find { |opt| opt.is_a?(::DHCP::RouterOption) }
38
+ networkOptions['Gateway'] = dns.payload.join('.') if router
39
+ end
40
+
41
+ def self.findMessageByIP(messages, ip)
42
+ if messages.length > 1
43
+ keys = messages.keys.select { |ipinfo| ipinfo.last.ip_address == ip }
44
+ raise "Couldn't match DHCP Discover message to respective interface" if keys.empty?
45
+ else
46
+ keys = [messages.keys.first]
47
+ end
48
+ [keys.first, messages[keys.first]]
49
+ end
50
+
51
+ def self.boot(dir, uri, networkOptions, bootFileResolver, options, logger)
52
+ dhcp = DHCP.new(logger)
53
+ if networkOptions['IP'].nil?
54
+ self.fillNetworkInfo(networkOptions, dhcp)
55
+ end
56
+
57
+ useHTTP = uri.scheme == 'pxe+http'
58
+ server = useHTTP ? HTTP.new(dir, networkOptions['IP'], options, logger) : TFTP.new(dir, networkOptions['IP'], logger)
59
+ server.start
60
+
61
+ discoverMessages = dhcp.waitDiscover(5 * 60, useHTTP)
62
+ raise 'Timeout while waiting for valid DHCP Discover request!' if discoverMessages.empty?
63
+ discoverMessageInfo = self.findMessageByIP(discoverMessages, networkOptions['IP'])
64
+ #offerMessage = dhcp.waitOffer(discoverMessageInfo, 5)
65
+ isFullDHCP = true #offerMessage.nil?
66
+
67
+ clientArch = 0x0000
68
+ clientArchOption = discoverMessageInfo.last.options.find { |option| option.is_a?(::DHCP::ClientSystemArchitectureOption) }
69
+ clientArch = clientArchOption.payload.pack('C*').unpack('n').first if clientArchOption
70
+ bootFile = bootFileResolver.call(clientArch)
71
+ bootFileSize = File.size(dir + bootFile)
72
+ bootFile = server.url(bootFile) if useHTTP
73
+
74
+ offerRequest = dhcp.sendOffer(discoverMessageInfo, isFullDHCP, networkOptions, bootFile, bootFileSize)
75
+ if isFullDHCP
76
+ requestMessages = dhcp.waitRequest(offerRequest, 10)
77
+ requestMessageInfo = self.findMessageByIP(requestMessages, networkOptions['IP'])
78
+ raise 'Timeout while waiting for valid DHCP Request!' if requestMessages.empty?
79
+ dhcp.sendACK(requestMessageInfo, offerRequest)
80
+ end
81
+ if clientArch != 0x0000 && !useHTTP
82
+ proxyRequestMessage = dhcp.waitProxyRequest(20)
83
+ raise 'Timeout while waiting for valid proxyDHCP Request!' unless proxyRequestMessage
84
+ dhcp.sendProxyACK(proxyRequestMessage, offerRequest)
85
+ end
86
+ server.wait(3 * 60)
87
+ raise 'Didn\'t receive boot file read!' unless server.hadRead?
88
+ server.stop()
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/ssh'
4
+ require 'net/scp'
5
+ require 'securerandom'
6
+
7
+ module ConfigLMM
8
+ module IO
9
+ class SSH
10
+
11
+ attr_reader :prompt
12
+ attr_reader :logger
13
+ attr_reader :ssh
14
+
15
+ def initialize(prompt, logger, ssh)
16
+ @prompt = prompt
17
+ @logger = logger
18
+ @ssh = ssh
19
+ end
20
+
21
+ def rm(path, dry)
22
+ if dry
23
+ prompt.say("Would remove ssh://#{ssh.transport.host}:#{ssh.transport.port}" + path)
24
+ else
25
+ self.class.exec!(ssh, "rm -rf #{path}", false, {}, self.prompt, self.logger)
26
+ end
27
+ end
28
+
29
+ def exec(command, allowFailure = false, options = {})
30
+ self.class.exec!(ssh, command, allowFailure, options, self.prompt, self.logger)
31
+ end
32
+
33
+ def adminExec(command, allowFailure = false, options = {})
34
+ self.exec(command, allowFailure, options)
35
+ end
36
+
37
+ def updateFile(file, options, atTop = false, comment = '#', &block)
38
+ localFile = options['output'] + '/' + SecureRandom.alphanumeric(10)
39
+ File.write(localFile, '')
40
+ self.exec("touch #{file}", false, options)
41
+ self.download(file, localFile, options)
42
+ Local.new(self.prompt, self.logger).updateFile(localFile, options, atTop, comment, &block)
43
+ self.upload(localFile, file, options)
44
+ end
45
+
46
+ def download(source, target, options = {})
47
+ if options['dry']
48
+ prompt.say("Would download scp -P #{ssh.transport.port} #{ssh.transport.host}:#{source} #{target}")
49
+ else
50
+ ssh.scp.download!(source, target)
51
+ end
52
+ end
53
+
54
+ def upload(source, target, options = {})
55
+ if options['dry']
56
+ prompt.say("Would upload scp -P #{ssh.transport.port} #{source} #{ssh.transport.host}:#{target}")
57
+ else
58
+ ssh.scp.upload!(source, target)
59
+ end
60
+ end
61
+
62
+ def uploadFolder(folder, target, options = {})
63
+ target += '/' + File.basename(folder) + '/'
64
+ Dir[folder + '/*'].each do |file|
65
+ if options['dry']
66
+ prompt.say("Would upload scp -P #{ssh.transport.port} #{file} #{ssh.transport.host}:#{target + File.basename(file)}")
67
+ else
68
+ ssh.scp.upload!(file, target + File.basename(file), recursive: true)
69
+ end
70
+ end
71
+ end
72
+
73
+ def shell(user)
74
+ Shell.new(self, user)
75
+ end
76
+
77
+ def self.tunnel(uri, &block)
78
+ uri = Addressable::URI.parse(uri) if uri.is_a?(String)
79
+ server, params = self.toParams(uri)
80
+ Net::SSH.start(server, nil, params, &block)
81
+ end
82
+
83
+ def self.ping(uri, prompt, logger)
84
+ server, params = self.toParams(uri)
85
+ options = Net::SSH.configuration_for(server, true).merge(params)
86
+ server = options[:host_name] || server
87
+ options[:timeout] = 3 unless options.key?(:timeout)
88
+ Net::SSH::Transport::Session.new(server, options)
89
+ true
90
+ rescue Errno::EHOSTUNREACH, Errno::ECONNREFUSED, Net::SSH::ConnectionTimeout
91
+ false
92
+ end
93
+
94
+ def self.toParams(locationUri)
95
+ server = locationUri.hostname
96
+ params = {}
97
+ params[:port] = locationUri.port if locationUri.port
98
+ params[:user] = locationUri.user if locationUri.user
99
+ [server, params]
100
+ end
101
+
102
+ def self.exec!(ssh, command, allowFailure = false, options = {}, prompt = nil, logger = nil)
103
+ if options['dry']
104
+ message = "Would execute: ssh #{ssh.transport.host} -p #{ssh.transport.port} '#{command}'"
105
+ if prompt
106
+ prompt.say(message)
107
+ else
108
+ puts message
109
+ end
110
+ return ''
111
+ end
112
+ status = {}
113
+ output = ''
114
+ if options[:hide]
115
+ command = ' ' + command
116
+ if logger
117
+ logger.debug("[#{ssh.transport.host}:#{ssh.transport.port}]# **HIDDEN**")
118
+ end
119
+ else
120
+ if logger
121
+ logger.debug("[#{ssh.transport.host}:#{ssh.transport.port}]# #{command}")
122
+ end
123
+ end
124
+
125
+ channel = ssh.exec(command, status: status) do |channel, stream, data|
126
+ output += data
127
+ end
128
+ channel.wait
129
+ output = output.dup.force_encoding(Encoding::UTF_8)
130
+ if logger
131
+ logger.debug("[#{ssh.transport.host}:#{ssh.transport.port}](#{status[:exit_code]})> #{output}")
132
+ end
133
+ if !allowFailure && (status[:exit_code].nil? || !status[:exit_code].zero?) && status[:exit_signal].to_i != 4 # SIGILL... Sometimes this happens for unknown reason
134
+ raise ExecError.new("Failed '#{command}'", command, output, output, status)
135
+ end
136
+ output
137
+ end
138
+
139
+ def self.cmd(uri)
140
+ uri = Addressable::URI.parse(uri) if uri.is_a?(String)
141
+ server, sshParams = self.toParams(uri)
142
+ cmd = 'ssh '
143
+ cmd += '-p ' + sshParams[:port] if sshParams[:port]
144
+ cmd += sshParams[:user] + '@' if sshParams[:port]
145
+ cmd + server
146
+ end
147
+
148
+ def self.sshSuccess?(ssh, command)
149
+ status = {}
150
+ ssh.exec!(command, status)
151
+ status[:exit_code].zero?
152
+ end
153
+
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,105 @@
1
+
2
+ require 'tftp'
3
+
4
+ module ConfigLMM
5
+ module IO
6
+
7
+ class TFTPHandler < ::TFTP::Handler::Base
8
+
9
+ attr_reader :lastReadTime
10
+
11
+ def initialize(dir, opts)
12
+ @Dir = dir
13
+ @lastReadTime = nil
14
+ super(opts)
15
+ end
16
+
17
+ def processRequest(tag, req, sock, src)
18
+ case req
19
+ when ::TFTP::Packet::RRQ
20
+ if !req.filename.match?(/\w[\w\-\.\/]*/)
21
+ log :warn, "#{tag} #{req.filename} - File not found"
22
+ sock.send(::TFTP::Packet::ERROR.new(1, 'File not found.').encode, 0)
23
+ return false
24
+ end
25
+ filename = req.filename
26
+ loop do
27
+ filename = filename.gsub('//', '/')
28
+ break unless filename.include?('//')
29
+ end
30
+ filename = filename.gsub('../', '')
31
+ path = @Dir + filename
32
+ if File.file?(path)
33
+ mode = 'r'
34
+ mode += 'b' if req.mode == :octet
35
+ io = File.open(path, mode)
36
+ log :debug, "#{tag} Sending #{req.filename} - #{path}"
37
+ if req.options.key?('tsize')
38
+ sendOACK(tag, sock, { 'tsize' => io.stat.size })
39
+ end
40
+ send(tag, sock, io)
41
+ io.close
42
+ @lastReadTime = Time.now
43
+ return true
44
+ else
45
+ log :warn, "#{tag} #{req.filename} - File not found"
46
+ sock.send(::TFTP::Packet::ERROR.new(1, 'File not found.').encode, 0)
47
+ end
48
+ when ::TFTP::Packet::WRQ
49
+ log :info, "#{tag} Denied write request for #{req.filename}"
50
+ sock.send(::TFTP::Packet::ERROR.new(2, 'Access denied.').encode, 0)
51
+ end
52
+ return false
53
+ end
54
+
55
+ # Handle a session.
56
+ #
57
+ # Has to close the socket (and any other resources).
58
+ #
59
+ # @param tag [String] Tag used for logging
60
+ # @param req [Packet] The initial request packet
61
+ # @param sock [UDPSocket] Connected socket
62
+ # @param src [UDPSource] Initial connection information
63
+ def run!(tag, req, sock, src)
64
+ processRequest(tag, req, sock, src)
65
+ sock.close
66
+ end
67
+ end
68
+
69
+ class TFTP
70
+ def initialize(dir, ip, logger, options = {})
71
+ @Logger = logger
72
+ @Handler = TFTPHandler.new(dir, { **options, logger: @Logger })
73
+ @Server = ::TFTP::Server::Base.new(@Handler, { **options, logger: @Logger })
74
+ end
75
+
76
+ def start()
77
+ Thread.new do
78
+ @Server.run!
79
+ end
80
+ @Logger.debug('Listening on UDP port 69 (TFTP Server)')
81
+ end
82
+
83
+ def stop()
84
+ @Server.stop
85
+ end
86
+
87
+ def wait(timeout)
88
+ timeoutTime = Time.now + timeout
89
+ previousReadTime = @Handler.lastReadTime
90
+ loop do
91
+ if previousReadTime != @Handler.lastReadTime
92
+ previousReadTime = @Handler.lastReadTime
93
+ timeoutTime = Time.now + timeout
94
+ end
95
+ return if Time.now >= timeoutTime
96
+ sleep(0.1)
97
+ end
98
+ end
99
+
100
+ def hadRead?
101
+ !@Handler.lastReadTime.nil?
102
+ end
103
+ end
104
+ end
105
+ end
data/lib/ConfigLMM/io.rb CHANGED
@@ -1,2 +1,4 @@
1
1
 
2
2
  require_relative 'io/configList'
3
+ require_relative 'io/connection'
4
+ require_relative 'io/pxe'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConfigLMM
4
+ module Secrets
5
+ class EnvStore
6
+
7
+ def initialize(logger, prompt)
8
+ @Secrets = {}
9
+ @Logger = logger
10
+ @Prompt = prompt
11
+ end
12
+
13
+ def getID(id, name)
14
+ "#{id.upcase}_#{name.upcase.tr('@.', '_')}"
15
+ end
16
+
17
+ def load(id, name)
18
+ raise "Invalid id! #{id.inspect}" unless id
19
+ id = getID(id, name)
20
+ if @Secrets.key?(id)
21
+ @Secrets[id]
22
+ else
23
+ ENV[id]
24
+ end
25
+ end
26
+
27
+ def store(id, name, value)
28
+ raise "Invalid secret #{value.inspect}!" if !value.is_a?(String) || value.include?("\n")
29
+ id = getID(id, name)
30
+ @Secrets[id] = value
31
+ end
32
+
33
+ def print(message, value)
34
+ @Prompt.say(message + ': ' + value, :color => :magenta)
35
+ end
36
+
37
+ end
38
+ end
39
+ end