cloud-mu 2.0.0.pre.beta2 → 2.0.0.pre.beta3

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/Berksfile.lock +1 -1
  3. data/cloud-mu.gemspec +4 -3
  4. data/cookbooks/mu-master/templates/default/mu.rc.erb +2 -2
  5. data/cookbooks/mu-tools/files/default/Mu_CA.pem +18 -19
  6. data/cookbooks/mu-tools/recipes/rsyslog.rb +1 -1
  7. data/modules/mu/cleanup.rb +14 -1
  8. data/modules/mu/cloud.rb +40 -22
  9. data/modules/mu/clouds/aws/alarm.rb +6 -0
  10. data/modules/mu/clouds/aws/bucket.rb +29 -0
  11. data/modules/mu/clouds/aws/cache_cluster.rb +6 -0
  12. data/modules/mu/clouds/aws/container_cluster.rb +6 -0
  13. data/modules/mu/clouds/aws/database.rb +6 -0
  14. data/modules/mu/clouds/aws/dnszone.rb +6 -0
  15. data/modules/mu/clouds/aws/endpoint.rb +6 -0
  16. data/modules/mu/clouds/aws/firewall_rule.rb +6 -0
  17. data/modules/mu/clouds/aws/folder.rb +6 -0
  18. data/modules/mu/clouds/aws/function.rb +6 -0
  19. data/modules/mu/clouds/aws/group.rb +6 -0
  20. data/modules/mu/clouds/aws/loadbalancer.rb +6 -0
  21. data/modules/mu/clouds/aws/log.rb +6 -0
  22. data/modules/mu/clouds/aws/msg_queue.rb +6 -0
  23. data/modules/mu/clouds/aws/nosqldb.rb +6 -0
  24. data/modules/mu/clouds/aws/notifier.rb +6 -0
  25. data/modules/mu/clouds/aws/role.rb +97 -11
  26. data/modules/mu/clouds/aws/search_domain.rb +6 -0
  27. data/modules/mu/clouds/aws/server.rb +6 -0
  28. data/modules/mu/clouds/aws/server_pool.rb +6 -0
  29. data/modules/mu/clouds/aws/storage_pool.rb +6 -0
  30. data/modules/mu/clouds/aws/user.rb +6 -0
  31. data/modules/mu/clouds/aws/vpc.rb +25 -1
  32. data/modules/mu/clouds/google.rb +86 -16
  33. data/modules/mu/clouds/google/bucket.rb +78 -3
  34. data/modules/mu/clouds/google/container_cluster.rb +12 -0
  35. data/modules/mu/clouds/google/database.rb +15 -1
  36. data/modules/mu/clouds/google/firewall_rule.rb +18 -2
  37. data/modules/mu/clouds/google/folder.rb +183 -16
  38. data/modules/mu/clouds/google/group.rb +7 -1
  39. data/modules/mu/clouds/google/habitat.rb +139 -24
  40. data/modules/mu/clouds/google/loadbalancer.rb +26 -12
  41. data/modules/mu/clouds/google/server.rb +25 -10
  42. data/modules/mu/clouds/google/server_pool.rb +16 -3
  43. data/modules/mu/clouds/google/user.rb +7 -1
  44. data/modules/mu/clouds/google/vpc.rb +87 -76
  45. data/modules/mu/config.rb +12 -0
  46. data/modules/mu/config/bucket.rb +4 -0
  47. data/modules/mu/config/folder.rb +1 -0
  48. data/modules/mu/config/habitat.rb +1 -1
  49. data/modules/mu/config/role.rb +78 -34
  50. data/modules/mu/config/vpc.rb +1 -0
  51. data/modules/mu/groomers/chef.rb +1 -1
  52. data/modules/mu/kittens.rb +689 -283
  53. metadata +5 -4
@@ -216,6 +216,12 @@ module MU
216
216
  false
217
217
  end
218
218
 
219
+ # Denote whether this resource implementation is experiment, ready for
220
+ # testing, or ready for production use.
221
+ def self.quality
222
+ MU::Cloud::BETA
223
+ end
224
+
219
225
  # Remove all logs associated with the currently loaded deployment.
220
226
  # @param noop [Boolean]: If true, will only print what would be done
221
227
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -133,6 +133,12 @@ module MU
133
133
  false
134
134
  end
135
135
 
136
+ # Denote whether this resource implementation is experiment, ready for
137
+ # testing, or ready for production use.
138
+ def self.quality
139
+ MU::Cloud::RELEASE
140
+ end
141
+
136
142
  # Remove all msg_queues associated with the currently loaded deployment.
137
143
  # @param noop [Boolean]: If true, will only print what would be done
138
144
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -160,6 +160,12 @@ pp params
160
160
  false
161
161
  end
162
162
 
163
+ # Denote whether this resource implementation is experiment, ready for
164
+ # testing, or ready for production use.
165
+ def self.quality
166
+ MU::Cloud::BETA
167
+ end
168
+
163
169
  # Remove all buckets associated with the currently loaded deployment.
164
170
  # @param noop [Boolean]: If true, will only print what would be done
165
171
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -62,6 +62,12 @@ module MU
62
62
  false
63
63
  end
64
64
 
65
+ # Denote whether this resource implementation is experiment, ready for
66
+ # testing, or ready for production use.
67
+ def self.quality
68
+ MU::Cloud::BETA
69
+ end
70
+
65
71
  # Remove all notifiers associated with the currently loaded deployment.
66
72
  # @param noop [Boolean]: If true, will only print what would be done
67
73
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -292,6 +292,12 @@ module MU
292
292
  true
293
293
  end
294
294
 
295
+ # Denote whether this resource implementation is experiment, ready for
296
+ # testing, or ready for production use.
297
+ def self.quality
298
+ MU::Cloud::BETA
299
+ end
300
+
295
301
  # Remove all roles associated with the currently loaded deployment.
296
302
  # @param noop [Boolean]: If true, will only print what would be done
297
303
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -485,6 +491,42 @@ module MU
485
491
  resp.instance_profile.arn
486
492
  end
487
493
 
494
+ # Schema fragment for IAM policy conditions, which some other resource
495
+ # types may need to import.
496
+ def self.condition_schema
497
+ {
498
+ "items" => {
499
+ "properties" => {
500
+ "conditions" => {
501
+ "type" => "array",
502
+ "items" => {
503
+ "type" => "object",
504
+ "description" => "One or more conditions under which to apply this policy. See also: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html",
505
+ "required" => ["comparison", "variable", "values"],
506
+ "properties" => {
507
+ "comparison" => {
508
+ "type" => "string",
509
+ "description" => "A comparison to make, like +DateGreaterThan+ or +IpAddress+."
510
+ },
511
+ "variable" => {
512
+ "type" => "string",
513
+ "description" => "The variable which we will compare, like +aws:CurrentTime+ or +aws:SourceIp+."
514
+ },
515
+ "values" => {
516
+ "type" => "array",
517
+ "items" => {
518
+ "type" => "string",
519
+ "description" => "Value(s) to which we will compare our variable, like +2013-08-16T15:00:00Z+ or +192.0.2.0/24+."
520
+ }
521
+ }
522
+ }
523
+ }
524
+ }
525
+ }
526
+ }
527
+ }
528
+ end
529
+
488
530
  # Cloud-specific configuration properties.
489
531
  # @param config [MU::Config]: The calling MU::Config object
490
532
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
@@ -499,9 +541,11 @@ module MU
499
541
  end
500
542
  }.map { |t| MU::Cloud.resource_types[t][:cfg_name] }.sort
501
543
 
544
+
502
545
  schema = {
503
546
  "tags" => MU::Config.tags_primitive,
504
547
  "optional_tags" => MU::Config.optional_tags_primitive,
548
+ "policies" => self.condition_schema,
505
549
  "import" => {
506
550
  "items" => {
507
551
  "description" => "Can be a shorthand reference to a canned IAM policy like +AdministratorAccess+, or a full ARN like +arn:aws:iam::aws:policy/AmazonESCognitoAccess+"
@@ -611,14 +655,15 @@ module MU
611
655
  ok
612
656
  end
613
657
 
614
- private
615
-
616
- # Convert entries from the cloud-neutral @config['policies'] list into
617
- # AWS syntax.
618
- def convert_policies_to_iam
658
+ # Convert our generic internal representation of access policies into
659
+ # structures suitable for AWS IAM policy documents.
660
+ # @param policies [Array<Hash>]: One or more policy chunks
661
+ # @param deploy_obj [MU::MommaCat]: Deployment object to use when looking up sibling Mu resources
662
+ # @return [Array<Hash>]
663
+ def self.genPolicyDocument(policies, deploy_obj: nil)
619
664
  iam_policies = []
620
- if @config['policies']
621
- @config['policies'].each { |policy|
665
+ if policies
666
+ policies.each { |policy|
622
667
  doc = {
623
668
  "Version" => "2012-10-17",
624
669
  "Statement" => [
@@ -633,19 +678,52 @@ module MU
633
678
  policy["permissions"].each { |perm|
634
679
  doc["Statement"].first["Action"] << perm
635
680
  }
681
+ if policy["conditions"]
682
+ doc["Statement"].first["Condition"] ||= {}
683
+ policy["conditions"].each { |cond|
684
+ doc["Statement"].first["Condition"][cond['comparison']] = {
685
+ cond["variable"] => cond["values"]
686
+ }
687
+ }
688
+ end
689
+ if policy["grant_to"] # XXX factor this with target, they're too similar
690
+ doc["Statement"].first["Principal"] ||= []
691
+ policy["grant_to"].each { |grantee|
692
+ if grantee["type"] and deploy_obj
693
+ sibling = deploy_obj.findLitterMate(
694
+ name: grantee["identifier"],
695
+ type: grantee["type"]
696
+ )
697
+ if sibling
698
+ id = sibling.cloudobj.arn
699
+ doc["Statement"].first["Principal"] << id
700
+ else
701
+ raise MuError, "Couldn't find a #{grantee["type"]} named #{grantee["identifier"]} when generating IAM policy"
702
+ end
703
+ else
704
+ doc["Statement"].first["Principal"] << grantee["identifier"]
705
+ end
706
+ }
707
+ if policy["grant_to"].size == 1
708
+ doc["Statement"].first["Principal"] = doc["Statement"].first["Principal"].first
709
+ end
710
+ end
636
711
  if policy["targets"]
637
712
  policy["targets"].each { |target|
638
- if target["type"]
639
- sibling = @deploy.findLitterMate(
713
+ if target["type"] and deploy_obj
714
+ sibling = deploy_obj.findLitterMate(
640
715
  name: target["identifier"],
641
716
  type: target["type"]
642
717
  )
643
718
  if sibling
644
- doc["Statement"].first["Resource"] << sibling.cloudobj.arn
719
+ id = sibling.cloudobj.arn
720
+ id += target["path"] if target["path"]
721
+ doc["Statement"].first["Resource"] << id
645
722
  else
646
- raise MuError, "Couldn't find a #{target["entity_type"]} named #{target["identifier"]} when generating IAM policy in role #{@mu_name}"
723
+ raise MuError, "Couldn't find a #{target["entity_type"]} named #{target["identifier"]} when generating IAM policy"
647
724
  end
648
725
  else
726
+ target["identifier"] += target["path"] if target["path"]
649
727
  doc["Statement"].first["Resource"] << target["identifier"]
650
728
  end
651
729
  }
@@ -657,6 +735,14 @@ module MU
657
735
  iam_policies
658
736
  end
659
737
 
738
+ private
739
+
740
+ # Convert entries from the cloud-neutral @config['policies'] list into
741
+ # AWS syntax.
742
+ def convert_policies_to_iam
743
+ MU::Cloud::AWS::Role.genPolicyDocument(@config['policies'], deploy_obj: @deploy)
744
+ end
745
+
660
746
  def get_tag_params(strip_std = false)
661
747
  @config['tags'] ||= []
662
748
 
@@ -108,6 +108,12 @@ module MU
108
108
  false
109
109
  end
110
110
 
111
+ # Denote whether this resource implementation is experiment, ready for
112
+ # testing, or ready for production use.
113
+ def self.quality
114
+ MU::Cloud::RELEASE
115
+ end
116
+
111
117
  # Remove all search_domains associated with the currently loaded deployment.
112
118
  # @param noop [Boolean]: If true, will only print what would be done
113
119
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -1794,6 +1794,12 @@ module MU
1794
1794
  false
1795
1795
  end
1796
1796
 
1797
+ # Denote whether this resource implementation is experiment, ready for
1798
+ # testing, or ready for production use.
1799
+ def self.quality
1800
+ MU::Cloud::RELEASE
1801
+ end
1802
+
1797
1803
  # Remove all instances associated with the currently loaded deployment. Also cleans up associated volumes, droppings in the MU master's /etc/hosts and ~/.ssh, and in whatever Groomer was used.
1798
1804
  # @param noop [Boolean]: If true, will only print what would be done
1799
1805
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -989,6 +989,12 @@ module MU
989
989
  false
990
990
  end
991
991
 
992
+ # Denote whether this resource implementation is experiment, ready for
993
+ # testing, or ready for production use.
994
+ def self.quality
995
+ MU::Cloud::RELEASE
996
+ end
997
+
992
998
  # Remove all autoscale groups associated with the currently loaded deployment.
993
999
  # @param noop [Boolean]: If true, will only print what would be done
994
1000
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -349,6 +349,12 @@ module MU
349
349
  false
350
350
  end
351
351
 
352
+ # Denote whether this resource implementation is experiment, ready for
353
+ # testing, or ready for production use.
354
+ def self.quality
355
+ MU::Cloud::RELEASE
356
+ end
357
+
352
358
  # Called by {MU::Cleanup}. Locates resources that were created by the
353
359
  # currently-loaded deployment, and purges them.
354
360
  # @param noop [Boolean]: If true, will only print what would be done
@@ -136,6 +136,12 @@ module MU
136
136
  true
137
137
  end
138
138
 
139
+ # Denote whether this resource implementation is experiment, ready for
140
+ # testing, or ready for production use.
141
+ def self.quality
142
+ MU::Cloud::BETA
143
+ end
144
+
139
145
  # Remove all users associated with the currently loaded deployment.
140
146
  # @param noop [Boolean]: If true, will only print what would be done
141
147
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -1129,6 +1129,12 @@ module MU
1129
1129
  false
1130
1130
  end
1131
1131
 
1132
+ # Denote whether this resource implementation is experiment, ready for
1133
+ # testing, or ready for production use.
1134
+ def self.quality
1135
+ MU::Cloud::RELEASE
1136
+ end
1137
+
1132
1138
  # Remove all VPC resources associated with the currently loaded deployment.
1133
1139
  # @param noop [Boolean]: If true, will only print what would be done
1134
1140
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -1440,10 +1446,28 @@ module MU
1440
1446
  ok
1441
1447
  end
1442
1448
 
1449
+ # Remove all network interfaces associated with the currently loaded deployment.
1450
+ # @param noop [Boolean]: If true, will only print what would be done
1451
+ # @param tagfilters [Array<Hash>]: EC2 tags to filter against when search for resources to purge
1452
+ # @param region [String]: The cloud provider region
1453
+ # @return [void]
1454
+ def self.purge_interfaces(noop = false, tagfilters = [{name: "tag:MU-ID", values: [MU.deploy_id]}], region: MU.curRegion, credentials: nil)
1455
+ resp = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_network_interfaces(
1456
+ filters: tagfilters
1457
+ )
1458
+ ifaces = resp.data.network_interfaces
1459
+
1460
+ return if ifaces.nil? or ifaces.size == 0
1461
+
1462
+ ifaces.each { |iface|
1463
+ MU.log "Deleting Network Interface #{iface.network_interface_id}"
1464
+ MU::Cloud::AWS.ec2(credentials: credentials, region: region).delete_network_interface(network_interface_id: iface.network_interface_id)
1465
+ }
1466
+ end
1467
+
1443
1468
 
1444
1469
  private
1445
1470
 
1446
-
1447
1471
  # List the route tables for each subnet in the given VPC
1448
1472
  def self.listAllSubnetRouteTables(vpc_id, region: MU.curRegion, credentials: nil)
1449
1473
  resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_subnets(
@@ -28,6 +28,7 @@ module MU
28
28
  @@my_hosted_cfg = nil
29
29
  @@authorizers = {}
30
30
  @@acct_to_profile_map = {}
31
+ @@enable_semaphores = {}
31
32
 
32
33
  # Any cloud-specific instance methods we require our resource
33
34
  # implementations to have, above and beyond the ones specified by
@@ -71,6 +72,36 @@ module MU
71
72
  $MU_CFG['google'].keys
72
73
  end
73
74
 
75
+ # A shortcut for {MU::MommaCat.findStray} to resolve a shorthand project
76
+ # name into a cloud object, whether it refers to a sibling by internal
77
+ # name or by cloud identifier.
78
+ # @param name [String]
79
+ # @param deploy [String]
80
+ # @param raise_on_fail [Boolean]
81
+ # @param sibling_only [Boolean]
82
+ # @return [MU::Cloud::Habitat,nil]
83
+ def self.projectLookup(name, deploy = MU.mommacat, raise_on_fail: true, sibling_only: false)
84
+ project_obj = deploy.findLitterMate(type: "habitats", name: name)
85
+
86
+ if !project_obj and !sibling_only
87
+ resp = MU::MommaCat.findStray(
88
+ "Google",
89
+ "habitats",
90
+ deploy_id: deploy.deploy_id,
91
+ cloud_id: name,
92
+ name: name,
93
+ dummy_ok: true
94
+ )
95
+ project_obj = resp.first if resp
96
+ end
97
+
98
+ if (!project_obj or !project_obj.cloud_id) and raise_on_fail
99
+ raise MuError, "Failed to find project '#{name}' in deploy #{deploy.deploy_id}"
100
+ end
101
+
102
+ project_obj
103
+ end
104
+
74
105
  # Resolve the administrative Cloud Storage bucket for a given credential
75
106
  # set, or return a default.
76
107
  # @param credentials [String]
@@ -211,7 +242,7 @@ module MU
211
242
 
212
243
  [name, "log_vol_ebs_key"].each { |obj|
213
244
  MU.log "Granting #{acct} access to #{obj} in Cloud Storage bucket #{adminBucketName(credentials)}"
214
- pp aclobj
245
+
215
246
  MU::Cloud::Google.storage(credentials: credentials).insert_object_access_control(
216
247
  adminBucketName(credentials),
217
248
  obj,
@@ -323,6 +354,10 @@ module MU
323
354
 
324
355
  cfg = credConfig(credentials)
325
356
 
357
+ if cfg['project']
358
+ @@enable_semaphores[cfg['project']] ||= Mutex.new
359
+ end
360
+
326
361
  if cfg
327
362
  data = nil
328
363
  @@authorizers[credentials] ||= {}
@@ -597,7 +632,8 @@ module MU
597
632
  require 'google/apis/cloudresourcemanager_v1'
598
633
 
599
634
  if subclass.nil?
600
- @@resource_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV1::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
635
+ # @@resource_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV1::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/cloudplatformprojects'], masquerade: MU::Cloud::Google.credConfig(credentials)['masquerade_as'], credentials: credentials)
636
+ @@resource_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV1::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/cloudplatformprojects'], credentials: credentials)
601
637
  return @@resource_api[credentials]
602
638
  elsif subclass.is_a?(Symbol)
603
639
  return Object.const_get("::Google").const_get("Apis").const_get("CloudresourcemanagerV1").const_get(subclass)
@@ -605,15 +641,15 @@ module MU
605
641
  end
606
642
 
607
643
  # Google's Cloud Resource Manager API V2, which apparently has all the folder bits
608
- # @param subclass [<Google::Apis::CloudresourcemanagerV2beta1>]: If specified, will return the class ::Google::Apis::CloudresourcemanagerV2beta1::subclass instead of an API client instance
644
+ # @param subclass [<Google::Apis::CloudresourcemanagerV2>]: If specified, will return the class ::Google::Apis::CloudresourcemanagerV2::subclass instead of an API client instance
609
645
  def self.folder(subclass = nil, credentials: nil)
610
- require 'google/apis/cloudresourcemanager_v2beta1'
646
+ require 'google/apis/cloudresourcemanager_v2'
611
647
 
612
648
  if subclass.nil?
613
- @@resource2_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV2beta1::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
649
+ @@resource2_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudresourcemanagerV2::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/cloudplatformfolders'], credentials: credentials)
614
650
  return @@resource2_api[credentials]
615
651
  elsif subclass.is_a?(Symbol)
616
- return Object.const_get("::Google").const_get("Apis").const_get("CloudresourcemanagerV2beta1").const_get(subclass)
652
+ return Object.const_get("::Google").const_get("Apis").const_get("CloudresourcemanagerV2").const_get(subclass)
617
653
  end
618
654
  end
619
655
 
@@ -682,6 +718,31 @@ module MU
682
718
  end
683
719
  end
684
720
 
721
+ # Google's Cloud Billing Service API
722
+ # @param subclass [<Google::Apis::LoggingV2>]: If specified, will return the class ::Google::Apis::LoggingV2::subclass instead of an API client instance
723
+ def self.billing(subclass = nil, credentials: nil)
724
+ require 'google/apis/cloudbilling_v1'
725
+
726
+ if subclass.nil?
727
+ @@billing_api[credentials] ||= MU::Cloud::Google::GoogleEndpoint.new(api: "CloudbillingV1::CloudbillingService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
728
+ return @@billing_api[credentials]
729
+ elsif subclass.is_a?(Symbol)
730
+ return Object.const_get("::Google").const_get("Apis").const_get("CloudbillingV1").const_get(subclass)
731
+ end
732
+ end
733
+
734
+
735
+ # Retrieve the organization, if any, to which these credentials belong.
736
+ # @param credentials [String]
737
+ # @return [Array<OpenStruct>],nil]
738
+ def self.getOrg(credentials = nil)
739
+ resp = MU::Cloud::Google.resource_manager(credentials: credentials).search_organizations
740
+ if resp and resp.organizations
741
+ # XXX no idea if it's possible to be a member of multiple orgs
742
+ return resp.organizations.first
743
+ end
744
+ nil
745
+ end
685
746
 
686
747
  private
687
748
 
@@ -762,7 +823,7 @@ module MU
762
823
  end
763
824
  # TODO validate that the resource actually went away, because it seems not to do so very reliably
764
825
  rescue ::Google::Apis::ClientError => e
765
- raise e if !e.message.match(/^notFound: /)
826
+ raise e if !e.message.match(/(^notFound: |operation in progress)/)
766
827
  end while failed and retries < 6
767
828
  end
768
829
  }
@@ -798,30 +859,37 @@ module MU
798
859
  else
799
860
  raise MU::MuError, "Service account #{MU::Cloud::Google.svc_account_name} has insufficient privileges to call #{method_sym}"
800
861
  end
801
- rescue ::Google::Apis::ClientError => e
862
+ rescue ::Google::Apis::ClientError, OpenSSL::SSL::SSLError => e
802
863
  if e.message.match(/^invalidParameter:/)
803
864
  MU.log "#{method_sym.to_s}: "+e.message, MU::ERR, details: arguments
804
865
  # uncomment for debugging stuff; this can occur in benign situations so we don't normally want it logging
805
866
  elsif e.message.match(/^forbidden:/)
806
867
  MU.log "Using credentials #{@credentials}: #{method_sym.to_s}: "+e.message, MU::ERR, details: caller
807
868
  end
808
- if retries <= 1 and e.message.match(/^accessNotConfigured/)
869
+ @@enable_semaphores ||= {}
870
+ max_retries = 3
871
+ wait_time = 90
872
+ if retries <= max_retries and e.message.match(/^accessNotConfigured/)
809
873
  enable_obj = nil
810
874
  project = arguments.size > 0 ? arguments.first.to_s : MU::Cloud::Google.defaultProject(@credentials)
875
+ @@enable_semaphores[project] ||= Mutex.new
811
876
  enable_obj = MU::Cloud::Google.service_manager(:EnableServiceRequest).new(
812
877
  consumer_id: "project:"+project
813
878
  )
814
879
  # XXX dumbass way to get this string
815
- e.message.match(/Enable it by visiting https:\/\/console\.developers\.google\.com\/apis\/api\/(.+?)\//)
880
+ e.message.match(/by visiting https:\/\/console\.developers\.google\.com\/apis\/api\/(.+?)\//)
881
+
816
882
  svc_name = Regexp.last_match[1]
817
883
  save_verbosity = MU.verbosity
818
884
  if svc_name != "servicemanagement.googleapis.com"
819
- MU.setLogging(MU::Logger::NORMAL)
820
- MU.log "Attempting to enable #{svc_name} in project #{project}, then waiting for 30s", MU::WARN
821
- MU.setLogging(save_verbosity)
822
- MU::Cloud::Google.service_manager(credentials: @credentials).enable_service(svc_name, enable_obj)
823
- sleep 30
824
885
  retries += 1
886
+ @@enable_semaphores[project].synchronize {
887
+ MU.setLogging(MU::Logger::NORMAL)
888
+ MU.log "Attempting to enable #{svc_name} in project #{project}; will retry #{method_sym.to_s} in #{(wait_time/retries).to_s}s (#{retries.to_s}/#{max_retries.to_s})", MU::NOTICE
889
+ MU.setLogging(save_verbosity)
890
+ MU::Cloud::Google.service_manager(credentials: @credentials).enable_service(svc_name, enable_obj)
891
+ }
892
+ sleep wait_time/retries
825
893
  retry
826
894
  else
827
895
  MU.setLogging(MU::Logger::NORMAL)
@@ -831,7 +899,8 @@ module MU
831
899
  end
832
900
  elsif retries <= 10 and
833
901
  e.message.match(/^resourceNotReady:/) or
834
- (e.message.match(/^resourceInUseByAnotherResource:/) and method_sym.to_s.match(/^delete_/))
902
+ (e.message.match(/^resourceInUseByAnotherResource:/) and method_sym.to_s.match(/^delete_/)) or
903
+ e.message.match(/SSL_connect/)
835
904
  if retries > 0 and retries % 3 == 0
836
905
  MU.log "Will retry #{method_sym} after #{e.message} (retry #{retries})", MU::NOTICE, details: arguments
837
906
  else
@@ -968,6 +1037,7 @@ module MU
968
1037
  @@service_api = {}
969
1038
  @@firestore_api = {}
970
1039
  @@admin_directory_api = {}
1040
+ @@billing_api = {}
971
1041
  end
972
1042
  end
973
1043
  end