cloud-mu 3.1.5 → 3.1.6

Sign up to get free protection for your applications and to get access to all the features.
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