cloud-mu 3.1.5 → 3.1.6

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 (61) 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 +2 -1
  7. data/bin/mu-configure +16 -0
  8. data/bin/mu-node-manage +15 -16
  9. data/cloud-mu.gemspec +2 -2
  10. data/cookbooks/mu-activedirectory/resources/domain.rb +4 -4
  11. data/cookbooks/mu-activedirectory/resources/domain_controller.rb +4 -4
  12. data/cookbooks/mu-tools/recipes/eks.rb +2 -2
  13. data/cookbooks/mu-tools/recipes/windows-client.rb +25 -22
  14. data/extras/clean-stock-amis +25 -19
  15. data/extras/image-generators/AWS/win2k12.yaml +2 -0
  16. data/extras/image-generators/AWS/win2k16.yaml +2 -0
  17. data/extras/image-generators/AWS/win2k19.yaml +2 -0
  18. data/modules/mommacat.ru +1 -1
  19. data/modules/mu.rb +6 -5
  20. data/modules/mu/adoption.rb +19 -4
  21. data/modules/mu/cleanup.rb +181 -293
  22. data/modules/mu/cloud.rb +58 -17
  23. data/modules/mu/clouds/aws.rb +36 -1
  24. data/modules/mu/clouds/aws/container_cluster.rb +30 -21
  25. data/modules/mu/clouds/aws/role.rb +1 -1
  26. data/modules/mu/clouds/aws/vpc.rb +5 -1
  27. data/modules/mu/clouds/azure.rb +10 -0
  28. data/modules/mu/clouds/cloudformation.rb +10 -0
  29. data/modules/mu/clouds/google.rb +18 -4
  30. data/modules/mu/clouds/google/bucket.rb +2 -2
  31. data/modules/mu/clouds/google/container_cluster.rb +10 -7
  32. data/modules/mu/clouds/google/database.rb +3 -3
  33. data/modules/mu/clouds/google/firewall_rule.rb +3 -3
  34. data/modules/mu/clouds/google/function.rb +3 -3
  35. data/modules/mu/clouds/google/loadbalancer.rb +4 -4
  36. data/modules/mu/clouds/google/role.rb +18 -9
  37. data/modules/mu/clouds/google/server.rb +16 -14
  38. data/modules/mu/clouds/google/server_pool.rb +4 -4
  39. data/modules/mu/clouds/google/user.rb +2 -2
  40. data/modules/mu/clouds/google/vpc.rb +9 -13
  41. data/modules/mu/config.rb +1 -1
  42. data/modules/mu/config/container_cluster.rb +5 -0
  43. data/modules/mu/config/doc_helpers.rb +1 -1
  44. data/modules/mu/config/ref.rb +12 -6
  45. data/modules/mu/config/schema_helpers.rb +8 -3
  46. data/modules/mu/config/server.rb +7 -0
  47. data/modules/mu/config/tail.rb +1 -0
  48. data/modules/mu/config/vpc.rb +15 -7
  49. data/modules/mu/config/vpc.yml +0 -1
  50. data/modules/mu/defaults/AWS.yaml +48 -48
  51. data/modules/mu/deploy.rb +1 -1
  52. data/modules/mu/groomer.rb +1 -1
  53. data/modules/mu/groomers/ansible.rb +69 -4
  54. data/modules/mu/groomers/chef.rb +48 -4
  55. data/modules/mu/master.rb +75 -3
  56. data/modules/mu/mommacat.rb +104 -855
  57. data/modules/mu/mommacat/naming.rb +28 -0
  58. data/modules/mu/mommacat/search.rb +463 -0
  59. data/modules/mu/mommacat/storage.rb +185 -183
  60. data/modules/tests/super_simple_bok.yml +1 -3
  61. metadata +8 -5
@@ -19,6 +19,34 @@ module MU
19
19
  # the normal synchronous deploy sequence invoked by *mu-deploy*.
20
20
  class MommaCat
21
21
 
22
+ # Given a cloud provider's native descriptor for a resource, make some
23
+ # reasonable guesses about what the thing's name should be.
24
+ def self.guessName(desc, resourceclass, cloud_id: nil, tag_value: nil)
25
+ if desc.respond_to?(:tags) and
26
+ desc.tags.is_a?(Array) and
27
+ desc.tags.first.respond_to?(:key) and
28
+ desc.tags.map { |t| t.key }.include?("Name")
29
+ desc.tags.select { |t| t.key == "Name" }.first.value
30
+ else
31
+ try = nil
32
+ # Various GCP fields
33
+ [:display_name, :name, (resourceclass.cfg_name+"_name").to_sym].each { |field|
34
+ if desc.respond_to?(field) and desc.send(field).is_a?(String)
35
+ try = desc.send(field)
36
+ break
37
+ end
38
+
39
+ }
40
+ try ||= if !tag_value.nil?
41
+ tag_value
42
+ else
43
+ cloud_id
44
+ end
45
+ try
46
+ end
47
+
48
+ end
49
+
22
50
  # Generate a three-character string which can be used to unique-ify the
23
51
  # names of resources which might potentially collide, e.g. Windows local
24
52
  # hostnames, Amazon Elastic Load Balancers, or server pool instances.
@@ -0,0 +1,463 @@
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.assertSupportedCloud(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
+ 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)
111
+
112
+ cloud_descs.each_pair.each { |p, regions|
113
+ regions.each_pair.each { |r, results|
114
+ results.each_pair { |kitten_cloud_id, descriptor|
115
+ # We already have a MU::Cloud object for this guy, use it
116
+ if kittens.has_key?(kitten_cloud_id)
117
+ matches << kittens[kitten_cloud_id]
118
+ elsif dummy_ok and kittens.empty?
119
+ # XXX this is why this was threaded
120
+ matches << generate_dummy_object(type, cloud, name, mu_name, kitten_cloud_id, descriptor, r, p, tag_value, calling_deploy, creds)
121
+ end
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ matches
128
+ end
129
+
130
+ # Return the resource object of another member of this deployment
131
+ # @param type [String,Symbol]: The type of resource
132
+ # @param name [String]: The name of the resource as defined in its 'name' Basket of Kittens field
133
+ # @param mu_name [String]: The fully-resolved and deployed name of the resource
134
+ # @param cloud_id [String]: The cloud provider's unique identifier for this resource
135
+ # @param created_only [Boolean]: Only return the littermate if its cloud_id method returns a value
136
+ # @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.
137
+ # @return [MU::Cloud]
138
+ def findLitterMate(type: nil, name: nil, mu_name: nil, cloud_id: nil, created_only: false, return_all: false, credentials: nil, habitat: nil, **flags)
139
+ _shortclass, _cfg_name, type, _classname, attrs = MU::Cloud.getResourceNames(type)
140
+
141
+ # If we specified a habitat, which we may also have done by its shorthand
142
+ # sibling name, or a Ref. Convert to something we can use.
143
+ habitat = resolve_habitat(habitat, credentials: credentials)
144
+
145
+ nofilter = (mu_name.nil? and cloud_id.nil? and credentials.nil?)
146
+
147
+ does_match = Proc.new { |obj|
148
+
149
+ (!created_only or !obj.cloud_id.nil?) and (nofilter or (
150
+ (mu_name and obj.mu_name and mu_name.to_s == obj.mu_name) or
151
+ (cloud_id and obj.cloud_id and cloud_id.to_s == obj.cloud_id.to_s) or
152
+ (credentials and obj.credentials and credentials.to_s == obj.credentials.to_s) and
153
+ !(
154
+ (mu_name and obj.mu_name and mu_name.to_s != obj.mu_name) or
155
+ (cloud_id and obj.cloud_id and cloud_id.to_s != obj.cloud_id.to_s) or
156
+ (credentials and obj.credentials and credentials.to_s != obj.credentials.to_s)
157
+ )
158
+ ))
159
+ }
160
+
161
+ @kitten_semaphore.synchronize {
162
+ return nil if !@kittens.has_key?(type)
163
+ matches = []
164
+
165
+ @kittens[type].each { |habitat_group, sib_classes|
166
+ next if habitat and habitat_group and habitat_group != habitat
167
+ sib_classes.each_pair { |sib_class, cloud_objs|
168
+ if attrs[:has_multiples]
169
+ next if !name.nil? and name != sib_class or cloud_objs.empty?
170
+ if !name.nil?
171
+ if return_all
172
+ return cloud_objs.dup
173
+ elsif cloud_objs.size == 1 and does_match.call(cloud_objs.values.first)
174
+ return cloud_objs.values.first
175
+ end
176
+ end
177
+
178
+ cloud_objs.each_value { |obj|
179
+ if does_match.call(obj)
180
+ return (return_all ? cloud_objs.clone : obj.clone)
181
+ end
182
+ }
183
+ # has_multiples is false
184
+ elsif (name.nil? and does_match.call(cloud_objs)) or [sib_class, cloud_objs.virtual_name(name)].include?(name.to_s)
185
+ matches << cloud_objs.clone
186
+ end
187
+ }
188
+ }
189
+
190
+ return matches.first if matches.size == 1
191
+
192
+ return matches if return_all and matches.size > 1
193
+ }
194
+
195
+ return nil
196
+ end
197
+
198
+
199
+ private
200
+
201
+ def resolve_habitat(habitat, credentials: nil, debug: false)
202
+ return nil if habitat.nil?
203
+ if habitat.is_a?(MU::Config::Ref) and habitat.id
204
+ return habitat.id
205
+ else
206
+ realhabitat = findLitterMate(type: "habitat", name: habitat, credentials: credentials)
207
+ if realhabitat and realhabitat.mu_name
208
+ return realhabitat.cloud_id
209
+ elsif debug
210
+ MU.log "Failed to resolve habitat name #{habitat}", MU::WARN
211
+ end
212
+ end
213
+ end
214
+
215
+ def self.generate_dummy_object(type, cloud, name, mu_name, cloud_id, desc, region, habitat, tag_value, calling_deploy, credentials)
216
+ resourceclass = MU::Cloud.loadCloudType(cloud, type)
217
+
218
+ use_name = if (name.nil? or name.empty?)
219
+ if !mu_name.nil?
220
+ mu_name
221
+ else
222
+ guessName(desc, resourceclass, cloud_id: cloud_id, tag_value: tag_value)
223
+ end
224
+ else
225
+ name
226
+ end
227
+
228
+ if use_name.nil?
229
+ return
230
+ end
231
+
232
+ cfg = {
233
+ "name" => use_name,
234
+ "cloud" => cloud,
235
+ "credentials" => credentials
236
+ }
237
+ if !region.nil? and !resourceclass.isGlobal?
238
+ cfg["region"] = region
239
+ end
240
+
241
+ if resourceclass.canLiveIn.include?(:Habitat) and habitat
242
+ cfg["project"] = habitat
243
+ end
244
+
245
+ # If we can at least find the config from the deploy this will
246
+ # belong with, use that, even if it's an ungroomed resource.
247
+ if !calling_deploy.nil? and
248
+ !calling_deploy.original_config.nil? and
249
+ !calling_deploy.original_config[type+"s"].nil?
250
+ calling_deploy.original_config[type+"s"].each { |s|
251
+ if s["name"] == use_name
252
+ cfg = s.dup
253
+ break
254
+ end
255
+ }
256
+
257
+ return resourceclass.new(mommacat: calling_deploy, kitten_cfg: cfg, cloud_id: cloud_id)
258
+ else
259
+ if !@@dummy_cache[type] or !@@dummy_cache[type][cfg.to_s]
260
+ newobj = resourceclass.new(mu_name: use_name, kitten_cfg: cfg, cloud_id: cloud_id, from_cloud_desc: desc)
261
+ @@desc_semaphore.synchronize {
262
+ @@dummy_cache[type] ||= {}
263
+ @@dummy_cache[type][cfg.to_s] = newobj
264
+ }
265
+ end
266
+ return @@dummy_cache[type][cfg.to_s]
267
+ end
268
+ end
269
+ private_class_method :generate_dummy_object
270
+
271
+ def self.search_cloud_provider(type, cloud, habitats, region, cloud_id: nil, tag_key: nil, tag_value: nil, credentials: nil, flags: nil)
272
+ cloudclass = MU::Cloud.assertSupportedCloud(cloud)
273
+ resourceclass = MU::Cloud.loadCloudType(cloud, type)
274
+
275
+ # Decide what regions we'll search, if applicable for this resource
276
+ # type.
277
+ regions = if resourceclass.isGlobal?
278
+ [nil]
279
+ else
280
+ region ? [region] : cloudclass.listRegions(credentials: credentials)
281
+ end
282
+
283
+ # Decide what habitats (accounts/projects/subscriptions) we'll
284
+ # search, if applicable for this resource type.
285
+ habitats ||= []
286
+ if habitats.empty?
287
+ if resourceclass.canLiveIn.include?(nil)
288
+ habitats << nil
289
+ end
290
+ if resourceclass.canLiveIn.include?(:Habitat)
291
+ habitats.concat(cloudclass.listHabitats(credentials))
292
+ end
293
+ end
294
+ habitats << nil if habitats.empty?
295
+ habitats.uniq!
296
+
297
+ cloud_descs = {}
298
+
299
+ thread_waiter = Proc.new { |threads, threshold|
300
+ begin
301
+ threads.each { |t| t.join(0.1) }
302
+ threads.reject! { |t| t.nil? or !t.alive? or !t.status }
303
+ sleep 1 if threads.size > threshold
304
+ end while threads.size > threshold
305
+ }
306
+
307
+ habitat_threads = []
308
+ found_the_thing = false
309
+ habitats.each { |hab|
310
+ break if found_the_thing
311
+ thread_waiter.call(habitat_threads, 5)
312
+
313
+ habitat_threads << Thread.new(hab) { |habitat|
314
+ cloud_descs[habitat] = {}
315
+ region_threads = []
316
+ regions.each { |reg|
317
+ break if found_the_thing
318
+ region_threads << Thread.new(reg) { |r|
319
+ found = resourceclass.find(cloud_id: cloud_id, region: r, tag_key: tag_key, tag_value: tag_value, credentials: credentials, habitat: habitat, flags: flags)
320
+
321
+ if found
322
+ @@desc_semaphore.synchronize {
323
+ cloud_descs[habitat][r] = found
324
+ }
325
+ end
326
+ # Stop if you found the thing by a specific cloud_id
327
+ if cloud_id and found and !found.empty?
328
+ found_the_thing = true
329
+ end
330
+ }
331
+ }
332
+ thread_waiter.call(region_threads, 0)
333
+ }
334
+ }
335
+ thread_waiter.call(habitat_threads, 0)
336
+
337
+ cloud_descs
338
+ end
339
+ private_class_method :search_cloud_provider
340
+
341
+ def self.search_my_deploys(type, deploy_id: nil, name: nil, mu_name: nil, cloud_id: nil, credentials: nil)
342
+ kittens = {}
343
+ _shortclass, _cfg_name, type, _classname, attrs = MU::Cloud.getResourceNames(type, true)
344
+
345
+ # Check our in-memory cache of live deploys before resorting to
346
+ # metadata
347
+ littercache = nil
348
+ # Sometimes we're called inside a locked thread, sometimes not. Deal
349
+ # with locking gracefully.
350
+ begin
351
+ @@litter_semaphore.synchronize {
352
+ littercache = @@litters.dup
353
+ }
354
+ rescue ThreadError => e
355
+ raise e if !e.message.match(/recursive locking/)
356
+ littercache = @@litters.dup
357
+ end
358
+
359
+ # First, see what we have in deploys that already happen to be loaded in
360
+ # memory.
361
+ littercache.each_pair { |cur_deploy, momma|
362
+ next if deploy_id and deploy_id != cur_deploy
363
+
364
+ @@deploy_struct_semaphore.synchronize {
365
+ @deploy_cache[deploy_id] = {
366
+ "mtime" => Time.now,
367
+ "data" => momma.deployment
368
+ }
369
+ }
370
+
371
+ straykitten = momma.findLitterMate(type: type, cloud_id: cloud_id, name: name, mu_name: mu_name, credentials: credentials, created_only: true)
372
+ if straykitten
373
+ MU.log "Found matching kitten #{straykitten.mu_name} in-memory - #{sprintf("%.2fs", (Time.now-start))}", MU::DEBUG
374
+ # Peace out if we found the exact resource we want
375
+ if cloud_id and straykitten.cloud_id.to_s == cloud_id.to_s
376
+ return { straykitten.cloud_id => straykitten }
377
+ elsif mu_name and straykitten.mu_name == mu_name
378
+ return { straykitten.cloud_id => straykitten }
379
+ else
380
+ kittens[straykitten.cloud_id] ||= straykitten
381
+ end
382
+ end
383
+ }
384
+
385
+ # Now go rifle metadata from any other deploys we have on disk, if they
386
+ # weren't already there in memory.
387
+ cacheDeployMetadata(deploy_id) # freshen up @@deploy_cache
388
+ mu_descs = {}
389
+ if deploy_id.nil?
390
+ @@deploy_cache.each_key { |deploy|
391
+ next if littercache[deploy]
392
+ next if !@@deploy_cache[deploy].has_key?('data')
393
+ next if !@@deploy_cache[deploy]['data'].has_key?(type)
394
+ if !name.nil?
395
+ next if @@deploy_cache[deploy]['data'][type][name].nil?
396
+ mu_descs[deploy] ||= []
397
+ mu_descs[deploy] << @@deploy_cache[deploy]['data'][type][name].dup
398
+ else
399
+ mu_descs[deploy] ||= []
400
+ mu_descs[deploy].concat(@@deploy_cache[deploy]['data'][type].values)
401
+ end
402
+ }
403
+ elsif !@@deploy_cache[deploy_id].nil?
404
+ if !@@deploy_cache[deploy_id]['data'].nil? and
405
+ !@@deploy_cache[deploy_id]['data'][type].nil?
406
+ if !name.nil? and !@@deploy_cache[deploy_id]['data'][type][name].nil?
407
+ mu_descs[deploy_id] ||= []
408
+ mu_descs[deploy_id] << @@deploy_cache[deploy_id]['data'][type][name].dup
409
+ else
410
+ mu_descs[deploy_id] = @@deploy_cache[deploy_id]['data'][type].values
411
+ end
412
+ end
413
+ end
414
+
415
+ mu_descs.each_pair { |deploy, matches|
416
+ next if matches.nil? or matches.size == 0
417
+ momma = MU::MommaCat.getLitter(deploy)
418
+
419
+ # If we found exactly one match in this deploy, use its metadata to
420
+ # guess at resource names we weren't told.
421
+ straykitten = if matches.size > 1 and cloud_id
422
+ momma.findLitterMate(type: type, cloud_id: cloud_id, credentials: credentials, created_only: true)
423
+ elsif matches.size == 1 and (!attrs[:has_multiples] or matches.first.size == 1) and name.nil? and mu_name.nil?
424
+ actual_data = attrs[:has_multiples] ? matches.first.values.first : matches.first
425
+ if cloud_id.nil?
426
+ momma.findLitterMate(type: type, name: (actual_data["name"] || actual_data["MU_NODE_CLASS"]), cloud_id: actual_data["cloud_id"], credentials: credentials)
427
+ else
428
+ momma.findLitterMate(type: type, name: (actual_data["name"] || actual_data["MU_NODE_CLASS"]), cloud_id: cloud_id, credentials: credentials)
429
+ end
430
+ else
431
+ # There's more than one of this type of resource in the target
432
+ # deploy, so see if findLitterMate can narrow it down for us
433
+ momma.findLitterMate(type: type, name: name, mu_name: mu_name, cloud_id: cloud_id, credentials: credentials)
434
+ end
435
+
436
+ next if straykitten.nil?
437
+ straykitten.intoDeploy(momma)
438
+
439
+ if straykitten.cloud_id.nil?
440
+ MU.log "findStray: kitten #{straykitten.mu_name} came back with nil cloud_id", MU::WARN
441
+ next
442
+ end
443
+ next if cloud_id and straykitten.cloud_id.to_s != cloud_id.to_s
444
+
445
+ # Peace out if we found the exact resource we want
446
+ if (cloud_id and straykitten.cloud_id.to_s == cloud_id.to_s) or
447
+ (mu_descs.size == 1 and matches.size == 1) or
448
+ (credentials and straykitten.credentials == credentials)
449
+ # XXX strictly speaking this last check is only valid if findStray is searching
450
+ # exactly one set of credentials
451
+
452
+ return { straykitten.cloud_id => straykitten }
453
+ end
454
+
455
+ kittens[straykitten.cloud_id] ||= straykitten
456
+ }
457
+
458
+ kittens
459
+ end
460
+ private_class_method :search_my_deploys
461
+
462
+ end #class
463
+ end #module