ConfigLMM 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/CNAME +1 -0
  4. data/Examples/.lmm.state.yaml +159 -0
  5. data/Examples/ConfigLMM.mm.yaml +32 -0
  6. data/Examples/Implemented.mm.yaml +252 -4
  7. data/Examples/SmallBusiness.mm.yaml +492 -0
  8. data/Plugins/Apps/Answer/answer.lmm.rb +165 -0
  9. data/Plugins/Apps/Answer/answer@.service +40 -0
  10. data/Plugins/Apps/ArchiSteamFarm/ArchiSteamFarm.conf.erb +0 -3
  11. data/Plugins/Apps/ArchiSteamFarm/ArchiSteamFarm.lmm.rb +0 -1
  12. data/Plugins/Apps/Authentik/Authentik-ProxyOutpost.container +7 -1
  13. data/Plugins/Apps/Authentik/Authentik-Server.container +6 -1
  14. data/Plugins/Apps/Authentik/Authentik-Worker.container +6 -1
  15. data/Plugins/Apps/Authentik/Authentik.conf.erb +12 -7
  16. data/Plugins/Apps/Authentik/Authentik.lmm.rb +226 -61
  17. data/Plugins/Apps/BookStack/BookStack.conf.erb +0 -3
  18. data/Plugins/Apps/BookStack/BookStack.container +5 -0
  19. data/Plugins/Apps/BookStack/BookStack.lmm.rb +14 -3
  20. data/Plugins/Apps/Cassandra/Cassandra.lmm.rb +9 -19
  21. data/Plugins/Apps/ClickHouse/ClickHouse.container +28 -0
  22. data/Plugins/Apps/ClickHouse/ClickHouse.lmm.rb +113 -0
  23. data/Plugins/Apps/ClickHouse/Config/listen.yaml +2 -0
  24. data/Plugins/Apps/ClickHouse/Config/logger.yaml +8 -0
  25. data/Plugins/Apps/ClickHouse/Config/zookeepers.yaml +5 -0
  26. data/Plugins/Apps/ClickHouse/Connection.rb +96 -0
  27. data/Plugins/Apps/Discourse/Discourse-Sidekiq.container +5 -0
  28. data/Plugins/Apps/Discourse/Discourse.conf.erb +1 -4
  29. data/Plugins/Apps/Discourse/Discourse.container +4 -0
  30. data/Plugins/Apps/Discourse/Discourse.lmm.rb +116 -55
  31. data/Plugins/Apps/Dovecot/Dovecot.lmm.rb +74 -62
  32. data/Plugins/Apps/ERPNext/ERPNext-Frontend.container +6 -1
  33. data/Plugins/Apps/ERPNext/ERPNext-Queue.container +5 -0
  34. data/Plugins/Apps/ERPNext/ERPNext-Scheduler.container +5 -0
  35. data/Plugins/Apps/ERPNext/ERPNext-Websocket.container +6 -1
  36. data/Plugins/Apps/ERPNext/ERPNext.container +6 -1
  37. data/Plugins/Apps/ERPNext/ERPNext.lmm.rb +138 -127
  38. data/Plugins/Apps/GitLab/GitLab.container +6 -0
  39. data/Plugins/Apps/GitLab/GitLab.lmm.rb +43 -49
  40. data/Plugins/Apps/Homepage/Homepage.conf.erb +86 -0
  41. data/Plugins/Apps/Homepage/Homepage.container +19 -0
  42. data/Plugins/Apps/Homepage/Homepage.lmm.rb +54 -0
  43. data/Plugins/Apps/IPFS/IPFS.conf.erb +0 -3
  44. data/Plugins/Apps/IPFS/IPFS.lmm.rb +0 -1
  45. data/Plugins/Apps/InfluxDB/InfluxDB.conf.erb +0 -3
  46. data/Plugins/Apps/InfluxDB/InfluxDB.lmm.rb +0 -1
  47. data/Plugins/Apps/Jackett/Jackett.conf.erb +0 -3
  48. data/Plugins/Apps/Jackett/Jackett.lmm.rb +0 -1
  49. data/Plugins/Apps/Jellyfin/Jellyfin.conf.erb +0 -3
  50. data/Plugins/Apps/Jellyfin/Jellyfin.lmm.rb +0 -1
  51. data/Plugins/Apps/LetsEncrypt/LetsEncrypt.lmm.rb +49 -28
  52. data/Plugins/Apps/LibreTranslate/LibreTranslate.container +21 -0
  53. data/Plugins/Apps/LibreTranslate/LibreTranslate.lmm.rb +34 -0
  54. data/Plugins/Apps/Lobsters/Containerfile +81 -0
  55. data/Plugins/Apps/Lobsters/Lobsters-Tasks.container +26 -0
  56. data/Plugins/Apps/Lobsters/Lobsters.conf.erb +99 -0
  57. data/Plugins/Apps/Lobsters/Lobsters.container +27 -0
  58. data/Plugins/Apps/Lobsters/Lobsters.lmm.rb +196 -0
  59. data/Plugins/Apps/Lobsters/crontab +3 -0
  60. data/Plugins/Apps/Lobsters/database.yml +26 -0
  61. data/Plugins/Apps/Lobsters/entrypoint.sh +30 -0
  62. data/Plugins/Apps/Lobsters/generateCredentials.rb +19 -0
  63. data/Plugins/Apps/Lobsters/lobsters-cron.sh +25 -0
  64. data/Plugins/Apps/Lobsters/lobsters-daily.sh +23 -0
  65. data/Plugins/Apps/Lobsters/puma.rb +49 -0
  66. data/Plugins/Apps/MariaDB/Connection.rb +55 -0
  67. data/Plugins/Apps/MariaDB/MariaDB.lmm.rb +60 -53
  68. data/Plugins/Apps/Mastodon/Mastodon-Sidekiq.container +22 -0
  69. data/Plugins/Apps/Mastodon/Mastodon-Streaming.container +20 -0
  70. data/Plugins/Apps/Mastodon/Mastodon.conf.erb +34 -45
  71. data/Plugins/Apps/Mastodon/Mastodon.container +28 -0
  72. data/Plugins/Apps/Mastodon/Mastodon.lmm.rb +240 -5
  73. data/Plugins/Apps/Mastodon/configlmm.rake +30 -0
  74. data/Plugins/Apps/Mastodon/entrypoint.sh +16 -0
  75. data/Plugins/Apps/Matrix/Element.container +5 -0
  76. data/Plugins/Apps/Matrix/Matrix.conf.erb +2 -8
  77. data/Plugins/Apps/Matrix/Matrix.lmm.rb +100 -71
  78. data/Plugins/Apps/Matrix/Synapse.container +5 -0
  79. data/Plugins/Apps/Netdata/Netdata.conf.erb +0 -3
  80. data/Plugins/Apps/Netdata/Netdata.lmm.rb +0 -1
  81. data/Plugins/Apps/Nextcloud/Nextcloud.conf.erb +3 -4
  82. data/Plugins/Apps/Nextcloud/Nextcloud.lmm.rb +150 -68
  83. data/Plugins/Apps/Nextcloud/autoconfig.php +13 -0
  84. data/Plugins/Apps/Nextcloud/config.php +10 -1
  85. data/Plugins/Apps/Nextcloud/nextcloudcron.service +8 -0
  86. data/Plugins/Apps/Nextcloud/nextcloudcron.timer +10 -0
  87. data/Plugins/Apps/Nginx/Connection.rb +93 -0
  88. data/Plugins/Apps/Nginx/conf.d/configlmm.conf +50 -9
  89. data/Plugins/Apps/Nginx/conf.d/languages.conf +21 -0
  90. data/Plugins/Apps/Nginx/config-lmm/errors.conf +25 -20
  91. data/Plugins/Apps/Nginx/config-lmm/gateway-errors.conf +20 -0
  92. data/Plugins/Apps/Nginx/config-lmm/proxy.conf +1 -1
  93. data/Plugins/Apps/Nginx/main.conf.erb +7 -3
  94. data/Plugins/Apps/Nginx/nginx.conf +2 -2
  95. data/Plugins/Apps/Nginx/nginx.lmm.rb +99 -81
  96. data/Plugins/Apps/Nginx/proxy.conf.erb +11 -3
  97. data/Plugins/Apps/Odoo/Odoo.conf.erb +0 -3
  98. data/Plugins/Apps/Odoo/Odoo.container +5 -0
  99. data/Plugins/Apps/Odoo/Odoo.lmm.rb +4 -5
  100. data/Plugins/Apps/Ollama/Ollama.container +26 -0
  101. data/Plugins/Apps/Ollama/Ollama.lmm.rb +73 -0
  102. data/Plugins/Apps/OpenTelemetry/Config/config.yaml +704 -0
  103. data/Plugins/Apps/OpenTelemetry/OpenTelemetry.lmm.rb +154 -0
  104. data/Plugins/Apps/OpenVidu/Ingress.container +5 -0
  105. data/Plugins/Apps/OpenVidu/OpenVidu.conf.erb +0 -3
  106. data/Plugins/Apps/OpenVidu/OpenVidu.container +5 -0
  107. data/Plugins/Apps/OpenVidu/OpenVidu.lmm.rb +7 -3
  108. data/Plugins/Apps/OpenVidu/OpenViduCall.conf.erb +0 -3
  109. data/Plugins/Apps/OpenVidu/OpenViduCall.container +5 -0
  110. data/Plugins/Apps/PHP-FPM/Connection.rb +91 -0
  111. data/Plugins/Apps/PHP-FPM/PHP-FPM.lmm.rb +31 -4
  112. data/Plugins/Apps/Peppermint/Peppermint.conf.erb +2 -5
  113. data/Plugins/Apps/Peppermint/Peppermint.container +5 -0
  114. data/Plugins/Apps/Peppermint/Peppermint.lmm.rb +29 -33
  115. data/Plugins/Apps/Perplexica/Perplexica.container +25 -0
  116. data/Plugins/Apps/Perplexica/Perplexica.lmm.rb +92 -0
  117. data/Plugins/Apps/Perplexica/config.toml +26 -0
  118. data/Plugins/Apps/Podman/Connection.rb +24 -0
  119. data/Plugins/Apps/Podman/Podman.lmm.rb +80 -0
  120. data/Plugins/Apps/Podman/storage.conf +6 -0
  121. data/Plugins/Apps/Postfix/Postfix.lmm.rb +242 -164
  122. data/Plugins/Apps/PostgreSQL/Connection.rb +97 -0
  123. data/Plugins/Apps/PostgreSQL/PostgreSQL.lmm.rb +184 -148
  124. data/Plugins/Apps/Pterodactyl/Pterodactyl.conf.erb +0 -3
  125. data/Plugins/Apps/Pterodactyl/Pterodactyl.lmm.rb +0 -2
  126. data/Plugins/Apps/Pterodactyl/Wings.conf.erb +0 -3
  127. data/Plugins/Apps/RVM/RVM.lmm.rb +57 -0
  128. data/Plugins/Apps/Roundcube/Roundcube.conf.erb +0 -3
  129. data/Plugins/Apps/Roundcube/Roundcube.lmm.rb +15 -19
  130. data/Plugins/Apps/SSH/SSH.lmm.rb +9 -15
  131. data/Plugins/Apps/SearXNG/SearXNG.container +22 -0
  132. data/Plugins/Apps/SearXNG/SearXNG.lmm.rb +79 -0
  133. data/Plugins/Apps/SearXNG/limiter.toml +40 -0
  134. data/Plugins/Apps/SearXNG/settings.yml +2 -0
  135. data/Plugins/Apps/SigNoz/Config/alerts.yml +11 -0
  136. data/Plugins/Apps/SigNoz/Config/otel-collector-config.yaml +110 -0
  137. data/Plugins/Apps/SigNoz/Config/otel-collector-opamp-config.yaml +1 -0
  138. data/Plugins/Apps/SigNoz/Config/prometheus.yml +18 -0
  139. data/Plugins/Apps/SigNoz/SigNoz-Collector.container +23 -0
  140. data/Plugins/Apps/SigNoz/SigNoz-Migrator.container +17 -0
  141. data/Plugins/Apps/SigNoz/SigNoz.conf.erb +61 -0
  142. data/Plugins/Apps/SigNoz/SigNoz.container +26 -0
  143. data/Plugins/Apps/SigNoz/SigNoz.lmm.rb +319 -0
  144. data/Plugins/Apps/Solr/log4j2.xml +89 -0
  145. data/Plugins/Apps/Solr/solr.lmm.rb +82 -0
  146. data/Plugins/Apps/Sunshine/Sunshine.conf.erb +0 -3
  147. data/Plugins/Apps/Sunshine/Sunshine.lmm.rb +0 -1
  148. data/Plugins/Apps/Tunnel/tunnel.lmm.rb +33 -37
  149. data/Plugins/Apps/UVdesk/UVdesk.conf.erb +0 -3
  150. data/Plugins/Apps/Umami/Umami.container +19 -0
  151. data/Plugins/Apps/Umami/Umami.lmm.rb +108 -0
  152. data/Plugins/Apps/Valkey/Valkey.lmm.rb +54 -42
  153. data/Plugins/Apps/Vaultwarden/Vaultwarden.conf.erb +9 -6
  154. data/Plugins/Apps/Vaultwarden/Vaultwarden.container +7 -1
  155. data/Plugins/Apps/Vaultwarden/Vaultwarden.lmm.rb +64 -29
  156. data/Plugins/Apps/Wiki.js/Wiki.js.conf.erb +1 -4
  157. data/Plugins/Apps/Wiki.js/Wiki.js.container +5 -0
  158. data/Plugins/Apps/Wiki.js/Wiki.js.lmm.rb +31 -37
  159. data/Plugins/Apps/YaCy/YaCy.conf.erb +93 -0
  160. data/Plugins/Apps/YaCy/YaCy.container +21 -0
  161. data/Plugins/Apps/YaCy/YaCy.lmm.rb +160 -0
  162. data/Plugins/Apps/ZooKeeper/ZooKeeper.container +24 -0
  163. data/Plugins/Apps/ZooKeeper/ZooKeeper.lmm.rb +68 -0
  164. data/Plugins/Apps/bitmagnet/bitmagnet.conf.erb +0 -3
  165. data/Plugins/Apps/bitmagnet/bitmagnet.lmm.rb +0 -1
  166. data/Plugins/Apps/gollum/gollum.conf.erb +2 -4
  167. data/Plugins/Apps/gollum/gollum.container +6 -0
  168. data/Plugins/Apps/gollum/gollum.lmm.rb +51 -50
  169. data/Plugins/Apps/llama.cpp/llama.cpp.container +28 -0
  170. data/Plugins/Apps/llama.cpp/llama.cpp.lmm.rb +90 -0
  171. data/Plugins/Apps/vLLM/vLLM.container +32 -0
  172. data/Plugins/Apps/vLLM/vLLM.lmm.rb +89 -0
  173. data/Plugins/OS/General/Utils.lmm.rb +26 -0
  174. data/Plugins/OS/Linux/Connection.rb +472 -0
  175. data/Plugins/OS/Linux/Debian/preseed.cfg.erb +25 -6
  176. data/Plugins/OS/Linux/Flavours.yaml +13 -0
  177. data/Plugins/OS/Linux/Grub/grub.cfg +10 -0
  178. data/Plugins/OS/Linux/HTTP.rb +32 -0
  179. data/Plugins/OS/Linux/Linux.lmm.rb +533 -187
  180. data/Plugins/OS/Linux/Packages.yaml +20 -1
  181. data/Plugins/OS/Linux/Services.yaml +8 -0
  182. data/Plugins/OS/Linux/Shell.rb +70 -0
  183. data/Plugins/OS/Linux/Syslinux/default +8 -0
  184. data/Plugins/OS/Linux/WireGuard/WireGuard.lmm.rb +83 -59
  185. data/Plugins/OS/Linux/WireGuard/wg0.conf.erb +3 -0
  186. data/Plugins/OS/Linux/openSUSE/autoinst.xml.erb +29 -3
  187. data/Plugins/OS/Linux/systemd/systemd.lmm.rb +13 -11
  188. data/Plugins/OS/Routers/Aruba/ArubaInstant.lmm.rb +6 -5
  189. data/Plugins/Platforms/GitHub.lmm.rb +73 -28
  190. data/Plugins/Platforms/GoDaddy/GoDaddy.lmm.rb +9 -6
  191. data/Plugins/Platforms/Proxmox/Proxmox.lmm.rb +402 -0
  192. data/Plugins/Platforms/Proxmox/XTerm.rb +321 -0
  193. data/Plugins/Platforms/libvirt/libvirt.lmm.rb +38 -13
  194. data/Plugins/Platforms/porkbun.lmm.rb +12 -2
  195. data/Plugins/Platforms/porkbun_spec.rb +2 -2
  196. data/Plugins/Services/DNS/AmberBit.lmm.rb +1 -1
  197. data/Plugins/Services/DNS/ArubaItDNS.lmm.rb +1 -1
  198. data/Plugins/Services/DNS/NICLV.lmm.rb +1 -1
  199. data/Plugins/Services/DNS/PowerDNS.lmm.rb +70 -68
  200. data/Plugins/Services/DNS/tonic.lmm.rb +22 -12
  201. data/lib/ConfigLMM/Framework/plugins/dns.rb +4 -3
  202. data/lib/ConfigLMM/Framework/plugins/linuxApp.rb +145 -184
  203. data/lib/ConfigLMM/Framework/plugins/nginxApp.rb +34 -17
  204. data/lib/ConfigLMM/Framework/plugins/plugin.rb +53 -181
  205. data/lib/ConfigLMM/Framework/plugins/store.rb +4 -4
  206. data/lib/ConfigLMM/Framework/variables.rb +75 -0
  207. data/lib/ConfigLMM/Framework.rb +1 -0
  208. data/lib/ConfigLMM/cli.rb +12 -6
  209. data/lib/ConfigLMM/commands/configsCommand.rb +37 -6
  210. data/lib/ConfigLMM/commands/diff.rb +33 -9
  211. data/lib/ConfigLMM/context.rb +22 -3
  212. data/lib/ConfigLMM/io/configList.rb +82 -6
  213. data/lib/ConfigLMM/io/connection.rb +143 -0
  214. data/lib/ConfigLMM/io/dhcp.rb +330 -0
  215. data/lib/ConfigLMM/io/http.rb +78 -0
  216. data/lib/ConfigLMM/io/local.rb +207 -0
  217. data/lib/ConfigLMM/io/pxe.rb +92 -0
  218. data/lib/ConfigLMM/io/ssh.rb +156 -0
  219. data/lib/ConfigLMM/io/tftp.rb +105 -0
  220. data/lib/ConfigLMM/io.rb +2 -0
  221. data/lib/ConfigLMM/secrets/envStore.rb +39 -0
  222. data/lib/ConfigLMM/secrets/fileStore.rb +43 -0
  223. data/lib/ConfigLMM/state.rb +2 -1
  224. data/lib/ConfigLMM/version.rb +2 -1
  225. data/lib/ConfigLMM.rb +1 -0
  226. data/{Examples → scripts}/configlmmAuth.sh +7 -5
  227. metadata +205 -8
@@ -2,16 +2,21 @@
2
2
  # encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
 
5
+ require_relative 'secrets/envStore'
6
+ require_relative 'secrets/fileStore'
7
+
5
8
  require 'yaml'
6
9
 
7
10
  module ConfigLMM
8
11
  class Context
9
12
  CONTEXT_FILE = 'configlmm/context.yaml'
10
13
 
11
- def initialize(logger, prompt, xdg, contextFile)
14
+ def initialize(logger, prompt, xdg, options)
12
15
  @Logger = logger
13
16
  @Prompt = prompt
14
- load!(xdg.config_home, contextFile)
17
+ contextFile = options[:context]
18
+ secretsProvider = options[:secrets]
19
+ load!(xdg.config_home, contextFile, secretsProvider)
15
20
  end
16
21
 
17
22
  def likes?(name)
@@ -30,10 +35,15 @@ module ConfigLMM
30
35
  @Context['Dislikes'] += context['Dislikes']
31
36
  end
32
37
 
38
+ def secrets
39
+ @Secrets
40
+ end
41
+
33
42
  private
34
43
 
35
- def load!(configHome, contextFile)
44
+ def load!(configHome, contextFile, secretsProvider)
36
45
  @Context = {}
46
+ @Secrets = Secrets::EnvStore.new(@Logger, @Prompt)
37
47
  if (contextFile && !File.exist?(contextFile))
38
48
  @Logger.error("Provided Context file doesn't exist: #{contextFile}")
39
49
  raise 'Missing Context!'
@@ -46,6 +56,15 @@ module ConfigLMM
46
56
  end
47
57
  @Context['Likes'] ||= []
48
58
  @Context['Dislikes'] ||= []
59
+
60
+ if secretsProvider && secretsProvider != 'no'
61
+ url = Addressable::URI.parse(secretsProvider)
62
+ if url.scheme.nil?
63
+ @Secrets = Secrets::FileStore.new(@Logger, @Prompt, url.path)
64
+ else
65
+ raise 'Only file secret provider is implemented!'
66
+ end
67
+ end
49
68
  end
50
69
 
51
70
  end
@@ -3,6 +3,7 @@
3
3
  require_relative 'path'
4
4
  require 'find'
5
5
  require 'yaml'
6
+ require 'deep_merge'
6
7
 
7
8
  module ConfigLMM
8
9
  module IO
@@ -12,7 +13,7 @@ module ConfigLMM
12
13
  def self.create(targets, logger)
13
14
  targets = targets.uniq.select do |target|
14
15
  exist = File.exist?(target)
15
- logger.warn("'#{path}' doesn't exist, ignoring!") unless exist
16
+ logger.warn("'#{target}' doesn't exist, ignoring!") unless exist
16
17
  exist
17
18
  end
18
19
  self.new(targets)
@@ -59,7 +60,7 @@ module ConfigLMM
59
60
  raise ConfigError.new("Missing 'Type' field: #{id}!")
60
61
  end
61
62
  data['Name'] = id unless data.has_key?('Name')
62
- data['Type'] = data['Type'].to_sym
63
+ data['Type'] = data['Type'].to_s.gsub('.', '').to_sym
63
64
  data[:Parent] = parent
64
65
  data
65
66
  end
@@ -67,10 +68,12 @@ module ConfigLMM
67
68
  def toConfig(context)
68
69
  config = {}
69
70
  @Sources.each do |source|
71
+ seenIncludes = Set.new
70
72
  data = YAML.safe_load_file(source.to_s, permitted_classes: [Symbol])
71
73
  next unless data.is_a?(Hash)
74
+ data = processIncludes(data, source.to_s, seenIncludes)
75
+ data = processVariables(data, source.to_s)
72
76
  data.each do |id, data|
73
- normalizedId = self.class.normalizeId(id)
74
77
  if id == '_CONTEXT_'
75
78
  context.add(data)
76
79
  next
@@ -78,9 +81,12 @@ module ConfigLMM
78
81
 
79
82
  self.class.processConfig(id, data, source.parent)
80
83
 
81
- # TODO FIXME we should deep merge them instead
82
- raise ConfigError.new("Duplicate ID: #{id} (#{normalizedId}) - #{source}") if config.has_key?(normalizedId)
83
- config[normalizedId] = data
84
+ normalizedId = self.class.normalizeId(id)
85
+ if config.has_key?(normalizedId)
86
+ config[normalizedId].deep_merge!(data, :extend_existing_arrays => true)
87
+ else
88
+ config[normalizedId] = data
89
+ end
84
90
  end
85
91
  #rescue YAML::SyntaxError => error
86
92
  # raise ConfigError.new(error)
@@ -95,6 +101,76 @@ module ConfigLMM
95
101
  def to_a
96
102
  @Sources
97
103
  end
104
+
105
+ private
106
+
107
+ def processIncludes(data, source, seenIncludes)
108
+ seenIncludes << source
109
+ includes = data['_INCLUDE_']
110
+ return data if includes.nil?
111
+ includes = [includes] unless includes.is_a?(Array)
112
+ includesData = {}
113
+ includes.each do |file|
114
+ file = file.to_s
115
+ file += '.yaml' unless file.end_with?('.yaml')
116
+ file = File.expand_path(file, File.dirname(source))
117
+ next if seenIncludes.include?(file)
118
+ raise ConfigError.new("#{file} doesn't exist! - #{source}") unless File.exist?(file)
119
+ innerData = YAML.safe_load_file(file, permitted_classes: [Symbol])
120
+ next unless innerData.is_a?(Hash)
121
+ innerData = processIncludes(innerData, file, seenIncludes.dup)
122
+ includesData.deep_merge!(innerData, :extend_existing_arrays => true)
123
+ end
124
+ includesData.deep_merge!(data, :extend_existing_arrays => true)
125
+ includesData.delete('_INCLUDE_')
126
+ includesData
127
+ end
128
+
129
+ def processVariables(data, source)
130
+ variables = data['_VARIABLES_']
131
+ raise ConfigError.new("_VARIABLES_ must be a hash! - #{source}") if !variables.nil? && !variables.is_a?(Hash)
132
+ variables = variables.to_h.transform_keys { |key| key.to_s.upcase }
133
+ data.delete('_VARIABLES_')
134
+ data.each do |id, content|
135
+ data[id] = processContent(content, variables, source)
136
+ end
137
+ data
138
+ end
139
+
140
+ def processContent(content, variables, source)
141
+ if content.is_a?(Array)
142
+ content.each_with_index do |item, i|
143
+ content[i] = processContent(item, variables, source)
144
+ end
145
+ elsif content.is_a?(Hash)
146
+ newContent = {}
147
+ content.each do |key, item|
148
+ key = processContent(key, variables, source) if key.is_a?(String)
149
+ newContent[key] = processContent(item, variables, source)
150
+ end
151
+ content = newContent
152
+ else
153
+ content = fillVariable(content, variables, source)
154
+ end
155
+ content
156
+ end
157
+
158
+ def fillVariable(content, variables, source)
159
+ variableStart = content.to_s.index('${VAR:')
160
+ if variableStart
161
+ variableEnd = content.index('}', variableStart + 6)
162
+ raise "Unterminated variable: #{content}" if variableEnd.nil?
163
+ name = content[variableStart + 6...variableEnd].to_s
164
+ raise ConfigError.new("Empty variable name #{content} - #{source}") if name.empty?
165
+ raise ConfigError.new("Undefined variable #{name} - #{source}") unless variables.key?(name.upcase)
166
+ if variableStart.zero? && variableEnd == content.length
167
+ content = variables[name.upcase]
168
+ else
169
+ content = content[0...variableStart].to_s + variables[name.upcase].to_s + fillVariable(content[(variableEnd + 1)..-1].to_s, variables, source).to_s
170
+ end
171
+ end
172
+ content
173
+ end
98
174
  end
99
175
  end
100
176
  end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'addressable/uri'
4
+ require_relative 'local'
5
+ require_relative 'ssh'
6
+
7
+ module ConfigLMM
8
+ module IO
9
+ ConnectionError = Class.new(Framework::PluginProcessError)
10
+
11
+ class ExecError < ConnectionError
12
+ attr_reader :command, :stdout, :stderr, :status
13
+
14
+ def initialize(message, command, stdout, stderr, status)
15
+ super(message)
16
+ @command = command
17
+ @stdout = stdout
18
+ @stderr = stderr
19
+ @status = status
20
+ end
21
+
22
+ def to_s
23
+ str = super + "\n"
24
+ str += stdout.to_s + "\n" if stdout
25
+ str += stderr.to_s if stdout != stderr
26
+ str
27
+ end
28
+ end
29
+
30
+ class Connection
31
+
32
+ attr_reader :type
33
+ attr_reader :tunnel
34
+ attr_reader :local
35
+ attr_reader :prompt
36
+ attr_reader :logger
37
+
38
+ def initialize(type, tunnel, prompt, logger)
39
+ @type = type
40
+ @tunnel = tunnel
41
+ @local = Local.new(prompt, logger)
42
+ @prompt = prompt
43
+ @logger = logger
44
+ end
45
+
46
+ def rm(path, dry)
47
+ @tunnel.rm(path, dry)
48
+ end
49
+
50
+ def exec(command, allowFailure = false, options = {})
51
+ @tunnel.exec(command, allowFailure, options)
52
+ end
53
+
54
+ def adminExec(command, allowFailure = false, options = {})
55
+ @tunnel.adminExec(command, allowFailure, options)
56
+ end
57
+
58
+ def filePresent?(file, options = {})
59
+ self.exec("stat #{file}", true, options) if options['dry']
60
+ result = self.exec("stat #{file}", true, { **options, 'dry' => false })
61
+ !result.start_with?('stat: cannot')
62
+ end
63
+
64
+ def fileLink?(file, options = {})
65
+ self.exec("stat #{file}", true, options) if options['dry']
66
+ result = self.exec("stat #{file}", true, { **options, 'dry' => false })
67
+ return false if result.start_with?('stat: cannot')
68
+ result.include?('symbolic link') && !result.include?('regular file')
69
+ end
70
+
71
+ def updateFile(file, options, atTop = false, comment = '#', &block)
72
+ @tunnel.updateFile(file, options, atTop, comment, &block)
73
+ end
74
+
75
+ def download(source, target, options = {})
76
+ @tunnel.download(source, target, options)
77
+ end
78
+
79
+ def upload(source, target, options = {})
80
+ @tunnel.upload(source, target, options)
81
+ end
82
+
83
+ def uploadFolder(folder, target, options = {})
84
+ @tunnel.uploadFolder(folder, target, options)
85
+ end
86
+
87
+ def self.exec(command, ssh = nil, allowFailure = false, options = {})
88
+ if ssh.nil?
89
+ Local.exec(command, allowFailure, options)
90
+ else
91
+ SSH.exec!(ssh, command, allowFailure, options)
92
+ end
93
+ end
94
+
95
+ def self.cmdSuccess?(command, ssh = nil)
96
+ if ssh.nil?
97
+ system(command, :out => File::NULL)
98
+ else
99
+ SSH.sshSuccess?(ssh, command)
100
+ end
101
+ end
102
+
103
+ # `connect': No route to host - connect(2) for 192.168.1.3:22 (Errno::EHOSTUNREACH)
104
+ # `connect': Connection refused - connect(2) for 192.168.1.3:22 (Errno::ECONNREFUSED)
105
+ def self.tunnel(uri, target, context, prompt, logger, &block)
106
+ scheme, uri = self.processURI(uri)
107
+ case scheme
108
+ when 'local'
109
+ yield(Connection.new(:Local, Local.new(prompt, logger), prompt, logger))
110
+ when 'ssh'
111
+ SSH.tunnel(uri) do |ssh|
112
+ yield(Connection.new(:SSH, SSH.new(prompt, logger, ssh), prompt, logger))
113
+ end
114
+ when 'proxmox+xterm'
115
+ LMM::Proxmox.withXTerm(uri, target, context, prompt, logger) do |xterm|
116
+ yield(Connection.new(:Proxmox, xterm, prompt, logger))
117
+ end
118
+ else
119
+ raise ConnectionError.new("Unsupported protocol: #{scheme}!")
120
+ end
121
+ end
122
+
123
+ def self.ping(uri, target, context, prompt, logger)
124
+ scheme, uri = self.processURI(uri)
125
+ case scheme
126
+ when 'local'
127
+ return true
128
+ when 'ssh'
129
+ SSH.ping(uri, prompt, logger)
130
+ else
131
+ raise ConnectionError.new("Unimplemented protocol: #{scheme}!")
132
+ end
133
+ end
134
+
135
+ def self.processURI(uri)
136
+ return 'local' if uri.nil? || uri.to_s.empty? || uri == '@me'
137
+ uri = Addressable::URI.parse(uri) if uri.is_a?(String)
138
+ [uri.scheme, uri]
139
+ end
140
+
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,330 @@
1
+
2
+ require 'net/dhcp'
3
+ require 'socket'
4
+ require 'securerandom'
5
+ require 'zlib'
6
+
7
+ module ConfigLMM
8
+ module IO
9
+ class DHCP
10
+ READ_SIZE = 1500
11
+ MIN_SIZE = 300
12
+
13
+ DHCP_BROADCAST_FLAG = 0x8000
14
+ DHCP_CLIENTMACHINEID = 0x61 # Option 97 - Client Machine Identifier
15
+ DHCP_UUID_IDENTIFIER = 0x00
16
+
17
+ def initialize(logger)
18
+ @Logger = logger
19
+ @ServerSocket = UDPSocket.new
20
+ @ServerSocket.do_not_reverse_lookup = true
21
+ @ServerSocket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
22
+ @ServerSocket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
23
+ @ServerSocket.setsockopt(Socket::IPPROTO_IP, Socket::IP_PKTINFO, true)
24
+ @ServerSocket.bind('0.0.0.0', 67)
25
+ @Logger.debug('Listening on UDP port 67 (DHCP Server)')
26
+
27
+ @ClientSocket = UDPSocket.new
28
+ @ClientSocket.do_not_reverse_lookup = true
29
+ @ClientSocket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
30
+ @ClientSocket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
31
+ @ClientSocket.setsockopt(Socket::IPPROTO_IP, Socket::IP_PKTINFO, true)
32
+ @ClientSocket.bind('0.0.0.0', 68)
33
+ @Logger.debug('Listening on UDP port 68 (DHCP Client)')
34
+
35
+ @ProxyServerSocket = UDPSocket.new
36
+ @ProxyServerSocket.do_not_reverse_lookup = true
37
+ @ProxyServerSocket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
38
+ @ProxyServerSocket.bind('0.0.0.0', 4011)
39
+ @Logger.debug('Listening on UDP port 4011 (proxyDHCP Server)')
40
+ end
41
+
42
+ def sendDiscover(mac)
43
+ discover = ::DHCP::Discover.new(flags: DHCP_BROADCAST_FLAG,
44
+ chaddr: [mac + '00000000000000000000'].pack('H*').unpack('C16'))
45
+ targetAddr = '<broadcast>'
46
+ @ClientSocket.send(discover.pack, 0, targetAddr, 67)
47
+ discover
48
+ end
49
+
50
+ def waitDiscover(timeout, useHTTP)
51
+ @Logger.info('Waiting for DHCP Discover request')
52
+ timeoutTime = Time.now + timeout
53
+ msgs = {}
54
+ loop do
55
+ return msgs if Time.now >= timeoutTime
56
+ data, sender_sockaddr, rflags, *controls = @ServerSocket.recvmsg_nonblock()
57
+ ipinfo = controls.find { |ancillary| ancillary.cmsg_is?(:IP, :PKTINFO) }.ip_pktinfo
58
+ if data.bytesize >= MIN_SIZE
59
+ message = ::DHCP::Message.from_udp_payload(data)
60
+ if isValidDiscover?(message, useHTTP)
61
+ msgs[ipinfo] = message
62
+ timeoutTime = Time.now + 0.0001
63
+ else
64
+ @Logger.debug("Ignoring unexpected DHCP request packet")
65
+ end
66
+ else
67
+ @Logger.debug("Ignoring too small DHCP packet")
68
+ end
69
+ rescue ::IO::WaitReadable
70
+ ::IO.select([@ServerSocket], [], [], 0.0001)
71
+ retry
72
+ end
73
+ msgs
74
+ end
75
+
76
+ def waitOffer(discoverMessage, timeout)
77
+ @Logger.info('Waiting for DHCP Offer response')
78
+ timeoutTime = Time.now + timeout
79
+ loop do
80
+ return false if Time.now >= timeoutTime
81
+ data, inetAddr = @ClientSocket.recvfrom_nonblock(READ_SIZE)
82
+ if data.bytesize >= MIN_SIZE
83
+ message = ::DHCP::Message.from_udp_payload(data)
84
+ if message.xid == discoverMessage.xid && message.chaddr[0, message.hlen] == discoverMessage.chaddr[0, discoverMessage.hlen]
85
+ return message
86
+ else
87
+ @Logger.debug("Ignoring unexpected DHCP response packet")
88
+ end
89
+ else
90
+ @Logger.debug("Ignoring too small DHCP packet")
91
+ end
92
+ rescue ::IO::WaitReadable
93
+ ::IO.select([@ClientSocket], [], [], 0.1)
94
+ retry
95
+ end
96
+ false
97
+ end
98
+
99
+ def packUUID(uuid)
100
+ uuidParts = uuid.split('-')
101
+ uuidParts[0, 3].map(&:reverse).pack('h*h*h*') + uuidParts[3, 2].pack('H*H*')
102
+ end
103
+
104
+ def sendOffer(discoverMessageInfo, isFullDHCP, networkOptions, bootFile, bootFileSize)
105
+ ipinfo, discoverMessage = discoverMessageInfo
106
+ fname = bootFile
107
+ sname = networkOptions['IP']
108
+ siaddr = networkOptions['IP'].split('.').map(&:to_i).pack('C4').unpack('N').first
109
+ yiaddr = 0
110
+ useHTTP = bootFile.start_with?('http://')
111
+ etherboot = discoverMessage.options.find { |option| option.is_a?(::DHCP::PrivateOption) } # Etherboot
112
+ # Etherboot won't HTTP boot as HTTPClient...
113
+ vendorClass = useHTTP && !etherboot ? 'HTTPClient' : 'PXEClient'
114
+
115
+ if isFullDHCP
116
+ yiaddr = networkOptions['ClientIP'].split('.').map(&:to_i).pack('C4').unpack('N').first
117
+ end
118
+ options = [
119
+ ::DHCP::MessageTypeOption.new(payload: [$DHCP_MSG_OFFER]),
120
+ ::DHCP::ServerIdentifierOption.new(payload: networkOptions['IP'].split('.').map(&:to_i)),
121
+ ::DHCP::Option.new(type: $DHCP_BOOTFILESIZE, payload: [(bootFileSize / 512.0).ceil].pack('n').unpack('C*')),
122
+ ::DHCP::Option.new(type: $DHCP_BOOTFILENAME, payload: bootFile.unpack('C*') + [0]),
123
+ ::DHCP::VendorClassIDOption.new(payload: vendorClass.unpack('C*'))
124
+ ]
125
+
126
+ if !useHTTP
127
+ options << ::DHCP::Option.new(type: $DHCP_TFTPSERVER, payload: networkOptions['IP'].unpack('C*') + [0])
128
+ end
129
+
130
+ clientMachineIdOption = discoverMessage.options.find { |option| option.type == DHCP_CLIENTMACHINEID }
131
+ if clientMachineIdOption
132
+ options << clientMachineIdOption
133
+ end
134
+ if isFullDHCP
135
+ options << ::DHCP::IPAddressLeaseTimeOption.new()
136
+ options << ::DHCP::Option.new(type: $DHCP_RENEWTIME, payload: [3600].pack('N').unpack('C*'))
137
+ options << ::DHCP::Option.new(type: $DHCP_REBINDTIME, payload: [3600].pack('N').unpack('C*'))
138
+ options << ::DHCP::SubnetMaskOption.new(payload: networkOptions['Subnet'].split('.').map(&:to_i))
139
+ options << ::DHCP::BroadcastAddressOption.new(payload: networkOptions['Broadcast'].split('.').map(&:to_i))
140
+ options << ::DHCP::RouterOption.new(payload: networkOptions['Gateway'].split('.').map(&:to_i)) if networkOptions['Gateway']
141
+ options << ::DHCP::DomainNameServerOption.new(payload: networkOptions['DNS'].split('.').map(&:to_i)) if networkOptions['DNS']
142
+ end
143
+
144
+ offer = ::DHCP::Offer.new(xid: discoverMessage.xid,
145
+ flags: discoverMessage.flags & DHCP_BROADCAST_FLAG,
146
+ siaddr: siaddr,
147
+ yiaddr: yiaddr,
148
+ chaddr: discoverMessage.chaddr,
149
+ sname: sname,
150
+ fname: fname,
151
+ options: options)
152
+ targetAddr = '<broadcast>'
153
+ #targetAddr = networkOptions['ClientIP'] if (discoverMessage.flags & DHCP_BROADCAST_FLAG).zero?
154
+ sockaddr = Socket.sockaddr_in(68, targetAddr)
155
+ @ServerSocket.sendmsg(offer.pack, 0, sockaddr, Socket::AncillaryData.ip_pktinfo(*ipinfo))
156
+ offer
157
+ end
158
+
159
+ def sendRequest(discoverMessage, offerRequest)
160
+ options = [::DHCP::MessageTypeOption.new({:payload=>[$DHCP_MSG_REQUEST]}), ::DHCP::ParameterRequestListOption.new]
161
+ serverIdentifier = offerRequest.options.find { |opt| opt.is_a?(::DHCP::ServerIdentifierOption) }
162
+ options << serverIdentifier if serverIdentifier
163
+ options << ::DHCP::RequestedIPAddressOption.new(payload: [offerRequest.yiaddr].pack('N').unpack('C4'))
164
+ request = ::DHCP::Request.new(
165
+ xid: discoverMessage.xid,
166
+ flags: DHCP_BROADCAST_FLAG,
167
+ chaddr: discoverMessage.chaddr,
168
+ options: options)
169
+ targetAddr = '<broadcast>'
170
+ @ClientSocket.send(request.pack, 0, targetAddr, 67)
171
+ request
172
+ end
173
+
174
+ def waitRequest(offerRequest, timeout)
175
+ @Logger.info('Waiting for DHCP Request response')
176
+ timeoutTime = Time.now + timeout
177
+ msgs = {}
178
+ loop do
179
+ return msgs if Time.now >= timeoutTime
180
+ data, sender_sockaddr, rflags, *controls = @ServerSocket.recvmsg_nonblock()
181
+ ipinfo = controls.find { |ancillary| ancillary.cmsg_is?(:IP, :PKTINFO) }.ip_pktinfo
182
+ if data.bytesize >= MIN_SIZE
183
+ message = ::DHCP::Message.from_udp_payload(data)
184
+ if isValidRequest?(message, offerRequest)
185
+ msgs[ipinfo] = message
186
+ timeoutTime = Time.now + 0.1
187
+ elsif message.is_a?(::DHCP::Request)
188
+ @Logger.info("Some router received DHCP Request packet")
189
+ elsif message.is_a?(::DHCP::Discover)
190
+ @Logger.debug("Received DHCP Discover packet, resending DHCP Offer")
191
+ sockaddr = Socket.sockaddr_in(68, '<broadcast>')
192
+ @ServerSocket.sendmsg(offerRequest.pack, 0, sockaddr, Socket::AncillaryData.ip_pktinfo(*ipinfo))
193
+ else
194
+ @Logger.debug("Ignoring unexpected DHCP response packet")
195
+ end
196
+ else
197
+ @Logger.debug("Ignoring too small DHCP packet")
198
+ end
199
+ rescue ::IO::WaitReadable
200
+ ::IO.select([@ServerSocket], [], [], 0.1)
201
+ retry
202
+ end
203
+ msgs
204
+ end
205
+
206
+ def createACK(requestMessage, offerRequest)
207
+ options = offerRequest.options.dup
208
+ options.delete_if { |option| option.is_a?(::DHCP::MessageTypeOption) }
209
+ ack = ::DHCP::ACK.new(xid: requestMessage.xid,
210
+ flags: requestMessage.flags,
211
+ ciaddr: offerRequest.ciaddr,
212
+ yiaddr: offerRequest.yiaddr,
213
+ siaddr: offerRequest.siaddr,
214
+ giaddr: offerRequest.giaddr,
215
+ chaddr: offerRequest.chaddr,
216
+ sname: offerRequest.sname,
217
+ fname: offerRequest.fname,
218
+ options: [::DHCP::MessageTypeOption.new(payload: [$DHCP_MSG_ACK])] + options)
219
+ ack
220
+ end
221
+
222
+ def waitACK(discoverMessage, requestMessage, timeout)
223
+ @Logger.info('Waiting for DHCP ACK response')
224
+ timeoutTime = Time.now + timeout
225
+ loop do
226
+ return false if Time.now >= timeoutTime
227
+ data, sender_sockaddr, rflags, *controls = @ClientSocket.recvmsg_nonblock()
228
+ ipinfo = controls.find { |ancillary| ancillary.cmsg_is?(:IP, :PKTINFO) }.ip_pktinfo
229
+ if data.bytesize >= MIN_SIZE
230
+ message = ::DHCP::Message.from_udp_payload(data)
231
+ if message.xid == discoverMessage.xid && message.chaddr[0, message.hlen] == discoverMessage.chaddr[0, discoverMessage.hlen]
232
+ return [ipinfo, message]
233
+ else
234
+ @Logger.debug("Ignoring unexpected DHCP response packet")
235
+ end
236
+ else
237
+ @Logger.debug("Ignoring too small DHCP packet")
238
+ end
239
+ rescue ::IO::WaitReadable
240
+ ::IO.select([@ClientSocket], [], [], 0.1)
241
+ retry
242
+ end
243
+ false
244
+ end
245
+
246
+ def sendACK(requestMessageInfo, offerRequest)
247
+ ipinfo, requestMessage = requestMessageInfo
248
+ ack = createACK(requestMessage, offerRequest)
249
+ targetAddr = '<broadcast>'
250
+ #targetAddr = [offerRequest.yiaddr].pack('N').unpack('C4').join('.') if (offerRequest.flags & DHCP_BROADCAST_FLAG).zero?
251
+ sockaddr = Socket.sockaddr_in(68, targetAddr)
252
+ @ServerSocket.sendmsg(ack.pack, 0, sockaddr, Socket::AncillaryData.ip_pktinfo(*ipinfo))
253
+ end
254
+
255
+ def waitProxyRequest(timeout)
256
+ @Logger.info('Waiting for proxyDHCP Request response')
257
+ timeoutTime = Time.now + timeout
258
+ loop do
259
+ return false if Time.now >= timeoutTime
260
+ data, inetAddr = @ProxyServerSocket.recvfrom_nonblock(READ_SIZE)
261
+ if data.bytesize >= MIN_SIZE
262
+ message = ::DHCP::Message.from_udp_payload(data)
263
+ if isValidProxyRequest?(message)
264
+ return message
265
+ else
266
+ @Logger.debug("Ignoring unexpected DHCP response packet")
267
+ end
268
+ else
269
+ @Logger.debug("Ignoring too small DHCP packet")
270
+ end
271
+ rescue ::IO::WaitReadable
272
+ ::IO.select([@ProxyServerSocket], [], [], 0.1)
273
+ retry
274
+ end
275
+ false
276
+ end
277
+
278
+ def sendProxyACK(proxyRequestMessage, offerRequest)
279
+ ack = createACK(proxyRequestMessage, offerRequest)
280
+ ack.ciaddr = proxyRequestMessage.ciaddr
281
+ targetAddr = [proxyRequestMessage.ciaddr].pack('N').unpack('C4').join('.')
282
+ @ProxyServerSocket.send(ack.pack, 0, targetAddr, 4011)
283
+ end
284
+
285
+ def isValidDiscover?(message, useHTTP)
286
+ isValid = message.is_a?(::DHCP::Discover) &&
287
+ message.htype == $DHCP_HTYPE_ETHERNET &&
288
+ message.hlen == $DHCP_HLEN_ETHERNET &&
289
+ message.giaddr.zero?
290
+ return isValid unless isValid
291
+ if useHTTP
292
+ clientArchOption = message.options.find { |option| option.is_a?(::DHCP::ClientSystemArchitectureOption) }
293
+ return true if clientArchOption && clientArchOption.payload.pack('C*').unpack('n').first == 0x0010 # x64 UEFI HTTP
294
+ return true if message.options.find { |option| option.is_a?(::DHCP::PrivateOption) } # Etherboot
295
+ else
296
+ message.options.each do |option|
297
+ return true if option.is_a?(::DHCP::ParameterRequestListOption) &&
298
+ option.payload.include?($DHCP_TFTPSERVER) &&
299
+ option.payload.include?($DHCP_BOOTFILENAME)
300
+ end
301
+ end
302
+ false
303
+ end
304
+
305
+ def isValidRequest?(message, offerRequest)
306
+ isValid = message.is_a?(::DHCP::Request) &&
307
+ message.htype == $DHCP_HTYPE_ETHERNET &&
308
+ message.hlen == $DHCP_HLEN_ETHERNET &&
309
+ message.giaddr.zero? &&
310
+ message.xid == offerRequest.xid &&
311
+ message.chaddr[0, message.hlen] == offerRequest.chaddr[0, offerRequest.hlen]
312
+
313
+ return isValid unless isValid
314
+ message.options.each do |option|
315
+ return true if option.is_a?(::DHCP::ServerIdentifierOption) &&
316
+ option.payload == offerRequest.options.find { |offerOption| offerOption.is_a?(::DHCP::ServerIdentifierOption) }.payload
317
+ end
318
+ false
319
+ end
320
+
321
+ def isValidProxyRequest?(message)
322
+ isValid = message.is_a?(::DHCP::Request) &&
323
+ message.htype == $DHCP_HTYPE_ETHERNET &&
324
+ message.hlen == $DHCP_HLEN_ETHERNET &&
325
+ message.giaddr.zero?
326
+ end
327
+
328
+ end
329
+ end
330
+ end