cloud-mu 3.1.5 → 3.3.2

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +5 -1
  3. data/ansible/roles/mu-windows/files/LaunchConfig.json +9 -0
  4. data/ansible/roles/mu-windows/files/config.xml +76 -0
  5. data/ansible/roles/mu-windows/tasks/main.yml +16 -0
  6. data/bin/mu-adopt +16 -12
  7. data/bin/mu-azure-tests +57 -0
  8. data/bin/mu-cleanup +2 -4
  9. data/bin/mu-configure +52 -0
  10. data/bin/mu-deploy +3 -3
  11. data/bin/mu-findstray-tests +25 -0
  12. data/bin/mu-gen-docs +2 -4
  13. data/bin/mu-load-config.rb +2 -1
  14. data/bin/mu-node-manage +15 -16
  15. data/bin/mu-run-tests +37 -12
  16. data/cloud-mu.gemspec +3 -3
  17. data/cookbooks/mu-activedirectory/resources/domain.rb +4 -4
  18. data/cookbooks/mu-activedirectory/resources/domain_controller.rb +4 -4
  19. data/cookbooks/mu-tools/libraries/helper.rb +1 -1
  20. data/cookbooks/mu-tools/recipes/apply_security.rb +14 -14
  21. data/cookbooks/mu-tools/recipes/aws_api.rb +9 -0
  22. data/cookbooks/mu-tools/recipes/eks.rb +2 -2
  23. data/cookbooks/mu-tools/recipes/windows-client.rb +25 -22
  24. data/extras/clean-stock-amis +25 -19
  25. data/extras/generate-stock-images +1 -0
  26. data/extras/image-generators/AWS/win2k12.yaml +2 -0
  27. data/extras/image-generators/AWS/win2k16.yaml +2 -0
  28. data/extras/image-generators/AWS/win2k19.yaml +2 -0
  29. data/modules/mommacat.ru +1 -1
  30. data/modules/mu.rb +86 -98
  31. data/modules/mu/adoption.rb +373 -58
  32. data/modules/mu/cleanup.rb +214 -303
  33. data/modules/mu/cloud.rb +128 -1733
  34. data/modules/mu/cloud/database.rb +49 -0
  35. data/modules/mu/cloud/dnszone.rb +44 -0
  36. data/modules/mu/cloud/machine_images.rb +212 -0
  37. data/modules/mu/cloud/providers.rb +81 -0
  38. data/modules/mu/cloud/resource_base.rb +929 -0
  39. data/modules/mu/cloud/server.rb +40 -0
  40. data/modules/mu/cloud/server_pool.rb +1 -0
  41. data/modules/mu/cloud/ssh_sessions.rb +228 -0
  42. data/modules/mu/cloud/winrm_sessions.rb +237 -0
  43. data/modules/mu/cloud/wrappers.rb +169 -0
  44. data/modules/mu/config.rb +123 -81
  45. data/modules/mu/config/alarm.rb +2 -6
  46. data/modules/mu/config/bucket.rb +32 -3
  47. data/modules/mu/config/cache_cluster.rb +2 -2
  48. data/modules/mu/config/cdn.rb +100 -0
  49. data/modules/mu/config/collection.rb +1 -1
  50. data/modules/mu/config/container_cluster.rb +7 -2
  51. data/modules/mu/config/database.rb +84 -105
  52. data/modules/mu/config/database.yml +1 -2
  53. data/modules/mu/config/dnszone.rb +5 -4
  54. data/modules/mu/config/doc_helpers.rb +5 -6
  55. data/modules/mu/config/endpoint.rb +2 -1
  56. data/modules/mu/config/firewall_rule.rb +3 -19
  57. data/modules/mu/config/folder.rb +1 -1
  58. data/modules/mu/config/function.rb +17 -8
  59. data/modules/mu/config/group.rb +1 -1
  60. data/modules/mu/config/habitat.rb +1 -1
  61. data/modules/mu/config/job.rb +89 -0
  62. data/modules/mu/config/loadbalancer.rb +57 -11
  63. data/modules/mu/config/log.rb +1 -1
  64. data/modules/mu/config/msg_queue.rb +1 -1
  65. data/modules/mu/config/nosqldb.rb +1 -1
  66. data/modules/mu/config/notifier.rb +8 -19
  67. data/modules/mu/config/ref.rb +92 -14
  68. data/modules/mu/config/role.rb +1 -1
  69. data/modules/mu/config/schema_helpers.rb +38 -37
  70. data/modules/mu/config/search_domain.rb +1 -1
  71. data/modules/mu/config/server.rb +12 -13
  72. data/modules/mu/config/server_pool.rb +3 -7
  73. data/modules/mu/config/storage_pool.rb +1 -1
  74. data/modules/mu/config/tail.rb +11 -0
  75. data/modules/mu/config/user.rb +1 -1
  76. data/modules/mu/config/vpc.rb +27 -23
  77. data/modules/mu/config/vpc.yml +0 -1
  78. data/modules/mu/defaults/AWS.yaml +90 -90
  79. data/modules/mu/defaults/Azure.yaml +1 -0
  80. data/modules/mu/defaults/Google.yaml +1 -0
  81. data/modules/mu/deploy.rb +34 -20
  82. data/modules/mu/groomer.rb +16 -1
  83. data/modules/mu/groomers/ansible.rb +69 -4
  84. data/modules/mu/groomers/chef.rb +51 -4
  85. data/modules/mu/logger.rb +120 -144
  86. data/modules/mu/master.rb +97 -4
  87. data/modules/mu/mommacat.rb +160 -874
  88. data/modules/mu/mommacat/daemon.rb +23 -14
  89. data/modules/mu/mommacat/naming.rb +110 -3
  90. data/modules/mu/mommacat/search.rb +497 -0
  91. data/modules/mu/mommacat/storage.rb +252 -194
  92. data/modules/mu/{clouds → providers}/README.md +1 -1
  93. data/modules/mu/{clouds → providers}/aws.rb +258 -57
  94. data/modules/mu/{clouds → providers}/aws/alarm.rb +3 -3
  95. data/modules/mu/{clouds → providers}/aws/bucket.rb +275 -41
  96. data/modules/mu/{clouds → providers}/aws/cache_cluster.rb +14 -50
  97. data/modules/mu/providers/aws/cdn.rb +782 -0
  98. data/modules/mu/{clouds → providers}/aws/collection.rb +5 -5
  99. data/modules/mu/{clouds → providers}/aws/container_cluster.rb +95 -84
  100. data/modules/mu/providers/aws/database.rb +1744 -0
  101. data/modules/mu/{clouds → providers}/aws/dnszone.rb +26 -12
  102. data/modules/mu/providers/aws/endpoint.rb +1072 -0
  103. data/modules/mu/{clouds → providers}/aws/firewall_rule.rb +39 -32
  104. data/modules/mu/{clouds → providers}/aws/folder.rb +1 -1
  105. data/modules/mu/{clouds → providers}/aws/function.rb +289 -134
  106. data/modules/mu/{clouds → providers}/aws/group.rb +18 -20
  107. data/modules/mu/{clouds → providers}/aws/habitat.rb +3 -3
  108. data/modules/mu/providers/aws/job.rb +466 -0
  109. data/modules/mu/{clouds → providers}/aws/loadbalancer.rb +77 -47
  110. data/modules/mu/{clouds → providers}/aws/log.rb +5 -5
  111. data/modules/mu/{clouds → providers}/aws/msg_queue.rb +14 -11
  112. data/modules/mu/{clouds → providers}/aws/nosqldb.rb +96 -5
  113. data/modules/mu/{clouds → providers}/aws/notifier.rb +135 -63
  114. data/modules/mu/{clouds → providers}/aws/role.rb +76 -48
  115. data/modules/mu/{clouds → providers}/aws/search_domain.rb +172 -41
  116. data/modules/mu/{clouds → providers}/aws/server.rb +66 -98
  117. data/modules/mu/{clouds → providers}/aws/server_pool.rb +42 -60
  118. data/modules/mu/{clouds → providers}/aws/storage_pool.rb +21 -38
  119. data/modules/mu/{clouds → providers}/aws/user.rb +12 -16
  120. data/modules/mu/{clouds → providers}/aws/userdata/README.md +0 -0
  121. data/modules/mu/{clouds → providers}/aws/userdata/linux.erb +5 -4
  122. data/modules/mu/{clouds → providers}/aws/userdata/windows.erb +0 -0
  123. data/modules/mu/{clouds → providers}/aws/vpc.rb +143 -74
  124. data/modules/mu/{clouds → providers}/aws/vpc_subnet.rb +0 -0
  125. data/modules/mu/{clouds → providers}/azure.rb +13 -0
  126. data/modules/mu/{clouds → providers}/azure/container_cluster.rb +1 -5
  127. data/modules/mu/{clouds → providers}/azure/firewall_rule.rb +8 -1
  128. data/modules/mu/{clouds → providers}/azure/habitat.rb +0 -0
  129. data/modules/mu/{clouds → providers}/azure/loadbalancer.rb +0 -0
  130. data/modules/mu/{clouds → providers}/azure/role.rb +0 -0
  131. data/modules/mu/{clouds → providers}/azure/server.rb +32 -24
  132. data/modules/mu/{clouds → providers}/azure/user.rb +1 -1
  133. data/modules/mu/{clouds → providers}/azure/userdata/README.md +0 -0
  134. data/modules/mu/{clouds → providers}/azure/userdata/linux.erb +0 -0
  135. data/modules/mu/{clouds → providers}/azure/userdata/windows.erb +0 -0
  136. data/modules/mu/{clouds → providers}/azure/vpc.rb +4 -6
  137. data/modules/mu/{clouds → providers}/cloudformation.rb +10 -0
  138. data/modules/mu/{clouds → providers}/cloudformation/alarm.rb +3 -3
  139. data/modules/mu/{clouds → providers}/cloudformation/cache_cluster.rb +3 -3
  140. data/modules/mu/{clouds → providers}/cloudformation/collection.rb +3 -3
  141. data/modules/mu/{clouds → providers}/cloudformation/database.rb +6 -17
  142. data/modules/mu/{clouds → providers}/cloudformation/dnszone.rb +3 -3
  143. data/modules/mu/{clouds → providers}/cloudformation/firewall_rule.rb +3 -3
  144. data/modules/mu/{clouds → providers}/cloudformation/loadbalancer.rb +3 -3
  145. data/modules/mu/{clouds → providers}/cloudformation/log.rb +3 -3
  146. data/modules/mu/{clouds → providers}/cloudformation/server.rb +7 -7
  147. data/modules/mu/{clouds → providers}/cloudformation/server_pool.rb +5 -5
  148. data/modules/mu/{clouds → providers}/cloudformation/vpc.rb +3 -3
  149. data/modules/mu/{clouds → providers}/docker.rb +0 -0
  150. data/modules/mu/{clouds → providers}/google.rb +29 -6
  151. data/modules/mu/{clouds → providers}/google/bucket.rb +4 -4
  152. data/modules/mu/{clouds → providers}/google/container_cluster.rb +38 -20
  153. data/modules/mu/{clouds → providers}/google/database.rb +5 -12
  154. data/modules/mu/{clouds → providers}/google/firewall_rule.rb +5 -5
  155. data/modules/mu/{clouds → providers}/google/folder.rb +5 -9
  156. data/modules/mu/{clouds → providers}/google/function.rb +6 -6
  157. data/modules/mu/{clouds → providers}/google/group.rb +9 -17
  158. data/modules/mu/{clouds → providers}/google/habitat.rb +4 -8
  159. data/modules/mu/{clouds → providers}/google/loadbalancer.rb +5 -5
  160. data/modules/mu/{clouds → providers}/google/role.rb +50 -31
  161. data/modules/mu/{clouds → providers}/google/server.rb +41 -24
  162. data/modules/mu/{clouds → providers}/google/server_pool.rb +14 -14
  163. data/modules/mu/{clouds → providers}/google/user.rb +34 -24
  164. data/modules/mu/{clouds → providers}/google/userdata/README.md +0 -0
  165. data/modules/mu/{clouds → providers}/google/userdata/linux.erb +0 -0
  166. data/modules/mu/{clouds → providers}/google/userdata/windows.erb +0 -0
  167. data/modules/mu/{clouds → providers}/google/vpc.rb +45 -14
  168. data/modules/tests/aws-jobs-functions.yaml +46 -0
  169. data/modules/tests/centos6.yaml +15 -0
  170. data/modules/tests/centos7.yaml +15 -0
  171. data/modules/tests/centos8.yaml +12 -0
  172. data/modules/tests/ecs.yaml +2 -2
  173. data/modules/tests/eks.yaml +1 -1
  174. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  175. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  176. data/modules/tests/microservice_app.yaml +288 -0
  177. data/modules/tests/rds.yaml +108 -0
  178. data/modules/tests/regrooms/rds.yaml +123 -0
  179. data/modules/tests/server-with-scrub-muisms.yaml +1 -1
  180. data/modules/tests/super_complex_bok.yml +2 -2
  181. data/modules/tests/super_simple_bok.yml +3 -5
  182. data/spec/mu/clouds/azure_spec.rb +2 -2
  183. metadata +122 -92
  184. data/modules/mu/clouds/aws/database.rb +0 -1974
  185. data/modules/mu/clouds/aws/endpoint.rb +0 -596
@@ -33,7 +33,7 @@ module MU
33
33
  my_key = OpenSSL::PKey::RSA.new(@private_key)
34
34
 
35
35
  begin
36
- if my_key.private_decrypt(ciphertext).force_encoding("UTF-8") == @deploy_secret.force_encoding("UTF-8")
36
+ if my_key.private_decrypt(ciphertext).force_encoding("UTF-8").chomp == @deploy_secret.force_encoding("UTF-8").chomp
37
37
  MU.log "Matched ciphertext for #{MU.deploy_id}", MU::INFO
38
38
  return true
39
39
  else
@@ -165,11 +165,11 @@ module MU
165
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
166
  MU.log "Grooming FAILED for #{kitten.mu_name} (#{e.inspect})", MU::ERR, details: e.backtrace
167
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
- )
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
173
  raise e if reraise_fail
174
174
  else
175
175
  MU.log "Grooming of #{kitten.mu_name} interrupted by cleanup or planned reboot"
@@ -288,8 +288,8 @@ module MU
288
288
 
289
289
  # Path to the PID file used by the Momma Cat daemon
290
290
  # @return [String]
291
- def self.daemonPidFile
292
- base = (Process.uid == 0 and !MU.localOnly) ? "/var" : MU.dataDir
291
+ def self.daemonPidFile(root = false)
292
+ base = ((Process.uid == 0 or root) and !MU.localOnly) ? "/var" : MU.dataDir
293
293
  "#{base}/run/mommacat.pid"
294
294
  end
295
295
 
@@ -306,8 +306,14 @@ module MU
306
306
  Dir.mkdir(dir)
307
307
  end
308
308
  }
309
- return 0 if status
309
+ if (Process.uid != 0 and
310
+ (!$MU_CFG['overridden_keys'] or !$MU_CFG['overridden_keys'].include?("mommacat_port")) and
311
+ status(true)
312
+ ) or status
313
+ return 0
314
+ end
310
315
 
316
+ File.unlink(daemonPidFile) if File.exists?(daemonPidFile)
311
317
  MU.log "Starting Momma Cat on port #{MU.mommaCatPort}, logging to #{daemonLogFile}, PID file #{daemonPidFile}"
312
318
  origdir = Dir.getwd
313
319
  Dir.chdir(MU.myRoot+"/modules")
@@ -342,22 +348,25 @@ module MU
342
348
  return $?.exitstatus
343
349
  end
344
350
 
351
+ @@notified_on_pid = {}
352
+
345
353
  # Return true if the Momma Cat daemon appears to be running
346
354
  # @return [Boolean]
347
- def self.status
355
+ def self.status(root = false)
348
356
  if MU.inGem? and MU.muCfg['disable_mommacat']
349
357
  return true
350
358
  end
351
- if File.exist?(daemonPidFile)
352
- pid = File.read(daemonPidFile).chomp.to_i
359
+ if File.exist?(daemonPidFile(root))
360
+ pid = File.read(daemonPidFile(root)).chomp.to_i
353
361
  begin
354
362
  Process.getpgid(pid)
355
- MU.log "Momma Cat running with pid #{pid.to_s}"
363
+ MU.log "Momma Cat running with pid #{pid.to_s}", (@@notified_on_pid[pid] ? MU::DEBUG : MU::INFO) # shush
364
+ @@notified_on_pid[pid] = true
356
365
  return true
357
366
  rescue Errno::ESRCH
358
367
  end
359
368
  end
360
- MU.log "Momma Cat daemon not running", MU::NOTICE, details: daemonPidFile
369
+ MU.log "Momma Cat daemon not running", MU::NOTICE, details: daemonPidFile(root)
361
370
  false
362
371
  end
363
372
 
@@ -19,6 +19,112 @@ module MU
19
19
  # the normal synchronous deploy sequence invoked by *mu-deploy*.
20
20
  class MommaCat
21
21
 
22
+ # Lookup table to translate the word "habitat" back to its
23
+ # provider-specific jargon
24
+ HABITAT_SYNONYMS = {
25
+ "AWS" => "account",
26
+ "CloudFormation" => "account",
27
+ "Google" => "project",
28
+ "Azure" => "subscription",
29
+ "VMWare" => "sddc"
30
+ }
31
+
32
+ # Given a cloud provider's native descriptor for a resource, make some
33
+ # reasonable guesses about what the thing's name should be.
34
+ def self.guessName(desc, resourceclass, cloud_id: nil, tag_value: nil)
35
+ if desc.respond_to?(:tags) and
36
+ desc.tags.is_a?(Array) and
37
+ desc.tags.first.respond_to?(:key) and
38
+ desc.tags.map { |t| t.key }.include?("Name")
39
+ desc.tags.select { |t| t.key == "Name" }.first.value
40
+ else
41
+ try = nil
42
+ # Various GCP fields
43
+ [:display_name, :name, (resourceclass.cfg_name+"_name").to_sym].each { |field|
44
+ if desc.respond_to?(field) and desc.send(field).is_a?(String)
45
+ try = desc.send(field)
46
+ break
47
+ end
48
+
49
+ }
50
+ try ||= if !tag_value.nil?
51
+ tag_value
52
+ else
53
+ cloud_id
54
+ end
55
+ try
56
+ end
57
+
58
+ end
59
+
60
+ # Given a piece of a BoK resource descriptor Hash, come up with shorthand
61
+ # strings to give it a name for human readers. If nothing reasonable can be
62
+ # extracted, returns nil.
63
+ # @param obj [Hash]
64
+ # @param array_of [String]
65
+ # @param habitat_translate [String]
66
+ # @return [Array<String,nil>]
67
+ def self.getChunkName(obj, array_of = nil, habitat_translate: nil)
68
+ return [nil, nil] if obj.nil?
69
+ if [String, Integer, Boolean].include?(obj.class)
70
+ return [obj, nil]
71
+ end
72
+ obj_type = array_of || obj['type']
73
+ obj_name = obj['name'] || obj['id'] || obj['mu_name'] || obj['cloud_id']
74
+
75
+ name_string = if obj_name
76
+ if obj_type
77
+ "#{obj_type}[#{obj_name}]"
78
+ else
79
+ obj_name.dup
80
+ end
81
+ else
82
+ found_it = nil
83
+ using = nil
84
+ ["entity", "role"].each { |subtype|
85
+ if obj[subtype] and obj[subtype].is_a?(Hash)
86
+ found_it = if obj[subtype]["id"]
87
+ obj[subtype]['id'].dup
88
+ elsif obj[subtype]["type"] and obj[subtype]["name"]
89
+ "#{obj[subtype]['type']}[#{obj[subtype]['name']}]"
90
+ end
91
+ break
92
+ end
93
+ }
94
+ found_it
95
+ end
96
+ if name_string
97
+ name_string.gsub!(/\[.+?\](\[.+?\]$)/, '\1')
98
+ if habitat_translate and HABITAT_SYNONYMS[habitat_translate]
99
+ name_string.sub!(/^habitats?\[(.+?)\]/i, HABITAT_SYNONYMS[habitat_translate]+'[\1]')
100
+ end
101
+ end
102
+
103
+ location_list = []
104
+
105
+ location = if obj['project']
106
+ obj['project']
107
+ elsif obj['habitat'] and (obj['habitat']['id'] or obj['habitat']['name'])
108
+ obj['habitat']['name'] || obj['habitat']['id']
109
+ else
110
+ hab_str = nil
111
+ ['projects', 'habitats'].each { |key|
112
+
113
+ if obj[key] and obj[key].is_a?(Array)
114
+ location_list = obj[key].sort.map { |p|
115
+ (p["name"] || p["id"]).gsub(/^.*?[^\/]+\/([^\/]+)$/, '\1')
116
+ }
117
+ hab_str = location_list.join(", ")
118
+ name_string.gsub!(/^.*?[^\/]+\/([^\/]+)$/, '\1') if name_string
119
+ break
120
+ end
121
+ }
122
+ hab_str
123
+ end
124
+
125
+ [name_string, location, location_list]
126
+ end
127
+
22
128
  # Generate a three-character string which can be used to unique-ify the
23
129
  # names of resources which might potentially collide, e.g. Windows local
24
130
  # hostnames, Amazon Elastic Load Balancers, or server pool instances.
@@ -190,17 +296,18 @@ module MU
190
296
  # SSH config entries, etc.
191
297
  # @param server [MU::Cloud::Server]: The {MU::Cloud::Server} we'll be setting up.
192
298
  # @param sync_wait [Boolean]: Whether to wait for DNS to fully synchronize before returning.
193
- def self.nameKitten(server, sync_wait: false)
299
+ def self.nameKitten(server, sync_wait: false, no_dns: false)
194
300
  node, config, _deploydata = server.describe
195
301
 
196
302
  mu_zone = nil
197
303
  # XXX GCP!
198
- if MU::Cloud::AWS.hosted? and !MU::Cloud::AWS.isGovCloud?
304
+ if !no_dns and MU::Cloud::AWS.hosted? and !MU::Cloud::AWS.isGovCloud?
199
305
  zones = MU::Cloud::DNSZone.find(cloud_id: "platform-mu")
200
306
  mu_zone = zones.values.first if !zones.nil?
201
307
  end
308
+
202
309
  if !mu_zone.nil?
203
- MU::Cloud::DNSZone.genericMuDNSEntry(name: node, target: server.canonicalIP, cloudclass: MU::Cloud::Server, sync_wait: sync_wait)
310
+ MU::Cloud::DNSZone.genericMuDNSEntry(name: node.gsub(/[^a-z0-9!"\#$%&'\(\)\*\+,\-\/:;<=>\?@\[\]\^_`{\|}~\.]/, '-').gsub(/--|^-/, ''), target: server.canonicalIP, cloudclass: MU::Cloud::Server, sync_wait: sync_wait)
204
311
  else
205
312
  MU::Master.addInstanceToEtcHosts(server.canonicalIP, node)
206
313
  end
@@ -0,0 +1,497 @@
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
+ @@desc_semaphore = Mutex.new
23
+
24
+ # A search which returned multiple matches, but is not allowed to
25
+ class MultipleMatches < MuError
26
+ def initialize(message = nil)
27
+ super(message, silent: true)
28
+ end
29
+ end
30
+
31
+ # Locate a resource that's either a member of another deployment, or of no
32
+ # deployment at all, and return a {MU::Cloud} object for it.
33
+ # @param cloud [String]: The Cloud provider to use.
34
+ # @param type [String]: The resource type. Can be the full class name, symbolic name, or Basket of Kittens configuration shorthand for the resource type.
35
+ # @param deploy_id [String]: The identifier of an outside deploy to search.
36
+ # @param name [String]: The name of the resource as defined in its 'name' Basket of Kittens field, typically used in conjunction with deploy_id.
37
+ # @param mu_name [String]: The fully-resolved and deployed name of the resource, typically used in conjunction with deploy_id.
38
+ # @param cloud_id [String]: A cloud provider identifier for this resource.
39
+ # @param region [String]: The cloud provider region
40
+ # @param tag_key [String]: A cloud provider tag to help identify the resource, used in conjunction with tag_value.
41
+ # @param tag_value [String]: A cloud provider tag to help identify the resource, used in conjunction with tag_key.
42
+ # @param allow_multi [Boolean]: Permit an array of matching resources to be returned (if applicable) instead of just one.
43
+ # @param dummy_ok [Boolean]: Permit return of a faked {MU::Cloud} object if we don't have enough information to identify a real live one.
44
+ # @return [Array<MU::Cloud>]
45
+ def self.findStray(cloud, type,
46
+ dummy_ok: false,
47
+ no_deploy_search: false,
48
+ allow_multi: false,
49
+ deploy_id: nil,
50
+ name: nil,
51
+ mu_name: nil,
52
+ cloud_id: nil,
53
+ credentials: nil,
54
+ region: nil,
55
+ tag_key: nil,
56
+ tag_value: nil,
57
+ calling_deploy: MU.mommacat,
58
+ habitats: [],
59
+ **flags
60
+ )
61
+ _shortclass, _cfg_name, type, _classname, _attrs = MU::Cloud.getResourceNames(type, true)
62
+
63
+ cloudclass = MU::Cloud.cloudClass(cloud)
64
+ return nil if cloudclass.virtual?
65
+
66
+ if (tag_key and !tag_value) or (!tag_key and tag_value)
67
+ raise MuError, "Can't call findStray with only one of tag_key and tag_value set, must be both or neither"
68
+ end
69
+
70
+ credlist = credentials ? [credentials] : cloudclass.listCredentials
71
+
72
+ # Help ourselves by making more refined parameters out of mu_name, if
73
+ # they weren't passed explicitly
74
+ if mu_name
75
+ # We can extract a deploy_id from mu_name if we don't have one already
76
+ deploy_id ||= mu_name.sub(/^(\w+-\w+-\d{10}-[A-Z]{2})-/, '\1')
77
+ if !tag_key and !tag_value
78
+ tag_key = "Name"
79
+ tag_value = mu_name
80
+ end
81
+ end
82
+
83
+ # See if the thing we're looking for is a member of the deploy that's
84
+ # asking after it.
85
+ if !deploy_id.nil? and !calling_deploy.nil? and
86
+ calling_deploy.deploy_id == deploy_id and (!name.nil? or !mu_name.nil?)
87
+ kitten = calling_deploy.findLitterMate(type: type, name: name, mu_name: mu_name, cloud_id: cloud_id, credentials: credentials)
88
+ return [kitten] if !kitten.nil?
89
+ end
90
+
91
+ # See if we have it in deployment metadata generally
92
+ kittens = {}
93
+ if !no_deploy_search and (deploy_id or name or mu_name or cloud_id)
94
+ kittens = search_my_deploys(type, deploy_id: deploy_id, name: name, mu_name: mu_name, cloud_id: cloud_id, credentials: credentials)
95
+ return kittens.values if kittens.size == 1
96
+
97
+ # We can't refine any further by asking the cloud provider...
98
+ if kittens.size > 1 and !allow_multi and
99
+ !cloud_id and !tag_key and !tag_value
100
+ raise MultipleMatches, "Multiple matches in MU::MommaCat.findStray where none allowed from #{cloud}, #{type}, name: #{name}, mu_name: #{mu_name}, cloud_id: #{cloud_id}, credentials: #{credentials}, habitats: #{habitats} (#{caller(1..1)})"
101
+ end
102
+ end
103
+
104
+ if !cloud_id and !(tag_key and tag_value) and (name or mu_name or deploy_id)
105
+ return kittens.values
106
+ end
107
+ matches = []
108
+
109
+ credlist.each { |creds|
110
+ # next if region and region.is_a?(Array) and !region.empty? and !region.include?(r)
111
+ cloud_descs = search_cloud_provider(type, cloud, habitats, region, cloud_id: cloud_id, tag_key: tag_key, tag_value: tag_value, credentials: creds, flags: flags)
112
+
113
+ cloud_descs.each_pair.each { |p, regions|
114
+ regions.each_pair.each { |r, results|
115
+ results.each_pair { |kitten_cloud_id, descriptor|
116
+ # We already have a MU::Cloud object for this guy, use it
117
+ if kittens.has_key?(kitten_cloud_id)
118
+ matches << kittens[kitten_cloud_id]
119
+ elsif dummy_ok and kittens.empty?
120
+ # XXX this is why this was threaded
121
+ matches << generate_dummy_object(type, cloud, name, mu_name, kitten_cloud_id, descriptor, r, p, tag_value, calling_deploy, creds)
122
+ end
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ matches
129
+ end
130
+
131
+ @object_load_fails = false
132
+
133
+ # Return the resource object of another member of this deployment
134
+ # @param type [String,Symbol]: The type of resource
135
+ # @param name [String]: The name of the resource as defined in its 'name' Basket of Kittens field
136
+ # @param mu_name [String]: The fully-resolved and deployed name of the resource
137
+ # @param cloud_id [String]: The cloud provider's unique identifier for this resource
138
+ # @param created_only [Boolean]: Only return the littermate if its cloud_id method returns a value
139
+ # @param return_all [Boolean]: Return a Hash of matching objects indexed by their mu_name, instead of a single match. Only valid for resource types where has_multiples is true.
140
+ # @return [MU::Cloud]
141
+ def findLitterMate(type: nil, name: nil, mu_name: nil, cloud_id: nil, created_only: false, return_all: false, credentials: nil, habitat: nil, ignore_missing: false, debug: false, **flags)
142
+ _shortclass, _cfg_name, type, _classname, attrs = MU::Cloud.getResourceNames(type)
143
+
144
+ # If we specified a habitat, which we may also have done by its shorthand
145
+ # sibling name, or a Ref. Convert to something we can use.
146
+ habitat = resolve_habitat(habitat, credentials: credentials)
147
+
148
+ nofilter = (mu_name.nil? and cloud_id.nil? and credentials.nil?)
149
+
150
+ does_match = Proc.new { |obj|
151
+
152
+ (!created_only or !obj.cloud_id.nil?) and (nofilter or (
153
+ (mu_name and obj.mu_name and mu_name.to_s == obj.mu_name) or
154
+ (cloud_id and obj.cloud_id and cloud_id.to_s == obj.cloud_id.to_s) or
155
+ (credentials and obj.credentials and credentials.to_s == obj.credentials.to_s) and
156
+ !(
157
+ (mu_name and obj.mu_name and mu_name.to_s != obj.mu_name) or
158
+ (cloud_id and obj.cloud_id and cloud_id.to_s != obj.cloud_id.to_s) or
159
+ (credentials and obj.credentials and credentials.to_s != obj.credentials.to_s)
160
+ )
161
+ ))
162
+ }
163
+
164
+ @kitten_semaphore.synchronize {
165
+
166
+ if !@kittens.has_key?(type)
167
+ return nil if !@original_config or @original_config[type].nil? or @original_config[type].empty?
168
+ begin
169
+ loadObjects(false)
170
+ rescue ThreadError => e
171
+ if e.message !~ /deadlock/
172
+ raise e
173
+ end
174
+ end
175
+ if @object_load_fails or !@kittens[type]
176
+ if !ignore_missing
177
+ MU.log "#{@deploy_id}'s original config has #{@original_config[type].size == 1 ? "a" : @original_config[type].size.to_s} #{type}, but loadObjects could not populate anything from deployment metadata", MU::ERR if !@object_load_fails
178
+ @object_load_fails = true
179
+ end
180
+ return nil
181
+ end
182
+ end
183
+ matches = {}
184
+ @kittens[type].each { |habitat_group, sib_classes|
185
+ next if habitat and habitat_group and habitat_group != habitat
186
+ sib_classes.each_pair { |sib_class, cloud_objs|
187
+
188
+ if attrs[:has_multiples]
189
+ next if !name.nil? and name != sib_class or cloud_objs.empty?
190
+ if !name.nil?
191
+ if return_all
192
+ matches.merge!(cloud_objs.clone)
193
+ next
194
+ elsif cloud_objs.size == 1 and does_match.call(cloud_objs.values.first)
195
+ return cloud_objs.values.first
196
+ end
197
+ end
198
+
199
+ cloud_objs.each_value { |obj|
200
+ if does_match.call(obj)
201
+ if return_all
202
+ matches.merge!(cloud_objs.clone)
203
+ else
204
+ return obj.clone
205
+ end
206
+ end
207
+ }
208
+ # has_multiples is false, "cloud_objs" is actually a singular object
209
+ elsif (name.nil? and does_match.call(cloud_objs)) or [sib_class, cloud_objs.virtual_name(name)].include?(name.to_s)
210
+ matches[cloud_objs.config['name']] = cloud_objs.clone
211
+ end
212
+ }
213
+ }
214
+
215
+ return matches if return_all and matches.size >= 1
216
+
217
+ return matches.values.first if matches.size == 1
218
+
219
+ }
220
+
221
+ return nil
222
+ end
223
+
224
+
225
+ private
226
+
227
+ def resolve_habitat(habitat, credentials: nil, debug: false)
228
+ return nil if habitat.nil?
229
+ if habitat.is_a?(MU::Config::Ref) and habitat.id
230
+ return habitat.id
231
+ else
232
+ realhabitat = findLitterMate(type: "habitat", name: habitat, credentials: credentials)
233
+ if realhabitat and realhabitat.mu_name
234
+ return realhabitat.cloud_id
235
+ elsif debug
236
+ MU.log "Failed to resolve habitat name #{habitat}", MU::WARN
237
+ end
238
+ end
239
+ end
240
+
241
+ def self.generate_dummy_object(type, cloud, name, mu_name, cloud_id, desc, region, habitat, tag_value, calling_deploy, credentials)
242
+ resourceclass = MU::Cloud.resourceClass(cloud, type)
243
+
244
+ use_name = if (name.nil? or name.empty?)
245
+ if !mu_name.nil?
246
+ mu_name
247
+ else
248
+ guessName(desc, resourceclass, cloud_id: cloud_id, tag_value: tag_value)
249
+ end
250
+ else
251
+ name
252
+ end
253
+
254
+ if use_name.nil?
255
+ return
256
+ end
257
+
258
+ cfg = {
259
+ "name" => use_name,
260
+ "cloud" => cloud,
261
+ "credentials" => credentials
262
+ }
263
+ if !region.nil? and !resourceclass.isGlobal?
264
+ cfg["region"] = region
265
+ end
266
+
267
+ if resourceclass.canLiveIn.include?(:Habitat) and habitat
268
+ cfg["project"] = habitat
269
+ end
270
+
271
+ # If we can at least find the config from the deploy this will
272
+ # belong with, use that, even if it's an ungroomed resource.
273
+ if !calling_deploy.nil? and
274
+ !calling_deploy.original_config.nil? and
275
+ !calling_deploy.original_config[type+"s"].nil?
276
+ calling_deploy.original_config[type+"s"].each { |s|
277
+ if s["name"] == use_name
278
+ cfg = s.dup
279
+ break
280
+ end
281
+ }
282
+
283
+ return resourceclass.new(mommacat: calling_deploy, kitten_cfg: cfg, cloud_id: cloud_id)
284
+ else
285
+ if !@@dummy_cache[type] or !@@dummy_cache[type][cfg.to_s]
286
+ newobj = resourceclass.new(mu_name: use_name, kitten_cfg: cfg, cloud_id: cloud_id, from_cloud_desc: desc)
287
+ @@desc_semaphore.synchronize {
288
+ @@dummy_cache[type] ||= {}
289
+ @@dummy_cache[type][cfg.to_s] = newobj
290
+ }
291
+ end
292
+ return @@dummy_cache[type][cfg.to_s]
293
+ end
294
+ end
295
+ private_class_method :generate_dummy_object
296
+
297
+ def self.search_cloud_provider(type, cloud, habitats, region, cloud_id: nil, tag_key: nil, tag_value: nil, credentials: nil, flags: nil)
298
+ cloudclass = MU::Cloud.cloudClass(cloud)
299
+ resourceclass = MU::Cloud.resourceClass(cloud, type)
300
+
301
+ # Decide what regions we'll search, if applicable for this resource
302
+ # type.
303
+ regions = if resourceclass.isGlobal?
304
+ [nil]
305
+ else
306
+ if region
307
+ if region.is_a?(Array) and !region.empty?
308
+ region
309
+ else
310
+ [region]
311
+ end
312
+ else
313
+ cloudclass.listRegions(credentials: credentials)
314
+ end
315
+ end
316
+
317
+ # Decide what habitats (accounts/projects/subscriptions) we'll
318
+ # search, if applicable for this resource type.
319
+ habitats ||= []
320
+ if habitats.empty?
321
+ if resourceclass.canLiveIn.include?(nil)
322
+ habitats << nil
323
+ end
324
+ if resourceclass.canLiveIn.include?(:Habitat)
325
+ habitats.concat(cloudclass.listHabitats(credentials, use_cache: false))
326
+ end
327
+ end
328
+ habitats << nil if habitats.empty?
329
+ habitats.uniq!
330
+
331
+ cloud_descs = {}
332
+
333
+ thread_waiter = Proc.new { |threads, threshold|
334
+ begin
335
+ threads.each { |t| t.join(0.1) }
336
+ threads.reject! { |t| t.nil? or !t.alive? or !t.status }
337
+ sleep 1 if threads.size > threshold
338
+ end while threads.size > threshold
339
+ }
340
+
341
+ habitat_threads = []
342
+ found_the_thing = false
343
+ habitats.each { |hab|
344
+ break if found_the_thing
345
+ thread_waiter.call(habitat_threads, 5)
346
+
347
+ habitat_threads << Thread.new(hab) { |habitat|
348
+ cloud_descs[habitat] = {}
349
+ region_threads = []
350
+ regions.each { |reg|
351
+ break if found_the_thing
352
+ region_threads << Thread.new(reg) { |r|
353
+ found = resourceclass.find(cloud_id: cloud_id, region: r, tag_key: tag_key, tag_value: tag_value, credentials: credentials, habitat: habitat, flags: flags)
354
+
355
+ if found
356
+ @@desc_semaphore.synchronize {
357
+ cloud_descs[habitat][r] = found
358
+ }
359
+ end
360
+ # Stop if you found the thing by a specific cloud_id
361
+ if cloud_id and found and !found.empty?
362
+ found_the_thing = true
363
+ end
364
+ }
365
+ }
366
+ thread_waiter.call(region_threads, 0)
367
+ }
368
+ }
369
+ thread_waiter.call(habitat_threads, 0)
370
+
371
+ cloud_descs
372
+ end
373
+ private_class_method :search_cloud_provider
374
+
375
+ def self.search_my_deploys(type, deploy_id: nil, name: nil, mu_name: nil, cloud_id: nil, credentials: nil)
376
+ kittens = {}
377
+ _shortclass, _cfg_name, type, _classname, attrs = MU::Cloud.getResourceNames(type, true)
378
+
379
+ # Check our in-memory cache of live deploys before resorting to
380
+ # metadata
381
+ littercache = nil
382
+ # Sometimes we're called inside a locked thread, sometimes not. Deal
383
+ # with locking gracefully.
384
+ begin
385
+ @@litter_semaphore.synchronize {
386
+ littercache = @@litters.dup
387
+ }
388
+ rescue ThreadError => e
389
+ raise e if !e.message.match(/recursive locking/)
390
+ littercache = @@litters.dup
391
+ end
392
+
393
+ # First, see what we have in deploys that already happen to be loaded in
394
+ # memory.
395
+ littercache.each_pair { |cur_deploy, momma|
396
+ next if deploy_id and deploy_id != cur_deploy
397
+
398
+ @@deploy_struct_semaphore.synchronize {
399
+ @deploy_cache[deploy_id] = {
400
+ "mtime" => Time.now,
401
+ "data" => momma.deployment
402
+ }
403
+ }
404
+
405
+ straykitten = momma.findLitterMate(type: type, cloud_id: cloud_id, name: name, mu_name: mu_name, credentials: credentials, created_only: true)
406
+ if straykitten
407
+ MU.log "Found matching kitten #{straykitten.mu_name} in-memory - #{sprintf("%.2fs", (Time.now-start))}", MU::DEBUG
408
+ # Peace out if we found the exact resource we want
409
+ if cloud_id and straykitten.cloud_id.to_s == cloud_id.to_s
410
+ return { straykitten.cloud_id => straykitten }
411
+ elsif mu_name and straykitten.mu_name == mu_name
412
+ return { straykitten.cloud_id => straykitten }
413
+ else
414
+ kittens[straykitten.cloud_id] ||= straykitten
415
+ end
416
+ end
417
+ }
418
+
419
+ # Now go rifle metadata from any other deploys we have on disk, if they
420
+ # weren't already there in memory.
421
+ cacheDeployMetadata(deploy_id) # freshen up @@deploy_cache
422
+ mu_descs = {}
423
+ if deploy_id.nil?
424
+ @@deploy_cache.each_key { |deploy|
425
+ next if littercache[deploy]
426
+ next if !@@deploy_cache[deploy].has_key?('data')
427
+ next if !@@deploy_cache[deploy]['data'].has_key?(type)
428
+ if !name.nil?
429
+ next if @@deploy_cache[deploy]['data'][type][name].nil?
430
+ mu_descs[deploy] ||= []
431
+ mu_descs[deploy] << @@deploy_cache[deploy]['data'][type][name].dup
432
+ else
433
+ mu_descs[deploy] ||= []
434
+ mu_descs[deploy].concat(@@deploy_cache[deploy]['data'][type].values)
435
+ end
436
+ }
437
+ elsif !@@deploy_cache[deploy_id].nil?
438
+ if !@@deploy_cache[deploy_id]['data'].nil? and
439
+ !@@deploy_cache[deploy_id]['data'][type].nil?
440
+ if !name.nil? and !@@deploy_cache[deploy_id]['data'][type][name].nil?
441
+ mu_descs[deploy_id] ||= []
442
+ mu_descs[deploy_id] << @@deploy_cache[deploy_id]['data'][type][name].dup
443
+ else
444
+ mu_descs[deploy_id] = @@deploy_cache[deploy_id]['data'][type].values
445
+ end
446
+ end
447
+ end
448
+
449
+ mu_descs.each_pair { |deploy, matches|
450
+ next if matches.nil? or matches.size == 0
451
+ momma = MU::MommaCat.getLitter(deploy)
452
+
453
+ # If we found exactly one match in this deploy, use its metadata to
454
+ # guess at resource names we weren't told.
455
+ straykitten = if matches.size > 1 and cloud_id
456
+ momma.findLitterMate(type: type, cloud_id: cloud_id, credentials: credentials, created_only: true)
457
+ elsif matches.size == 1 and (!attrs[:has_multiples] or matches.first.size == 1) and name.nil? and mu_name.nil?
458
+ actual_data = attrs[:has_multiples] ? matches.first.values.first : matches.first
459
+ if cloud_id.nil?
460
+ momma.findLitterMate(type: type, name: (actual_data["name"] || actual_data["MU_NODE_CLASS"]), cloud_id: actual_data["cloud_id"], credentials: credentials)
461
+ else
462
+ momma.findLitterMate(type: type, name: (actual_data["name"] || actual_data["MU_NODE_CLASS"]), cloud_id: cloud_id, credentials: credentials)
463
+ end
464
+ else
465
+ # There's more than one of this type of resource in the target
466
+ # deploy, so see if findLitterMate can narrow it down for us
467
+ momma.findLitterMate(type: type, name: name, mu_name: mu_name, cloud_id: cloud_id, credentials: credentials)
468
+ end
469
+
470
+ next if straykitten.nil?
471
+ straykitten.intoDeploy(momma)
472
+
473
+ if straykitten.cloud_id.nil?
474
+ MU.log "findStray: kitten #{straykitten.mu_name} came back with nil cloud_id", MU::WARN
475
+ next
476
+ end
477
+ next if cloud_id and straykitten.cloud_id.to_s != cloud_id.to_s
478
+
479
+ # Peace out if we found the exact resource we want
480
+ if (cloud_id and straykitten.cloud_id.to_s == cloud_id.to_s) or
481
+ (mu_descs.size == 1 and matches.size == 1) or
482
+ (credentials and straykitten.credentials == credentials)
483
+ # XXX strictly speaking this last check is only valid if findStray is searching
484
+ # exactly one set of credentials
485
+
486
+ return { straykitten.cloud_id => straykitten }
487
+ end
488
+
489
+ kittens[straykitten.cloud_id] ||= straykitten
490
+ }
491
+
492
+ kittens
493
+ end
494
+ private_class_method :search_my_deploys
495
+
496
+ end #class
497
+ end #module