ConfigLMM 0.1.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 (104) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +4 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Examples/Android.mm.yaml +8 -0
  6. data/Examples/Apps/Blog.mm.yaml +7 -0
  7. data/Examples/Apps/Jellyfin.mm.yaml +3 -0
  8. data/Examples/Implemented.mm.yaml +155 -0
  9. data/Examples/Keys.ini +7 -0
  10. data/Examples/Linux.mm.yaml +16 -0
  11. data/Examples/Windows.mm.yaml +11 -0
  12. data/Examples/configlmmAuth.sh +26 -0
  13. data/Plugins/Apps/ArchiSteamFarm/ArchiSteamFarm.conf.erb +38 -0
  14. data/Plugins/Apps/ArchiSteamFarm/ArchiSteamFarm.lmm.rb +19 -0
  15. data/Plugins/Apps/IPFS/IPFS.conf.erb +44 -0
  16. data/Plugins/Apps/IPFS/IPFS.lmm.rb +23 -0
  17. data/Plugins/Apps/InfluxDB/InfluxDB.conf.erb +34 -0
  18. data/Plugins/Apps/InfluxDB/InfluxDB.lmm.rb +19 -0
  19. data/Plugins/Apps/Jackett/Jackett.conf.erb +38 -0
  20. data/Plugins/Apps/Jackett/Jackett.lmm.rb +19 -0
  21. data/Plugins/Apps/Jellyfin/Jellyfin.conf.erb +59 -0
  22. data/Plugins/Apps/Jellyfin/Jellyfin.lmm.rb +23 -0
  23. data/Plugins/Apps/Mastodon/Mastodon.conf.erb +81 -0
  24. data/Plugins/Apps/Mastodon/Mastodon.lmm.rb +23 -0
  25. data/Plugins/Apps/Matrix/Matrix.conf.erb +36 -0
  26. data/Plugins/Apps/Matrix/Matrix.lmm.rb +23 -0
  27. data/Plugins/Apps/Netdata/Netdata.conf.erb +37 -0
  28. data/Plugins/Apps/Netdata/Netdata.lmm.rb +23 -0
  29. data/Plugins/Apps/Nextcloud/Nextcloud.conf.erb +165 -0
  30. data/Plugins/Apps/Nextcloud/Nextcloud.lmm.rb +23 -0
  31. data/Plugins/Apps/Nginx/config-lmm/errors.conf +31 -0
  32. data/Plugins/Apps/Nginx/config-lmm/private.conf +6 -0
  33. data/Plugins/Apps/Nginx/config-lmm/proxy.conf +15 -0
  34. data/Plugins/Apps/Nginx/config-lmm/public.conf +3 -0
  35. data/Plugins/Apps/Nginx/config-lmm/ssl.conf +18 -0
  36. data/Plugins/Apps/Nginx/main.conf +30 -0
  37. data/Plugins/Apps/Nginx/nginx.conf +90 -0
  38. data/Plugins/Apps/Nginx/nginx.lmm.rb +62 -0
  39. data/Plugins/Apps/Nginx/proxy.conf.erb +31 -0
  40. data/Plugins/Apps/Odoo/Odoo.conf.erb +44 -0
  41. data/Plugins/Apps/Odoo/Odoo.lmm.rb +23 -0
  42. data/Plugins/Apps/Pterodactyl/Pterodactyl.conf.erb +50 -0
  43. data/Plugins/Apps/Pterodactyl/Pterodactyl.lmm.rb +30 -0
  44. data/Plugins/Apps/Pterodactyl/Wings.conf.erb +38 -0
  45. data/Plugins/Apps/Sunshine/Sunshine.conf.erb +31 -0
  46. data/Plugins/Apps/Sunshine/Sunshine.lmm.rb +21 -0
  47. data/Plugins/Apps/Vaultwarden/Vaultwarden.conf.erb +48 -0
  48. data/Plugins/Apps/Vaultwarden/Vaultwarden.lmm.rb +25 -0
  49. data/Plugins/Apps/bitmagnet/bitmagnet.conf.erb +35 -0
  50. data/Plugins/Apps/bitmagnet/bitmagnet.lmm.rb +19 -0
  51. data/Plugins/Apps/gollum/config.ru +11 -0
  52. data/Plugins/Apps/gollum/gollum.conf.erb +41 -0
  53. data/Plugins/Apps/gollum/gollum.lmm.rb +52 -0
  54. data/Plugins/OS/Linux.lmm.rb +64 -0
  55. data/Plugins/OS/Routers/Aruba/ArubaInstant.lmm.rb +144 -0
  56. data/Plugins/Platforms/GitHub.lmm.rb +57 -0
  57. data/Plugins/Platforms/GoDaddy/GoDaddy.lmm.rb +83 -0
  58. data/Plugins/Platforms/GoDaddy/zone.txt.erb +13 -0
  59. data/Plugins/Platforms/porkbun.lmm.rb +129 -0
  60. data/Plugins/Platforms/porkbun_spec.rb +110 -0
  61. data/Plugins/Services/DNS/AmberBit.lmm.rb +14 -0
  62. data/Plugins/Services/DNS/ArubaItDNS.lmm.rb +14 -0
  63. data/Plugins/Services/DNS/NICLV.lmm.rb +18 -0
  64. data/Plugins/Services/DNS/PowerDNS.lmm.rb +261 -0
  65. data/Plugins/Services/DNS/tonic.lmm.rb +126 -0
  66. data/README.md +337 -0
  67. data/Rakefile +15 -0
  68. data/UNLICENSE +24 -0
  69. data/bin/configlmm +7 -0
  70. data/bin/console +11 -0
  71. data/bin/setup +8 -0
  72. data/lib/ConfigLMM/Framework/plugins/dns.rb +63 -0
  73. data/lib/ConfigLMM/Framework/plugins/errors.rb +23 -0
  74. data/lib/ConfigLMM/Framework/plugins/nginxApp.rb +55 -0
  75. data/lib/ConfigLMM/Framework/plugins/plugin.rb +167 -0
  76. data/lib/ConfigLMM/Framework/plugins/ssh.rb +37 -0
  77. data/lib/ConfigLMM/Framework/plugins/store.rb +57 -0
  78. data/lib/ConfigLMM/Framework/plugins.rb +5 -0
  79. data/lib/ConfigLMM/Framework/registrator.rb +32 -0
  80. data/lib/ConfigLMM/Framework.rb +9 -0
  81. data/lib/ConfigLMM/LMM/plugins.rb +5 -0
  82. data/lib/ConfigLMM/LMM.rb +8 -0
  83. data/lib/ConfigLMM/cli.rb +161 -0
  84. data/lib/ConfigLMM/command.rb +53 -0
  85. data/lib/ConfigLMM/commands/build.rb +41 -0
  86. data/lib/ConfigLMM/commands/cleanup.rb +30 -0
  87. data/lib/ConfigLMM/commands/configsCommand.rb +167 -0
  88. data/lib/ConfigLMM/commands/deploy.rb +39 -0
  89. data/lib/ConfigLMM/commands/diff.rb +45 -0
  90. data/lib/ConfigLMM/commands/list.rb +15 -0
  91. data/lib/ConfigLMM/commands/refresh.rb +46 -0
  92. data/lib/ConfigLMM/commands/types.rb +35 -0
  93. data/lib/ConfigLMM/commands/validate.rb +49 -0
  94. data/lib/ConfigLMM/context.rb +52 -0
  95. data/lib/ConfigLMM/io/configList.rb +98 -0
  96. data/lib/ConfigLMM/io/path.rb +48 -0
  97. data/lib/ConfigLMM/io/source.rb +47 -0
  98. data/lib/ConfigLMM/io.rb +2 -0
  99. data/lib/ConfigLMM/state.rb +78 -0
  100. data/lib/ConfigLMM/utils/filters.rb +126 -0
  101. data/lib/ConfigLMM/version.rb +5 -0
  102. data/lib/ConfigLMM.rb +6 -0
  103. data/sig/ConfigLMM.rbs +4 -0
  104. metadata +485 -0
@@ -0,0 +1,261 @@
1
+
2
+ # require 'fog'
3
+ require 'uri'
4
+ require 'addressable/uri'
5
+ require 'addressable/idna'
6
+ require 'fog/powerdns'
7
+
8
+ module ConfigLMM
9
+ module LMM
10
+ class PowerDNS < Framework::DNS
11
+
12
+ CONFIG_DIR = '/etc/pdns/pdns.d'
13
+ DEFAULT_HOST = 'localhost'
14
+ DEFAULT_PORT = 8081
15
+ SSH_TIMEOUT = 10
16
+
17
+ # TODO
18
+ # def actionPowerDNSValidate(id, target, activeState, context, options)
19
+ # We should check that target['DNS'] looks like valid config
20
+ #end
21
+
22
+ def actionPowerDNSBuild(id, target, activeState, context, options)
23
+ if target['Settings']
24
+ targetDir = options['output'] + CONFIG_DIR + '/'
25
+ mkdir(targetDir, options['dry'])
26
+ content = ''
27
+ target['Settings'].each do |name, value|
28
+ content += "#{name}=#{value}\n"
29
+ end
30
+ fileWrite(targetDir + 'configlmm.conf', content, options['dry'])
31
+ end
32
+ end
33
+
34
+ def actionPowerDNSRefresh(id, target, activeState, context, options)
35
+
36
+ connect(id, target, activeState, context, options) do |host, port, key|
37
+ refreshDNS(host, port, key, target['DNS'], activeState) if target['DNS']
38
+ end
39
+ end
40
+
41
+ #def actionPowerDNSDiff(id, target, activeState, context, options)
42
+ #end
43
+
44
+ def actionPowerDNSDeploy(id, target, activeState, context, options)
45
+ #actionPowerDNSDiff(id, target, activeState, context, options)
46
+
47
+ deploySettings(target)
48
+ if target['DNS']
49
+ connect(id, target, activeState, context, options) do |host, port, key|
50
+ updateDNS(host, port, key, target['DNS'])
51
+ end
52
+ end
53
+ end
54
+
55
+ def authenticate(actionMethod, target, activeState, context, options)
56
+ true
57
+ end
58
+
59
+ private
60
+
61
+ def refreshDNS(host, port, key, targetDNS, activeState)
62
+ dns = Fog::DNS::PowerDNS.new({
63
+ host: host,
64
+ port: port,
65
+ pdns_api_key: key
66
+ })
67
+
68
+ server = 'localhost'
69
+
70
+ targetDNS.each do |domain, info|
71
+ domain = Addressable::IDNA.to_ascii(domain)
72
+ canonicalDomain = domain + '.'
73
+
74
+ result = dns.list_zones(server)
75
+ domainsInfo = result.map { |info| [ info['name'], { :_meta_ => info } ] }.flatten
76
+ activeState['DNS'] = Hash[*domainsInfo]
77
+ activeState['DNS'].each do |domain, info|
78
+ zone = dns.get_zone(server, domain)
79
+ info[:_records_] = zone.to_h
80
+ end
81
+ end
82
+ end
83
+
84
+ def updateDNS(host, port, key, targetDNS)
85
+
86
+ dns = Fog::DNS::PowerDNS.new({
87
+ host: host,
88
+ port: port,
89
+ pdns_api_key: key
90
+ })
91
+
92
+ server = 'localhost'
93
+ targetDNS.each do |domain, info|
94
+ domain = Addressable::IDNA.to_ascii(domain)
95
+ canonicalDomain = domain + '.'
96
+
97
+ if !dns.list_zones(server).map { |zone| zone['name'].downcase }.include?(canonicalDomain.downcase)
98
+ dns.create_zone(server, canonicalDomain, [], { kind: 'Native' })
99
+ end
100
+
101
+ zone = dns.get_zone(server, canonicalDomain)
102
+
103
+ rrsets = []
104
+ remove = []
105
+ info.each do |name, data|
106
+ fullName = name + '.' + domain + '.'
107
+ fullName = domain + '.' if name == '@'
108
+ self.processDNS(domain, data).each do |type, records|
109
+ #remove += removeConflicting(zone, fullName, type)
110
+ rrset = {
111
+ name: fullName,
112
+ type: type,
113
+ ttl: records.first[:ttl],
114
+ changetype: 'REPLACE',
115
+ records: []
116
+ }
117
+ records.each do |record|
118
+ rrset[:records] << { content: record[:content], disabled: false }
119
+ end
120
+ rrsets << rrset
121
+ end
122
+ end
123
+
124
+ if !remove.empty?
125
+ dns.update_rrsets('localhost', zone['name'], { 'rrsets' => remove })
126
+ end
127
+ dns.update_rrsets('localhost', zone['name'], { 'rrsets' => rrsets })
128
+ end
129
+
130
+ end
131
+
132
+ def deploySettings(target)
133
+ if target['Settings']
134
+ # TODO
135
+ end
136
+ end
137
+
138
+
139
+ def connect(id, target, activeState, context, options)
140
+ host = DEFAULT_HOST
141
+ port = DEFAULT_PORT
142
+ key = ENV['POWERDNS_API_KEY']
143
+ key = target['Key'] if target['Key']
144
+ raise Framework::PluginProcessError.new('PowerDNS missing API key!') unless key
145
+
146
+ sshServer = nil
147
+ sshUser = nil
148
+ sshPort = nil
149
+ sshPassword = ENV['POWERDNS_SSH_PASSWORD']
150
+
151
+ if target['Location']
152
+ uri = Addressable::URI.parse(target['Location'])
153
+ if uri.scheme == 'ssh'
154
+ sshServer = uri.hostname
155
+ sshUser = uri.user
156
+ sshPort = uri.port if uri.port
157
+ params = {}
158
+ params = CGI.parse(uri.query) if uri.query
159
+ host = params['host'].first if params['host']
160
+ port = params['port'].first if params['port']
161
+ elsif uri.scheme == 'pdns'
162
+ host = uri.hostname
163
+ port = uri.port if uri.port
164
+ else
165
+ raise Framework::PluginProcessError.new('Unexpected protocol! Should be either ssh or pdns!')
166
+ end
167
+ end
168
+
169
+ if sshServer
170
+ sshParams = {}
171
+ sshParams[:port] = sshPort if sshPort
172
+ sshParams[:user] = sshUser if sshUser
173
+ sshParams[:password] = sshPassword if sshPassword
174
+
175
+ startPortForward(sshServer, sshParams, host, port) do |acquiredPort|
176
+ port = acquiredPort
177
+ host = 'localhost'
178
+ self.class.externalIp = externalIpFromSSH
179
+ end
180
+ waitPortForward(SSH_TIMEOUT)
181
+ end
182
+
183
+ yield(host, port, key)
184
+
185
+ if sshServer
186
+ finishPortForward
187
+ end
188
+ end
189
+
190
+ def removeConflicting(zone, name, type)
191
+ remove = []
192
+ if type == 'CNAME'
193
+ zone['rrsets'].each do |rrset|
194
+ if (rrset['name'].downcase == name.downcase && rrset['type'] == 'A')
195
+ remove << {
196
+ name: name,
197
+ type: 'A',
198
+ changetype: 'DELETE',
199
+ records: []
200
+ }
201
+ end
202
+ end
203
+ end
204
+ remove
205
+ end
206
+
207
+ def startPortForward(server, sshParams, targetHost, targetPort)
208
+ @SSHFowardProcessing = false
209
+ @SSHError = nil
210
+ @SSHThread = Thread.new do
211
+ @SSH = nil
212
+ Net::SSH.start(server, nil, sshParams) do |ssh|
213
+ @SSH = ssh
214
+ port = getFreePort
215
+ yield(port)
216
+
217
+ ssh.forward.local(port, targetHost, targetPort)
218
+ @SSHFowardProcessing = true
219
+ ssh.loop { ssh.busy? || @SSHFowardProcessing }
220
+ end
221
+ @SSH = nil
222
+ rescue IOError, SocketError, SystemCallError, Net::SSH::Exception, ScriptError => e
223
+ @SSHError = e.message
224
+ end
225
+ @SSHThread.report_on_exception = true
226
+ @SSHThread.abort_on_exception = true
227
+ end
228
+
229
+ def waitPortForward(timeout)
230
+ while !@SSHFowardProcessing && timeout.positive?
231
+ raise Framework::PluginProcessError, 'PowerDNS: ' + @SSHError.to_s unless @SSHError.nil?
232
+
233
+ sleep(0.2)
234
+ timeout -= 0.2
235
+ end
236
+
237
+ return if timeout.positive?
238
+
239
+ @SSHThread.terminate
240
+ raise Framework::PluginProcessError, 'PowerDNS: Timeout while waiting for SSH connection!'
241
+ end
242
+
243
+ def finishPortForward
244
+ @SSHFowardProcessing = false
245
+ @SSHThread.join(3)
246
+ end
247
+
248
+ def externalIpFromSSH
249
+ envs = @SSH.exec!('env').split("\n")
250
+ envVars = Hash[envs.map { |vars| vars.split('=', 2) }]
251
+ envVars['SSH_CLIENT'].split.first
252
+ end
253
+
254
+ def getFreePort
255
+ TCPServer.open(0) do |socket|
256
+ return socket.addr[1]
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,126 @@
1
+
2
+ require 'addressable/idna'
3
+ require 'http'
4
+ require 'nokogiri'
5
+
6
+ module ConfigLMM
7
+ module LMM
8
+ class Tonic < Framework::DNS
9
+
10
+ EDIT_URL = 'https://www.tonic.to/editdns.htm'
11
+ # UPDATE_URL = 'https://www.tonic.to/upddnssucc.htm'
12
+
13
+ def actionTonicDNSValidate(id, target, activeState, context, options)
14
+ errors = []
15
+ if target['Domain'].to_s.empty?
16
+ errors << id + ': Domain must be provided!'
17
+ end
18
+ if target['Nameservers'].to_h.empty?
19
+ errors << id + ': Nameservers must be provided!'
20
+ end
21
+ errors
22
+ end
23
+
24
+ def actionTonicDNSRefresh(id, target, activeState, context, options)
25
+ domain = target['Domain'].split('.').first
26
+ if domain.empty?
27
+ raise Framework::PluginProcessError.new('Invalid Domain for ' + id)
28
+ end
29
+ activeState['Domain'] = target['Domain']
30
+ response = HTTP.post(EDIT_URL, :form => {
31
+ command: 'editdns',
32
+ error: 'badpass.htm',
33
+ sld: domain,
34
+ password: ENV['TONIC_PASSWORD'],
35
+ 'B1.x' => "40",
36
+ 'B1.y' => "20"
37
+ })
38
+
39
+ fields = Nokogiri::HTML(response.to_s).xpath('//input[@value]/@value')
40
+ if !fields.empty?
41
+ if fields.first.value == 'updatedns'
42
+ values = fields.map { |attrs| attrs.value }
43
+ .reject { |v| v.empty? ||
44
+ v == 'updatedns' ||
45
+ v == 'Submit'
46
+ }
47
+ nameservers = Hash[*values].invert
48
+ activeState['Nameservers'] = nameservers
49
+ else
50
+ raise Framework::PluginProcessError.new('Unexpected value in response for ' + id)
51
+ end
52
+ else
53
+ raise Framework::PluginProcessError.new("Couldn't refresh " + id + "! Invalid TONIC_PASSWORD ?")
54
+ end
55
+ end
56
+
57
+ def actionTonicDNSDiff(id, target, activeState, context, options)
58
+ shouldMatch(id, 'Domain', 'Domain', target, activeState)
59
+ nameservers = activeState['Nameservers']&.transform_keys { |ns| Addressable::IDNA.to_unicode(ns) }
60
+ if target['Nameservers'] != nameservers
61
+ @Diff.update({'Nameservers' => [target['Nameservers'], nameservers]})
62
+ end
63
+ end
64
+
65
+ def actionTonicDNSDeploy(id, target, activeState, context, options)
66
+ domain = target['Domain'].split('.').first
67
+ if domain.empty?
68
+ raise Framework::PluginProcessError.new('Invalid Domain for ' + id)
69
+ end
70
+
71
+ actionTonicDNSDiff(id, target, activeState, context, options)
72
+
73
+ hosts = target['Nameservers'].keys.map { |ns| Addressable::IDNA.to_ascii(ns) }
74
+ addrs = target['Nameservers'].values
75
+
76
+ response = HTTP.post(EDIT_URL, :form => {
77
+ command: 'editdns',
78
+ error: 'badpass.htm',
79
+ sld: domain,
80
+ password: ENV['TONIC_PASSWORD'],
81
+ 'B1.x' => "40",
82
+ 'B1.y' => "20"
83
+ })
84
+
85
+ updateURL = Nokogiri::HTML(response.to_s).at('//form[@action]/@action')
86
+ if updateURL.nil?
87
+ raise Framework::PluginProcessError.new("Couldn't deploy " + id + "! Invalid TONIC_PASSWORD ?")
88
+ end
89
+
90
+ if options['dry']
91
+ prompt.say("Would HTTP POST #{updateURL.value}")
92
+ else
93
+ response = HTTP.cookies(response.cookies)
94
+ .post(updateURL.value, :form => {
95
+ command: 'updatedns',
96
+ priadd: addrs[0].to_s,
97
+ prihost: hosts[0].to_s,
98
+ secadd1: addrs[1].to_s,
99
+ sechost1: hosts[1].to_s,
100
+ secadd2: addrs[2].to_s,
101
+ sechost2: hosts[2].to_s,
102
+ secadd3: addrs[3].to_s,
103
+ sechost3: hosts[3].to_s,
104
+ 'B1.x': 45,
105
+ 'B1.y': 30
106
+ })
107
+
108
+ activeState['Domain'] = target['Domain']
109
+ activeState['Nameservers'] = target['Nameservers']
110
+
111
+ prompt.say(Nokogiri::HTML(response.to_s).at('//title/text()'))
112
+ end
113
+ end
114
+
115
+ def authenticate(actionMethod, target, activeState, context, options)
116
+ authSecret = ENV['TONIC_PASSWORD']
117
+ if authSecret.to_s.empty?
118
+ prompt.say('Set your Tonic DNS password to TONIC_PASSWORD as Environment Variable')
119
+ raise Framework::PluginPrerequisite.new('Need TONIC_PASSWORD')
120
+ end
121
+ true
122
+ end
123
+
124
+ end
125
+ end
126
+ end