cloud-mu 3.1.3 → 3.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +10 -2
  3. data/bin/mu-adopt +5 -1
  4. data/bin/mu-load-config.rb +2 -3
  5. data/bin/mu-run-tests +112 -27
  6. data/cloud-mu.gemspec +20 -20
  7. data/cookbooks/mu-tools/libraries/helper.rb +2 -1
  8. data/cookbooks/mu-tools/libraries/monkey.rb +35 -0
  9. data/cookbooks/mu-tools/recipes/google_api.rb +2 -2
  10. data/cookbooks/mu-tools/resources/disk.rb +1 -1
  11. data/extras/image-generators/Google/centos6.yaml +1 -0
  12. data/extras/image-generators/Google/centos7.yaml +1 -1
  13. data/modules/mommacat.ru +5 -15
  14. data/modules/mu.rb +10 -14
  15. data/modules/mu/adoption.rb +20 -14
  16. data/modules/mu/cleanup.rb +13 -9
  17. data/modules/mu/cloud.rb +26 -26
  18. data/modules/mu/clouds/aws.rb +100 -59
  19. data/modules/mu/clouds/aws/alarm.rb +4 -2
  20. data/modules/mu/clouds/aws/bucket.rb +25 -21
  21. data/modules/mu/clouds/aws/cache_cluster.rb +25 -23
  22. data/modules/mu/clouds/aws/collection.rb +21 -20
  23. data/modules/mu/clouds/aws/container_cluster.rb +47 -26
  24. data/modules/mu/clouds/aws/database.rb +57 -68
  25. data/modules/mu/clouds/aws/dnszone.rb +14 -14
  26. data/modules/mu/clouds/aws/endpoint.rb +20 -16
  27. data/modules/mu/clouds/aws/firewall_rule.rb +19 -16
  28. data/modules/mu/clouds/aws/folder.rb +7 -7
  29. data/modules/mu/clouds/aws/function.rb +15 -12
  30. data/modules/mu/clouds/aws/group.rb +14 -10
  31. data/modules/mu/clouds/aws/habitat.rb +16 -13
  32. data/modules/mu/clouds/aws/loadbalancer.rb +16 -15
  33. data/modules/mu/clouds/aws/log.rb +13 -10
  34. data/modules/mu/clouds/aws/msg_queue.rb +15 -8
  35. data/modules/mu/clouds/aws/nosqldb.rb +18 -11
  36. data/modules/mu/clouds/aws/notifier.rb +11 -6
  37. data/modules/mu/clouds/aws/role.rb +87 -70
  38. data/modules/mu/clouds/aws/search_domain.rb +30 -19
  39. data/modules/mu/clouds/aws/server.rb +102 -72
  40. data/modules/mu/clouds/aws/server_pool.rb +47 -28
  41. data/modules/mu/clouds/aws/storage_pool.rb +5 -6
  42. data/modules/mu/clouds/aws/user.rb +13 -10
  43. data/modules/mu/clouds/aws/vpc.rb +135 -121
  44. data/modules/mu/clouds/azure.rb +16 -9
  45. data/modules/mu/clouds/azure/container_cluster.rb +2 -3
  46. data/modules/mu/clouds/azure/firewall_rule.rb +10 -10
  47. data/modules/mu/clouds/azure/habitat.rb +8 -6
  48. data/modules/mu/clouds/azure/loadbalancer.rb +5 -5
  49. data/modules/mu/clouds/azure/role.rb +8 -10
  50. data/modules/mu/clouds/azure/server.rb +65 -25
  51. data/modules/mu/clouds/azure/user.rb +5 -7
  52. data/modules/mu/clouds/azure/vpc.rb +12 -15
  53. data/modules/mu/clouds/cloudformation.rb +8 -7
  54. data/modules/mu/clouds/cloudformation/vpc.rb +2 -4
  55. data/modules/mu/clouds/google.rb +39 -24
  56. data/modules/mu/clouds/google/bucket.rb +9 -11
  57. data/modules/mu/clouds/google/container_cluster.rb +27 -42
  58. data/modules/mu/clouds/google/database.rb +6 -9
  59. data/modules/mu/clouds/google/firewall_rule.rb +11 -10
  60. data/modules/mu/clouds/google/folder.rb +16 -9
  61. data/modules/mu/clouds/google/function.rb +127 -161
  62. data/modules/mu/clouds/google/group.rb +21 -18
  63. data/modules/mu/clouds/google/habitat.rb +18 -15
  64. data/modules/mu/clouds/google/loadbalancer.rb +14 -16
  65. data/modules/mu/clouds/google/role.rb +48 -31
  66. data/modules/mu/clouds/google/server.rb +105 -105
  67. data/modules/mu/clouds/google/server_pool.rb +12 -31
  68. data/modules/mu/clouds/google/user.rb +67 -13
  69. data/modules/mu/clouds/google/vpc.rb +58 -65
  70. data/modules/mu/config.rb +89 -1738
  71. data/modules/mu/config/bucket.rb +3 -3
  72. data/modules/mu/config/collection.rb +3 -3
  73. data/modules/mu/config/container_cluster.rb +2 -2
  74. data/modules/mu/config/dnszone.rb +5 -5
  75. data/modules/mu/config/doc_helpers.rb +517 -0
  76. data/modules/mu/config/endpoint.rb +3 -3
  77. data/modules/mu/config/firewall_rule.rb +118 -3
  78. data/modules/mu/config/folder.rb +3 -3
  79. data/modules/mu/config/function.rb +2 -2
  80. data/modules/mu/config/group.rb +3 -3
  81. data/modules/mu/config/habitat.rb +3 -3
  82. data/modules/mu/config/loadbalancer.rb +3 -3
  83. data/modules/mu/config/log.rb +3 -3
  84. data/modules/mu/config/msg_queue.rb +3 -3
  85. data/modules/mu/config/nosqldb.rb +3 -3
  86. data/modules/mu/config/notifier.rb +2 -2
  87. data/modules/mu/config/ref.rb +333 -0
  88. data/modules/mu/config/role.rb +3 -3
  89. data/modules/mu/config/schema_helpers.rb +508 -0
  90. data/modules/mu/config/search_domain.rb +3 -3
  91. data/modules/mu/config/server.rb +86 -58
  92. data/modules/mu/config/server_pool.rb +2 -2
  93. data/modules/mu/config/tail.rb +189 -0
  94. data/modules/mu/config/user.rb +3 -3
  95. data/modules/mu/config/vpc.rb +44 -4
  96. data/modules/mu/defaults/Google.yaml +2 -2
  97. data/modules/mu/deploy.rb +13 -10
  98. data/modules/mu/groomer.rb +1 -1
  99. data/modules/mu/groomers/ansible.rb +69 -24
  100. data/modules/mu/groomers/chef.rb +52 -44
  101. data/modules/mu/logger.rb +17 -14
  102. data/modules/mu/master.rb +317 -2
  103. data/modules/mu/master/chef.rb +3 -4
  104. data/modules/mu/master/ldap.rb +3 -3
  105. data/modules/mu/master/ssl.rb +12 -2
  106. data/modules/mu/mommacat.rb +85 -1766
  107. data/modules/mu/mommacat/daemon.rb +394 -0
  108. data/modules/mu/mommacat/naming.rb +366 -0
  109. data/modules/mu/mommacat/storage.rb +689 -0
  110. data/modules/tests/bucket.yml +4 -0
  111. data/modules/tests/{win2k12.yaml → needwork/win2k12.yaml} +0 -0
  112. data/modules/tests/regrooms/aws-iam.yaml +201 -0
  113. data/modules/tests/regrooms/bucket.yml +19 -0
  114. metadata +112 -102
@@ -0,0 +1,394 @@
1
+ # Copyright:: Copyright (c) 2020 eGlobalTech, Inc., all rights reserved
2
+ #
3
+ # Licensed under the BSD-3 license (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License in the root of the project or at
6
+ #
7
+ # http://egt-labs.com/mu/LICENSE.html
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module MU
16
+
17
+ # MommaCat is in charge of managing metadata about resources we've created,
18
+ # as well as orchestrating amongst them and bootstrapping nodes outside of
19
+ # the normal synchronous deploy sequence invoked by *mu-deploy*.
20
+ class MommaCat
21
+
22
+ # Check a provided deploy key against our stored version. The instance has
23
+ # in theory accessed a secret via S3 and encrypted it with the deploy's
24
+ # public key. If it decrypts correctly, we assume this instance is indeed
25
+ # one of ours.
26
+ # @param ciphertext [String]: The text to decrypt.
27
+ # return [Boolean]: Whether the provided text was encrypted with the correct key
28
+ def authKey(ciphertext)
29
+ if @private_key.nil? or @deploy_secret.nil?
30
+ MU.log "Missing auth metadata, can't authorize node in authKey", MU::ERR
31
+ return false
32
+ end
33
+ my_key = OpenSSL::PKey::RSA.new(@private_key)
34
+
35
+ begin
36
+ if my_key.private_decrypt(ciphertext).force_encoding("UTF-8") == @deploy_secret.force_encoding("UTF-8")
37
+ MU.log "Matched ciphertext for #{MU.deploy_id}", MU::INFO
38
+ return true
39
+ else
40
+ MU.log "Mis-matched ciphertext for #{MU.deploy_id}", MU::ERR
41
+ return false
42
+ end
43
+ rescue OpenSSL::PKey::RSAError => e
44
+ MU.log "Error decrypting provided ciphertext using private key from #{deploy_dir}/private_key: #{e.message}", MU::ERR, details: ciphertext
45
+ return false
46
+ end
47
+ end
48
+
49
+ # Run {MU::Cloud::Server#postBoot} and {MU::Cloud::Server#groom} on a node.
50
+ # @param cloud_id [OpenStruct]: The cloud provider's identifier for this node.
51
+ # @param name [String]: The MU resource name of the node being created.
52
+ # @param mu_name [String]: The full #{MU::MommaCat.getResourceName} name of the server we're grooming, if it's been initialized already.
53
+ # @param type [String]: The type of resource that created this node (either *server* or *serverpool*).
54
+ def groomNode(cloud_id, name, type, mu_name: nil, reraise_fail: false, sync_wait: true)
55
+ if cloud_id.nil?
56
+ raise GroomError, "MU::MommaCat.groomNode requires a {MU::Cloud::Server} object"
57
+ end
58
+ if name.nil? or name.empty?
59
+ raise GroomError, "MU::MommaCat.groomNode requires a resource name"
60
+ end
61
+ if type.nil? or type.empty?
62
+ raise GroomError, "MU::MommaCat.groomNode requires a resource type"
63
+ end
64
+
65
+ if !MU::MommaCat.lock(cloud_id+"-mommagroom", true)
66
+ MU.log "Instance #{cloud_id} on #{MU.deploy_id} (#{type}: #{name}) is already being groomed, ignoring this extra request.", MU::NOTICE
67
+ MU::MommaCat.unlockAll
68
+ if !MU::MommaCat.locks.nil? and MU::MommaCat.locks.size > 0
69
+ puts "------------------------------"
70
+ puts "Open flock() locks:"
71
+ pp MU::MommaCat.locks
72
+ puts "------------------------------"
73
+ end
74
+ return
75
+ end
76
+ loadDeploy
77
+
78
+ # XXX this is to stop Net::SSH from killing our entire stack when it
79
+ # throws an exception. See ECAP-139 in JIRA. Far as we can tell, it's
80
+ # just not entirely thread safe.
81
+ Thread.handle_interrupt(Net::SSH::Disconnect => :never) {
82
+ begin
83
+ Thread.handle_interrupt(Net::SSH::Disconnect => :immediate) {
84
+ MU.log "(Probably harmless) Caught a Net::SSH::Disconnect in #{Thread.current.inspect}", MU::DEBUG, details: Thread.current.backtrace
85
+ }
86
+ ensure
87
+ end
88
+ }
89
+
90
+ if @original_config[type+"s"].nil?
91
+ raise GroomError, "I see no configured resources of type #{type} (bootstrap request for #{name} on #{@deploy_id})"
92
+ end
93
+ kitten = nil
94
+
95
+ kitten = findLitterMate(type: "server", name: name, mu_name: mu_name, cloud_id: cloud_id)
96
+ if !kitten.nil?
97
+ MU.log "Re-grooming #{mu_name}", details: kitten.deploydata
98
+ else
99
+ first_groom = true
100
+ @original_config[type+"s"].each { |svr|
101
+ if svr['name'] == name
102
+ svr["instance_id"] = cloud_id
103
+
104
+ # This will almost always be true in server pools, but lets be safe. Somewhat problematic because we are only
105
+ # looking at deploy_id, but we still know this is our DNS record and not a custom one.
106
+ if svr['dns_records'] && !svr['dns_records'].empty?
107
+ svr['dns_records'].each { |dnsrec|
108
+ if dnsrec.has_key?("name") && dnsrec['name'].start_with?(MU.deploy_id.downcase)
109
+ MU.log "DNS record for #{MU.deploy_id.downcase}, #{name} is probably wrong, deleting", MU::WARN, details: dnsrec
110
+ dnsrec.delete('name')
111
+ dnsrec.delete('target')
112
+ end
113
+ }
114
+ end
115
+
116
+ kitten = MU::Cloud::Server.new(mommacat: self, kitten_cfg: svr, cloud_id: cloud_id)
117
+ mu_name = kitten.mu_name if mu_name.nil?
118
+ MU.log "Grooming #{mu_name} for the first time", details: svr
119
+ break
120
+ end
121
+ }
122
+ end
123
+
124
+ begin
125
+ # This is a shared lock with MU::Cloud::AWS::Server.create, to keep from
126
+ # stomping on synchronous deploys that are still running. This
127
+ # means we're going to wait here if this instance is still being
128
+ # bootstrapped by "regular" means.
129
+ if !MU::MommaCat.lock(cloud_id+"-create", true)
130
+ MU.log "#{mu_name} is still in mid-creation, skipping", MU::NOTICE
131
+ MU::MommaCat.unlockAll
132
+ if !MU::MommaCat.locks.nil? and MU::MommaCat.locks.size > 0
133
+ puts "------------------------------"
134
+ puts "Open flock() locks:"
135
+ pp MU::MommaCat.locks
136
+ puts "------------------------------"
137
+ end
138
+ return
139
+ end
140
+ MU::MommaCat.unlock(cloud_id+"-create")
141
+
142
+ if !kitten.postBoot(cloud_id)
143
+ MU.log "#{mu_name} is already being groomed, skipping", MU::NOTICE
144
+ MU::MommaCat.unlockAll
145
+ if !MU::MommaCat.locks.nil? and MU::MommaCat.locks.size > 0
146
+ puts "------------------------------"
147
+ puts "Open flock() locks:"
148
+ pp MU::MommaCat.locks
149
+ puts "------------------------------"
150
+ end
151
+ return
152
+ end
153
+
154
+ # This is a shared lock with MU::Deploy.createResources, simulating the
155
+ # thread logic that tells MU::Cloud::AWS::Server.deploy to wait until
156
+ # its dependencies are ready. We don't, for example, want to start
157
+ # deploying if we rely on an RDS instance that isn't ready yet. We can
158
+ # release this immediately, once we successfully grab it.
159
+ MU::MommaCat.lock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies")
160
+ MU::MommaCat.unlock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies")
161
+
162
+ kitten.groom
163
+ rescue StandardError => e
164
+ MU::MommaCat.unlockAll
165
+ if e.class.name != "MU::Cloud::AWS::Server::BootstrapTempFail" and !File.exist?(deploy_dir+"/.cleanup."+cloud_id) and !File.exist?(deploy_dir+"/.cleanup")
166
+ MU.log "Grooming FAILED for #{kitten.mu_name} (#{e.inspect})", MU::ERR, details: e.backtrace
167
+ sendAdminSlack("Grooming FAILED for `#{kitten.mu_name}` with `#{e.message}` :crying_cat_face:", msg: e.backtrace.join("\n"))
168
+ sendAdminMail("Grooming FAILED for #{kitten.mu_name} on #{MU.appname} \"#{MU.handle}\" (#{MU.deploy_id})",
169
+ msg: e.inspect,
170
+ data: e.backtrace,
171
+ debug: true
172
+ )
173
+ raise e if reraise_fail
174
+ else
175
+ MU.log "Grooming of #{kitten.mu_name} interrupted by cleanup or planned reboot"
176
+ end
177
+ return
178
+ end
179
+
180
+ if !@deployment['servers'].nil? and !sync_wait
181
+ syncLitter(@deployment["servers"].keys, triggering_node: kitten)
182
+ end
183
+ MU::MommaCat.unlock(cloud_id+"-mommagroom")
184
+ if MU.myCloud == "AWS"
185
+ MU::Cloud::AWS.openFirewallForClients # XXX add the other clouds, or abstract
186
+ end
187
+ MU::MommaCat.getLitter(MU.deploy_id)
188
+ MU::Master.syncMonitoringConfig(false)
189
+ MU.log "Grooming complete for '#{name}' mu_name on \"#{MU.handle}\" (#{MU.deploy_id})"
190
+ FileUtils.touch(MU.dataDir+"/deployments/#{MU.deploy_id}/#{name}_done.txt")
191
+ MU::MommaCat.unlockAll
192
+ if first_groom
193
+ sendAdminSlack("Grooming complete for #{mu_name} :heart_eyes_cat:")
194
+ sendAdminMail("Grooming complete for '#{name}' (#{mu_name}) on deploy \"#{MU.handle}\" (#{MU.deploy_id})")
195
+ end
196
+ return
197
+ end
198
+
199
+ @cleanup_threads = []
200
+
201
+ # Iterate over all known deployments and look for instances that have been
202
+ # terminated, but not yet cleaned up, then clean them up.
203
+ def self.cleanTerminatedInstances(debug = false)
204
+ loglevel = debug ? MU::NOTICE : MU::DEBUG
205
+ MU::MommaCat.lock("clean-terminated-instances", false, true)
206
+ MU.log "Checking for harvested instances in need of cleanup", loglevel
207
+ parent_thread_id = Thread.current.object_id
208
+ purged = 0
209
+
210
+ MU::MommaCat.listDeploys.each { |deploy_id|
211
+ next if File.exist?(deploy_dir(deploy_id)+"/.cleanup")
212
+ MU.log "Checking for dead wood in #{deploy_id}", loglevel
213
+ need_reload = false
214
+ @cleanup_threads << Thread.new {
215
+ MU.dupGlobals(parent_thread_id)
216
+ deploy = MU::MommaCat.getLitter(deploy_id, set_context_to_me: true)
217
+ purged_this_deploy = 0
218
+ MU.log "#{deploy_id} has some kittens in it", loglevel, details: deploy.kittens.keys
219
+ if deploy.kittens.has_key?("servers")
220
+ MU.log "#{deploy_id} has some servers declared", loglevel, details: deploy.object_id
221
+ deploy.kittens["servers"].values.each { |nodeclasses|
222
+ nodeclasses.each_pair { |nodeclass, servers|
223
+ deletia = []
224
+ MU.log "Checking status of servers under '#{nodeclass}'", loglevel, details: servers.keys
225
+ servers.each_pair { |mu_name, server|
226
+ server.describe
227
+ if !server.cloud_id
228
+ MU.log "Checking for presence of #{mu_name}, but unable to fetch its cloud_id", MU::WARN, details: server
229
+ elsif !server.active?
230
+ next if File.exist?(deploy_dir(deploy_id)+"/.cleanup-"+server.cloud_id)
231
+ deletia << mu_name
232
+ need_reload = true
233
+ MU.log "Cleaning up metadata for #{server} (#{nodeclass}), formerly #{server.cloud_id}, which appears to have been terminated", MU::NOTICE
234
+ begin
235
+ server.destroy
236
+ deploy.sendAdminMail("Retired metadata for terminated node #{mu_name}")
237
+ deploy.sendAdminSlack("Retired metadata for terminated node `#{mu_name}`")
238
+ rescue StandardError => e
239
+ MU.log "Saw #{e.message} while retiring #{mu_name}", MU::ERR, details: e.backtrace
240
+ next
241
+ end
242
+ MU.log "Cleanup of metadata for #{server} (#{nodeclass}), formerly #{server.cloud_id} complete", MU::NOTICE
243
+ purged = purged + 1
244
+ purged_this_deploy = purged_this_deploy + 1
245
+ end
246
+ }
247
+ deletia.each { |mu_name|
248
+ servers.delete(mu_name)
249
+ }
250
+ if purged_this_deploy > 0
251
+ # XXX triggering_node needs to take more than one node name
252
+ deploy.syncLitter(servers.keys, triggering_node: deletia.first)
253
+ end
254
+ }
255
+ }
256
+ end
257
+ if need_reload
258
+ MU.log "Saving modified deploy #{deploy_id}", loglevel
259
+ deploy.save!
260
+ MU::MommaCat.getLitter(deploy_id)
261
+ end
262
+ MU.purgeGlobals
263
+ }
264
+ }
265
+ @cleanup_threads.each { |t|
266
+ t.join
267
+ }
268
+ MU.log "cleanTerminatedInstances threads complete", loglevel
269
+ MU::MommaCat.unlock("clean-terminated-instances", true)
270
+ @cleanup_threads = []
271
+
272
+ if purged > 0
273
+ if MU.myCloud == "AWS"
274
+ MU::Cloud::AWS.openFirewallForClients # XXX add the other clouds, or abstract
275
+ end
276
+ MU::Master.syncMonitoringConfig
277
+ GC.start
278
+ end
279
+ MU.log "cleanTerminatedInstances returning", loglevel
280
+ end
281
+
282
+ # Path to the log file used by the Momma Cat daemon
283
+ # @return [String]
284
+ def self.daemonLogFile
285
+ base = (Process.uid == 0 and !MU.localOnly) ? "/var" : MU.dataDir
286
+ "#{base}/log/mu-momma-cat.log"
287
+ end
288
+
289
+ # Path to the PID file used by the Momma Cat daemon
290
+ # @return [String]
291
+ def self.daemonPidFile
292
+ base = (Process.uid == 0 and !MU.localOnly) ? "/var" : MU.dataDir
293
+ "#{base}/run/mommacat.pid"
294
+ end
295
+
296
+ # Start the Momma Cat daemon and return the exit status of the command used
297
+ # @return [Integer]
298
+ def self.start
299
+ if MU.inGem? and MU.muCfg['disable_mommacat']
300
+ return
301
+ end
302
+ base = (Process.uid == 0 and !MU.localOnly) ? "/var" : MU.dataDir
303
+ [base, "#{base}/log", "#{base}/run"].each { |dir|
304
+ if !Dir.exist?(dir)
305
+ MU.log "Creating #{dir}"
306
+ Dir.mkdir(dir)
307
+ end
308
+ }
309
+ return 0 if status
310
+
311
+ MU.log "Starting Momma Cat on port #{MU.mommaCatPort}, logging to #{daemonLogFile}, PID file #{daemonPidFile}"
312
+ origdir = Dir.getwd
313
+ Dir.chdir(MU.myRoot+"/modules")
314
+
315
+ # XXX what's the safest way to find the 'bundle' executable in both gem and non-gem installs?
316
+ if MU.inGem?
317
+ cmd = %Q{thin --threaded --daemonize --port #{MU.mommaCatPort} --pid #{daemonPidFile} --log #{daemonLogFile} --ssl --ssl-key-file #{MU.muCfg['ssl']['key']} --ssl-cert-file #{MU.muCfg['ssl']['cert']} --ssl-disable-verify --tag mu-momma-cat -R mommacat.ru start}
318
+ else
319
+ cmd = %Q{bundle exec thin --threaded --daemonize --port #{MU.mommaCatPort} --pid #{daemonPidFile} --log #{daemonLogFile} --ssl --ssl-key-file #{MU.muCfg['ssl']['key']} --ssl-cert-file #{MU.muCfg['ssl']['cert']} --ssl-disable-verify --tag mu-momma-cat -R mommacat.ru start}
320
+ end
321
+
322
+ MU.log cmd, MU::NOTICE
323
+
324
+ retries = 0
325
+ begin
326
+ output = %x{#{cmd}}
327
+ sleep 1
328
+ retries += 1
329
+ if retries >= 10
330
+ MU.log "MommaCat failed to start (command was #{cmd}, working directory #{MU.myRoot}/modules)", MU::WARN, details: output
331
+ pp caller
332
+ return $?.exitstatus
333
+ end
334
+ end while !status
335
+
336
+ Dir.chdir(origdir)
337
+
338
+ if $?.exitstatus != 0
339
+ exit 1
340
+ end
341
+
342
+ return $?.exitstatus
343
+ end
344
+
345
+ # Return true if the Momma Cat daemon appears to be running
346
+ # @return [Boolean]
347
+ def self.status
348
+ if MU.inGem? and MU.muCfg['disable_mommacat']
349
+ return true
350
+ end
351
+ if File.exist?(daemonPidFile)
352
+ pid = File.read(daemonPidFile).chomp.to_i
353
+ begin
354
+ Process.getpgid(pid)
355
+ MU.log "Momma Cat running with pid #{pid.to_s}"
356
+ return true
357
+ rescue Errno::ESRCH
358
+ end
359
+ end
360
+ MU.log "Momma Cat daemon not running", MU::NOTICE, details: daemonPidFile
361
+ false
362
+ end
363
+
364
+ # Stop the Momma Cat daemon, if it's running
365
+ def self.stop
366
+ if File.exist?(daemonPidFile)
367
+ pid = File.read(daemonPidFile).chomp.to_i
368
+ MU.log "Stopping Momma Cat with pid #{pid.to_s}"
369
+ Process.kill("INT", pid)
370
+ killed = false
371
+ begin
372
+ Process.getpgid(pid)
373
+ sleep 1
374
+ rescue Errno::ESRCH
375
+ killed = true
376
+ end while killed
377
+ MU.log "Momma Cat with pid #{pid.to_s} stopped", MU::DEBUG, details: daemonPidFile
378
+
379
+ begin
380
+ File.unlink(daemonPidFile)
381
+ rescue Errno::ENOENT
382
+ end
383
+ end
384
+ end
385
+
386
+ # (Re)start the Momma Cat daemon and return the exit status of the start command
387
+ # @return [Integer]
388
+ def self.restart
389
+ stop
390
+ start
391
+ end
392
+
393
+ end #class
394
+ end #module
@@ -0,0 +1,366 @@
1
+ # Copyright:: Copyright (c) 2020 eGlobalTech, Inc., all rights reserved
2
+ #
3
+ # Licensed under the BSD-3 license (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License in the root of the project or at
6
+ #
7
+ # http://egt-labs.com/mu/LICENSE.html
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module MU
16
+
17
+ # MommaCat is in charge of managing metadata about resources we've created,
18
+ # as well as orchestrating amongst them and bootstrapping nodes outside of
19
+ # the normal synchronous deploy sequence invoked by *mu-deploy*.
20
+ class MommaCat
21
+
22
+ # Generate a three-character string which can be used to unique-ify the
23
+ # names of resources which might potentially collide, e.g. Windows local
24
+ # hostnames, Amazon Elastic Load Balancers, or server pool instances.
25
+ # @return [String]: A three-character string consisting of two alphnumeric
26
+ # characters (uppercase) and one number.
27
+ def self.genUniquenessString
28
+ begin
29
+ candidate = SecureRandom.base64(2).slice(0..1) + SecureRandom.random_number(9).to_s
30
+ candidate.upcase!
31
+ end while candidate.match(/[^A-Z0-9]/)
32
+ return candidate
33
+ end
34
+
35
+ @unique_map_semaphore = Mutex.new
36
+ @name_unique_str_map = {}
37
+ # Keep a map of the uniqueness strings we assign to various full names, in
38
+ # case we want to reuse them later.
39
+ # @return [Hash<String>]
40
+ def self.name_unique_str_map
41
+ @name_unique_str_map
42
+ end
43
+
44
+ # Keep a map of the uniqueness strings we assign to various full names, in
45
+ # case we want to reuse them later.
46
+ # @return [Mutex]
47
+ def self.unique_map_semaphore
48
+ @unique_map_semaphore
49
+ end
50
+
51
+ # Generate a name string for a resource, incorporate the MU identifier
52
+ # for this deployment. Will dynamically shorten the name to fit for
53
+ # restrictive uses (e.g. Windows local hostnames, Amazon Elastic Load
54
+ # Balancers).
55
+ # @param name [String]: The shorthand name of the resource, usually the value of the "name" field in an Mu resource declaration.
56
+ # @param max_length [Integer]: The maximum length of the resulting resource name.
57
+ # @param need_unique_string [Boolean]: Whether to forcibly append a random three-character string to the name to ensure it's unique. Note that this behavior will be automatically invoked if the name must be truncated.
58
+ # @param scrub_mu_isms [Boolean]: Don't bother with generating names specific to this deployment. Used to generate generic CloudFormation templates, amongst other purposes.
59
+ # @param disallowed_chars [Regexp]: A pattern of characters that are illegal for this resource name, such as +/[^a-zA-Z0-9-]/+
60
+ # @return [String]: A full name string for this resource
61
+ def getResourceName(name, max_length: 255, need_unique_string: false, use_unique_string: nil, reuse_unique_string: false, scrub_mu_isms: @original_config['scrub_mu_isms'], disallowed_chars: nil)
62
+ if name.nil?
63
+ raise MuError, "Got no argument to MU::MommaCat.getResourceName"
64
+ end
65
+ if @appname.nil? or @environment.nil? or @timestamp.nil? or @seed.nil?
66
+ MU.log "getResourceName: Missing global deploy variables in thread #{Thread.current.object_id}, using bare name '#{name}' (appname: #{@appname}, environment: #{@environment}, timestamp: #{@timestamp}, seed: #{@seed}, deploy_id: #{@deploy_id}", MU::WARN, details: caller
67
+ return name
68
+ end
69
+ need_unique_string = false if scrub_mu_isms
70
+
71
+ muname = nil
72
+ if need_unique_string
73
+ reserved = 4
74
+ else
75
+ reserved = 0
76
+ end
77
+
78
+ # First, pare down the base name string until it will fit
79
+ basename = @appname.upcase + "-" + @environment.upcase + "-" + @timestamp + "-" + @seed.upcase + "-" + name.upcase
80
+ if scrub_mu_isms
81
+ basename = @appname.upcase + "-" + @environment.upcase + name.upcase
82
+ end
83
+
84
+ subchar = if disallowed_chars
85
+ if "-".match(disallowed_chars)
86
+ if !"_".match(disallowed_chars)
87
+ "_"
88
+ else
89
+ ""
90
+ end
91
+ else
92
+ "-"
93
+ end
94
+ end
95
+
96
+ if disallowed_chars
97
+ basename.gsub!(disallowed_chars, subchar) if disallowed_chars
98
+ end
99
+ attempts = 0
100
+ begin
101
+ if (basename.length + reserved) > max_length
102
+ MU.log "Stripping name down from #{basename}[#{basename.length.to_s}] (reserved: #{reserved.to_s}, max_length: #{max_length.to_s})", MU::DEBUG
103
+ if basename == @appname.upcase + "-" + @seed.upcase + "-" + name.upcase
104
+ # If we've run out of stuff to strip, truncate what's left and
105
+ # just leave room for the deploy seed and uniqueness string. This
106
+ # is the bare minimum, and probably what you'll see for most Windows
107
+ # hostnames.
108
+ basename = name.upcase + "-" + @appname.upcase
109
+ basename.slice!((max_length-(reserved+3))..basename.length)
110
+ basename.sub!(/-$/, "")
111
+ basename = basename + "-" + @seed.upcase
112
+ basename.gsub!(disallowed_chars, subchar) if disallowed_chars
113
+ else
114
+ # If we have to strip anything, assume we've lost uniqueness and
115
+ # will have to compensate with #genUniquenessString.
116
+ need_unique_string = true
117
+ reserved = 4
118
+ basename.sub!(/-[^-]+-#{@seed.upcase}-#{Regexp.escape(name.upcase)}$/, "")
119
+ basename = basename + "-" + @seed.upcase + "-" + name.upcase
120
+ basename.gsub!(disallowed_chars, subchar) if disallowed_chars
121
+ end
122
+ end
123
+ attempts += 1
124
+ raise MuError, "Failed to generate a reasonable name getResourceName(#{name}, max_length: #{max_length.to_s}, need_unique_string: #{need_unique_string.to_s}, use_unique_string: #{use_unique_string.to_s}, reuse_unique_string: #{reuse_unique_string.to_s}, scrub_mu_isms: #{scrub_mu_isms.to_s}, disallowed_chars: #{disallowed_chars})" if attempts > 10
125
+ end while (basename.length + reserved) > max_length
126
+
127
+ # Finally, apply our short random differentiator, if it's needed.
128
+ if need_unique_string
129
+ # Preferentially use a requested one, if it's not already in use.
130
+ if !use_unique_string.nil?
131
+ muname = basename + "-" + use_unique_string
132
+ if !allocateUniqueResourceName(muname) and !reuse_unique_string
133
+ MU.log "Requested to use #{use_unique_string} as differentiator when naming #{name}, but the name #{muname} is unavailable.", MU::WARN
134
+ muname = nil
135
+ end
136
+ end
137
+ if !muname
138
+ begin
139
+ unique_string = MU::MommaCat.genUniquenessString
140
+ muname = basename + "-" + unique_string
141
+ end while !allocateUniqueResourceName(muname)
142
+ MU::MommaCat.unique_map_semaphore.synchronize {
143
+ MU::MommaCat.name_unique_str_map[muname] = unique_string
144
+ }
145
+ end
146
+ else
147
+ muname = basename
148
+ end
149
+ muname.gsub!(disallowed_chars, subchar) if disallowed_chars
150
+
151
+ return muname
152
+ end
153
+
154
+ # List the name/value pairs for our mandatory standard set of resource tags, which
155
+ # should be applied to all taggable cloud provider resources.
156
+ # @return [Hash<String,String>]
157
+ def self.listStandardTags
158
+ return {} if !MU.deploy_id
159
+ {
160
+ "MU-ID" => MU.deploy_id,
161
+ "MU-APP" => MU.appname,
162
+ "MU-ENV" => MU.environment,
163
+ "MU-MASTER-IP" => MU.mu_public_ip
164
+ }
165
+ end
166
+ # List the name/value pairs for our mandatory standard set of resource tags
167
+ # for this deploy.
168
+ # @return [Hash<String,String>]
169
+ def listStandardTags
170
+ {
171
+ "MU-ID" => @deploy_id,
172
+ "MU-APP" => @appname,
173
+ "MU-ENV" => @environment,
174
+ "MU-MASTER-IP" => MU.mu_public_ip
175
+ }
176
+ end
177
+
178
+ # List the name/value pairs of our optional set of resource tags which
179
+ # should be applied to all taggable cloud provider resources.
180
+ # @return [Hash<String,String>]
181
+ def self.listOptionalTags
182
+ return {
183
+ "MU-HANDLE" => MU.handle,
184
+ "MU-MASTER-NAME" => Socket.gethostname,
185
+ "MU-OWNER" => MU.mu_user
186
+ }
187
+ end
188
+
189
+ # Make sure the given node has proper DNS entries, /etc/hosts entries,
190
+ # SSH config entries, etc.
191
+ # @param server [MU::Cloud::Server]: The {MU::Cloud::Server} we'll be setting up.
192
+ # @param sync_wait [Boolean]: Whether to wait for DNS to fully synchronize before returning.
193
+ def self.nameKitten(server, sync_wait: false)
194
+ node, config, _deploydata = server.describe
195
+
196
+ mu_zone = nil
197
+ # XXX GCP!
198
+ if MU::Cloud::AWS.hosted? and !MU::Cloud::AWS.isGovCloud?
199
+ zones = MU::Cloud::DNSZone.find(cloud_id: "platform-mu")
200
+ mu_zone = zones.values.first if !zones.nil?
201
+ end
202
+ if !mu_zone.nil?
203
+ MU::Cloud::DNSZone.genericMuDNSEntry(name: node, target: server.canonicalIP, cloudclass: MU::Cloud::Server, sync_wait: sync_wait)
204
+ else
205
+ MU::Master.addInstanceToEtcHosts(server.canonicalIP, node)
206
+ end
207
+
208
+ ## TO DO: Do DNS registration of "real" records as the last stage after the groomer completes
209
+ if config && config['dns_records'] && !config['dns_records'].empty?
210
+ dnscfg = config['dns_records'].dup
211
+ dnscfg.each { |dnsrec|
212
+ if !dnsrec.has_key?('name')
213
+ dnsrec['name'] = node.downcase
214
+ dnsrec['name'] = "#{dnsrec['name']}.#{MU.environment.downcase}" if dnsrec["append_environment_name"] && !dnsrec['name'].match(/\.#{MU.environment.downcase}$/)
215
+ end
216
+
217
+ if !dnsrec.has_key?("target")
218
+ # Default to register public endpoint
219
+ public = true
220
+
221
+ if dnsrec.has_key?("target_type")
222
+ # See if we have a preference for pubic/private endpoint
223
+ public = dnsrec["target_type"] == "private" ? false : true
224
+ end
225
+
226
+ dnsrec["target"] =
227
+ if dnsrec["type"] == "CNAME"
228
+ if public
229
+ # Make sure we have a public canonical name to register. Use the private one if we don't
230
+ server.cloud_desc.public_dns_name.empty? ? server.cloud_desc.private_dns_name : server.cloud_desc.public_dns_name
231
+ else
232
+ # If we specifically requested to register the private canonical name lets use that
233
+ server.cloud_desc.private_dns_name
234
+ end
235
+ elsif dnsrec["type"] == "A"
236
+ if public
237
+ # Make sure we have a public IP address to register. Use the private one if we don't
238
+ server.cloud_desc.public_ip_address ? server.cloud_desc.public_ip_address : server.cloud_desc.private_ip_address
239
+ else
240
+ # If we specifically requested to register the private IP lets use that
241
+ server.cloud_desc.private_ip_address
242
+ end
243
+ end
244
+ end
245
+ }
246
+ if !MU::Cloud::AWS.isGovCloud?
247
+ MU::Cloud::DNSZone.createRecordsFromConfig(dnscfg)
248
+ end
249
+ end
250
+
251
+ MU::Master.removeHostFromSSHConfig(node)
252
+ if server and server.canonicalIP
253
+ MU::Master.removeIPFromSSHKnownHosts(server.canonicalIP)
254
+ end
255
+ # XXX add names paramater with useful stuff
256
+ MU::Master.addHostToSSHConfig(
257
+ server,
258
+ ssh_owner: server.deploy.mu_user,
259
+ ssh_dir: Etc.getpwnam(server.deploy.mu_user).dir+"/.ssh"
260
+ )
261
+ end
262
+
263
+ # Manufactures a human-readable deployment name from the random
264
+ # two-character seed in MU-ID. Cat-themed when possible.
265
+ # @param seed [String]: A two-character seed from which we'll generate a name.
266
+ # @return [String]: Two words
267
+ def self.generateHandle(seed)
268
+ word_one=word_two=nil
269
+
270
+ # Unless we've got two letters that don't have corresponding cat-themed
271
+ # words, we'll insist that our generated handle have at least one cat
272
+ # element to it.
273
+ require_cat_words = true
274
+ if @catwords.select { |word| word.match(/^#{seed[0]}/i) }.size == 0 and
275
+ @catwords.select { |word| word.match(/^#{seed[1]}/i) }.size == 0
276
+ require_cat_words = false
277
+ MU.log "Got an annoying pair of letters #{seed}, not forcing cat-theming", MU::DEBUG
278
+ end
279
+ allnouns = @catnouns + @jaegernouns
280
+ alladjs = @catadjs + @jaegeradjs
281
+
282
+ tries = 0
283
+ begin
284
+ # Try to avoid picking something "nouny" for the first word
285
+ source = @catadjs + @catmixed + @jaegeradjs + @jaegermixed
286
+ first_ltr = source.select { |word| word.match(/^#{seed[0]}/i) }
287
+ if !first_ltr or first_ltr.size == 0
288
+ first_ltr = @words.select { |word| word.match(/^#{seed[0]}/i) }
289
+ end
290
+ word_one = first_ltr.shuffle.first
291
+
292
+ # If we got a paired set that happen to match our letters, go with it
293
+ if !word_one.nil? and word_one.match(/-#{seed[1]}/i)
294
+ word_one, word_two = word_one.split(/-/)
295
+ else
296
+ source = @words
297
+ if @catwords.include?(word_one)
298
+ source = @jaegerwords
299
+ elsif require_cat_words
300
+ source = @catwords
301
+ end
302
+ second_ltr = source.select { |word| word.match(/^#{seed[1]}/i) and !word.match(/-/i) }
303
+ word_two = second_ltr.shuffle.first
304
+ end
305
+ tries = tries + 1
306
+ end while tries < 50 and (word_one.nil? or word_two.nil? or word_one.match(/-/) or word_one == word_two or (allnouns.include?(word_one) and allnouns.include?(word_two)) or (alladjs.include?(word_one) and alladjs.include?(word_two)) or (require_cat_words and !@catwords.include?(word_one) and !@catwords.include?(word_two) and !@catwords.include?(word_one+"-"+word_two)))
307
+
308
+ if tries >= 50 and (word_one.nil? or word_two.nil?)
309
+ MU.log "I failed to generated a valid handle from #{seed}, faking it", MU::ERR
310
+ return "#{seed[0].capitalize} #{seed[1].capitalize}"
311
+ end
312
+
313
+ return "#{word_one.capitalize} #{word_two.capitalize}"
314
+ end
315
+
316
+ private
317
+
318
+ # Check to see whether a given resource name is unique across all
319
+ # deployments on this Mu server. We only enforce this for certain classes
320
+ # of names. If the name in question is available, add it to our cache of
321
+ # said names. See #{MU::MommaCat.getResourceName}
322
+ # @param name [String]: The name to attempt to allocate.
323
+ # @return [Boolean]: True if allocation was successful.
324
+ def allocateUniqueResourceName(name)
325
+ raise MuError, "Cannot call allocateUniqueResourceName without an active deployment" if @deploy_id.nil?
326
+ path = File.expand_path(MU.dataDir+"/deployments")
327
+ File.open(path+"/unique_ids", File::CREAT|File::RDWR, 0600) { |f|
328
+ existing = []
329
+ f.flock(File::LOCK_EX)
330
+ f.readlines.each { |line|
331
+ existing << line.chomp
332
+ }
333
+ begin
334
+ existing.each { |used|
335
+ if used.match(/^#{name}:/)
336
+ if !used.match(/^#{name}:#{@deploy_id}$/)
337
+ MU.log "#{name} is already reserved by another resource on this Mu server.", MU::WARN, details: caller
338
+ return false
339
+ else
340
+ return true
341
+ end
342
+ end
343
+ }
344
+ f.puts name+":"+@deploy_id
345
+ return true
346
+ ensure
347
+ f.flock(File::LOCK_UN)
348
+ end
349
+ }
350
+ end
351
+
352
+ # 2019-06-03 adding things from https://aiweirdness.com/post/185339301987/once-again-a-neural-net-tries-to-name-cats
353
+ @catadjs = %w{fuzzy ginger lilac chocolate xanthic wiggly itty chonky norty slonky floofy heckin bebby}
354
+ @catnouns = %w{bastet biscuits bobcat catnip cheetah chonk dot felix hamb hambina jaguar kitty leopard lion lynx maru mittens moggy neko nip ocelot panther patches paws phoebe purr queen roar saber sekhmet skogkatt socks sphinx spot tail tiger tom whiskers wildcat yowl floof beans ailurophile dander dewclaw grimalkin kibble quick tuft misty simba slonk mew quat eek ziggy whiskeridoo cromch monch screm}
355
+ @catmixed = %w{abyssinian angora bengal birman bobtail bombay burmese calico chartreux cheshire cornish-rex curl devon egyptian-mau feline furever fumbs havana himilayan japanese-bobtail javanese khao-manee maine-coon manx marmalade mau munchkin norwegian pallas persian peterbald polydactyl ragdoll russian-blue savannah scottish-fold serengeti shorthair siamese siberian singapura snowshoe stray tabby tonkinese tortoiseshell turkish-van tuxedo uncia caterwaul lilac-point chocolate-point mackerel maltese knead whitenose vorpal chewie-bean chicken-whiskey fish-especially thelonious-monsieur tom-glitter serendipitous-kill sparky-buttons nip-nops murder-mittens bite}
356
+ @catwords = @catadjs + @catnouns + @catmixed
357
+
358
+ @jaegeradjs = %w{azure fearless lucky olive vivid electric grey yarely violet ivory jade cinnamon crimson tacit umber mammoth ultra iron zodiac}
359
+ @jaegernouns = %w{horizon hulk ultimatum yardarm watchman whilrwind wright rhythm ocean enigma eruption typhoon jaeger brawler blaze vandal excalibur paladin juliet kaleidoscope romeo}
360
+ @jaegermixed = %w{alpha ajax amber avenger brave bravo charlie chocolate chrome corinthian dancer danger dash delta duet echo edge elite eureka foxtrot guardian gold hyperion illusion imperative india intercept kilo lancer night nova november oscar omega pacer quickstrike rogue ronin striker tango titan valor victor vulcan warder xenomorph xenon xray xylem yankee yell yukon zeal zero zoner zodiac}
361
+ @jaegerwords = @jaegeradjs + @jaegernouns + @jaegermixed
362
+
363
+ @words = @catwords + @jaegerwords
364
+
365
+ end #class
366
+ end #module