cloud-mu 1.9.0.pre.beta → 2.0.0.pre.alpha

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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/Berksfile +16 -54
  3. data/Berksfile.lock +14 -62
  4. data/bin/mu-aws-setup +131 -108
  5. data/bin/mu-configure +311 -74
  6. data/bin/mu-gcp-setup +84 -62
  7. data/bin/mu-load-config.rb +46 -2
  8. data/bin/mu-self-update +11 -9
  9. data/bin/mu-upload-chef-artifacts +4 -4
  10. data/{mu.gemspec → cloud-mu.gemspec} +2 -2
  11. data/cookbooks/awscli/Berksfile +8 -0
  12. data/cookbooks/mu-activedirectory/Berksfile +11 -0
  13. data/cookbooks/mu-firewall/Berksfile +9 -0
  14. data/cookbooks/mu-firewall/metadata.rb +1 -1
  15. data/cookbooks/mu-glusterfs/Berksfile +10 -0
  16. data/cookbooks/mu-jenkins/Berksfile +14 -0
  17. data/cookbooks/mu-master/Berksfile +23 -0
  18. data/cookbooks/mu-master/attributes/default.rb +1 -1
  19. data/cookbooks/mu-master/metadata.rb +2 -2
  20. data/cookbooks/mu-master/recipes/default.rb +1 -1
  21. data/cookbooks/mu-master/recipes/init.rb +7 -3
  22. data/cookbooks/mu-master/recipes/ssl-certs.rb +1 -0
  23. data/cookbooks/mu-mongo/Berksfile +10 -0
  24. data/cookbooks/mu-openvpn/Berksfile +11 -0
  25. data/cookbooks/mu-php54/Berksfile +13 -0
  26. data/cookbooks/mu-splunk/Berksfile +10 -0
  27. data/cookbooks/mu-tools/Berksfile +21 -0
  28. data/cookbooks/mu-tools/files/default/Mu_CA.pem +15 -15
  29. data/cookbooks/mu-utility/Berksfile +9 -0
  30. data/cookbooks/mu-utility/metadata.rb +2 -1
  31. data/cookbooks/nagios/Berksfile +7 -4
  32. data/cookbooks/s3fs/Berksfile +9 -0
  33. data/environments/dev.json +6 -6
  34. data/environments/prod.json +6 -6
  35. data/modules/mu.rb +20 -42
  36. data/modules/mu/cleanup.rb +102 -100
  37. data/modules/mu/cloud.rb +90 -28
  38. data/modules/mu/clouds/aws.rb +449 -218
  39. data/modules/mu/clouds/aws/alarm.rb +29 -17
  40. data/modules/mu/clouds/aws/cache_cluster.rb +78 -64
  41. data/modules/mu/clouds/aws/collection.rb +25 -18
  42. data/modules/mu/clouds/aws/container_cluster.rb +73 -66
  43. data/modules/mu/clouds/aws/database.rb +124 -116
  44. data/modules/mu/clouds/aws/dnszone.rb +27 -20
  45. data/modules/mu/clouds/aws/firewall_rule.rb +30 -22
  46. data/modules/mu/clouds/aws/folder.rb +18 -3
  47. data/modules/mu/clouds/aws/function.rb +77 -23
  48. data/modules/mu/clouds/aws/group.rb +19 -12
  49. data/modules/mu/clouds/aws/habitat.rb +153 -0
  50. data/modules/mu/clouds/aws/loadbalancer.rb +59 -52
  51. data/modules/mu/clouds/aws/log.rb +30 -23
  52. data/modules/mu/clouds/aws/msg_queue.rb +29 -20
  53. data/modules/mu/clouds/aws/notifier.rb +222 -0
  54. data/modules/mu/clouds/aws/role.rb +178 -90
  55. data/modules/mu/clouds/aws/search_domain.rb +40 -24
  56. data/modules/mu/clouds/aws/server.rb +169 -137
  57. data/modules/mu/clouds/aws/server_pool.rb +60 -83
  58. data/modules/mu/clouds/aws/storage_pool.rb +59 -31
  59. data/modules/mu/clouds/aws/user.rb +36 -27
  60. data/modules/mu/clouds/aws/userdata/linux.erb +101 -93
  61. data/modules/mu/clouds/aws/vpc.rb +250 -189
  62. data/modules/mu/clouds/azure.rb +132 -0
  63. data/modules/mu/clouds/cloudformation.rb +65 -1
  64. data/modules/mu/clouds/cloudformation/alarm.rb +8 -0
  65. data/modules/mu/clouds/cloudformation/cache_cluster.rb +7 -0
  66. data/modules/mu/clouds/cloudformation/collection.rb +7 -0
  67. data/modules/mu/clouds/cloudformation/database.rb +7 -0
  68. data/modules/mu/clouds/cloudformation/dnszone.rb +7 -0
  69. data/modules/mu/clouds/cloudformation/firewall_rule.rb +9 -2
  70. data/modules/mu/clouds/cloudformation/loadbalancer.rb +7 -0
  71. data/modules/mu/clouds/cloudformation/log.rb +7 -0
  72. data/modules/mu/clouds/cloudformation/server.rb +7 -0
  73. data/modules/mu/clouds/cloudformation/server_pool.rb +7 -0
  74. data/modules/mu/clouds/cloudformation/vpc.rb +7 -0
  75. data/modules/mu/clouds/google.rb +214 -110
  76. data/modules/mu/clouds/google/container_cluster.rb +42 -24
  77. data/modules/mu/clouds/google/database.rb +15 -6
  78. data/modules/mu/clouds/google/firewall_rule.rb +17 -25
  79. data/modules/mu/clouds/google/group.rb +13 -5
  80. data/modules/mu/clouds/google/habitat.rb +105 -0
  81. data/modules/mu/clouds/google/loadbalancer.rb +28 -20
  82. data/modules/mu/clouds/google/server.rb +93 -354
  83. data/modules/mu/clouds/google/server_pool.rb +18 -10
  84. data/modules/mu/clouds/google/user.rb +22 -14
  85. data/modules/mu/clouds/google/vpc.rb +97 -69
  86. data/modules/mu/config.rb +133 -38
  87. data/modules/mu/config/alarm.rb +25 -0
  88. data/modules/mu/config/cache_cluster.rb +5 -3
  89. data/modules/mu/config/cache_cluster.yml +23 -0
  90. data/modules/mu/config/database.rb +25 -16
  91. data/modules/mu/config/database.yml +3 -3
  92. data/modules/mu/config/function.rb +1 -2
  93. data/modules/mu/config/{project.rb → habitat.rb} +10 -10
  94. data/modules/mu/config/notifier.rb +85 -0
  95. data/modules/mu/config/notifier.yml +9 -0
  96. data/modules/mu/config/role.rb +1 -1
  97. data/modules/mu/config/search_domain.yml +2 -2
  98. data/modules/mu/config/server.rb +13 -1
  99. data/modules/mu/config/server.yml +3 -3
  100. data/modules/mu/config/server_pool.rb +3 -1
  101. data/modules/mu/config/storage_pool.rb +3 -1
  102. data/modules/mu/config/storage_pool.yml +19 -0
  103. data/modules/mu/config/vpc.rb +70 -8
  104. data/modules/mu/groomers/chef.rb +2 -3
  105. data/modules/mu/kittens.rb +500 -122
  106. data/modules/mu/master.rb +5 -5
  107. data/modules/mu/mommacat.rb +151 -91
  108. data/modules/tests/super_complex_bok.yml +12 -0
  109. data/modules/tests/super_simple_bok.yml +12 -0
  110. data/spec/mu/clouds/azure_spec.rb +82 -0
  111. data/spec/spec_helper.rb +105 -0
  112. metadata +26 -5
  113. data/modules/mu/clouds/aws/notification.rb +0 -139
  114. data/modules/mu/config/notification.rb +0 -44
@@ -0,0 +1,132 @@
1
+ # Copyright:: Copyright (c) 2018 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
+ require "net/http"
16
+ require 'net/https'
17
+ require 'multi_json'
18
+ require 'stringio'
19
+
20
+ module MU
21
+ class Cloud
22
+ # Support for Microsoft Azure as a provisioning layer.
23
+ class Azure
24
+ @@is_in_azure = nil
25
+
26
+ # Alias for #{MU::Cloud::AWS.hosted?}
27
+ def self.hosted
28
+ MU::Cloud::Azure.hosted?
29
+ end
30
+
31
+ # Determine whether we (the Mu master, presumably) are hosted in Azure.
32
+ # @return [Boolean]
33
+ def self.hosted?
34
+ if !@@is_in_azure.nil?
35
+ return @@is_in_azure
36
+ end
37
+
38
+ begin
39
+ metadata = get_metadata()
40
+ if metadata['compute']['vmId']
41
+ @@is_in_azure = true
42
+ return true
43
+ else
44
+ return false
45
+ end
46
+ rescue
47
+ # MU.log "Failed to get Azure MetaData. I assume I am not hosted in Azure", MU::DEBUG, details: resources
48
+ end
49
+
50
+ @@is_in_azure = false
51
+ false
52
+ end
53
+
54
+ def self.hosted_config
55
+ "TODO"
56
+ end
57
+
58
+ # Any cloud-specific instance methods we require our resource implementations to have, above and beyond the ones specified by {MU::Cloud}
59
+ # @return [Array<Symbol>]
60
+ def self.required_instance_methods
61
+ []
62
+ end
63
+
64
+ def self.myRegion
65
+ "TODO"
66
+ end
67
+
68
+ def self.listRegions
69
+ "TODO"
70
+ end
71
+
72
+ def self.listAZs
73
+ "TODO"
74
+ end
75
+
76
+ def self.config_example
77
+ "TODO"
78
+ end
79
+
80
+ def self.writeDeploySecret
81
+ "TODO"
82
+ end
83
+
84
+ def self.listCredentials
85
+ "TODO"
86
+ end
87
+
88
+ def self.credConfig
89
+ "TODO"
90
+ end
91
+
92
+ def self.listInstanceTypes
93
+ "TODO"
94
+ end
95
+
96
+ def self.adminBucketName(credentials = nil)
97
+ "TODO"
98
+ end
99
+
100
+ def self.adminBucketUrl(credentials = nil)
101
+ "TODO"
102
+ end
103
+
104
+ #END REQUIRED METHODS
105
+
106
+
107
+ # Fetch an Azure instance metadata parameter (example: public-ipv4).
108
+ # @param param [String]: The parameter name to fetch
109
+ # @return [String, nil]
110
+ def self.get_metadata()
111
+ base_url = "http://169.254.169.254/metadata/instance"
112
+ api_version = '2017-12-01'
113
+ begin
114
+ response = nil
115
+ Timeout.timeout(1) do
116
+ response = MultiJson.load(open("#{base_url}/?api-version=#{ api_version }", "Metadata" => "true").read)
117
+ end
118
+
119
+ response
120
+ rescue OpenURI::HTTPError, Timeout::Error, SocketError, Errno::ENETUNREACH, Net::HTTPServerException, Errno::EHOSTUNREACH => e
121
+ # This is normal on machines checking to see if they're AWS-hosted
122
+ logger = MU::Logger.new
123
+ logger.log "Failed metadata request #{base_url}/: #{e.inspect}", MU::DEBUG
124
+ return nil
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+
132
+
@@ -36,6 +36,64 @@ module MU
36
36
  @@cloudformation_mode
37
37
  end
38
38
 
39
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
40
+ # environment. See {MU::Cloud::AWS.hosted_config} instead.
41
+ def self.hosted_config
42
+ nil
43
+ end
44
+
45
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
46
+ # environment. See {MU::Cloud::AWS.credConfig} instead.
47
+ def self.credConfig(name = nil, name_only: false)
48
+ nil
49
+ end
50
+
51
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
52
+ # environment. See {MU::Cloud::AWS.listCredentials} instead.
53
+ def self.listCredentials
54
+ nil
55
+ end
56
+
57
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
58
+ # environment. Calls {MU::Cloud::AWS.listInstanceTypes} to return sensible
59
+ # values, if we happen to have AWS credentials configured.
60
+ def self.listInstanceTypes(region = myRegion)
61
+ MU::Cloud::AWS.listRegions(region)
62
+ end
63
+
64
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
65
+ # environment. Calls {MU::Cloud::AWS.listAZs} to return sensible
66
+ # values, if we happen to have AWS credentials configured.
67
+ def self.listAZs(region: MU.curRegion, account: nil, credentials: nil)
68
+ MU::Cloud::AWS.listAZs(region: region, account: account, credentials: credentials)
69
+ end
70
+
71
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
72
+ # environment. Calls {MU::Cloud::AWS.listRegions} to return sensible
73
+ # values, if we happen to have AWS credentials configured.
74
+ def self.listRegions(us_only = false, credentials: nil)
75
+ MU::Cloud::AWS.listRegions(us_only, credentials: credentials)
76
+ end
77
+
78
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
79
+ # environment. Calls {MU::Cloud::AWS.myRegion} to return sensible
80
+ # values, if we happen to have AWS credentials configured.
81
+ def self.myRegion
82
+ MU::Cloud::AWS.myRegion
83
+ end
84
+
85
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
86
+ # environment. See {MU::Cloud::AWS.adminBucketName} instead.
87
+ def self.adminBucketName(credentials = nil)
88
+ nil
89
+ end
90
+
91
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
92
+ # environment. See {MU::Cloud::AWS.adminBucketUrl} instead.
93
+ def self.adminBucketUrl(credentials = nil)
94
+ nil
95
+ end
96
+
39
97
  # Stub method- there's no such thing as being "hosted" in a CloudFormation
40
98
  # environment. See {MU::Cloud::AWS.hosted?} instead.
41
99
  def self.hosted?
@@ -43,11 +101,17 @@ module MU
43
101
  end
44
102
 
45
103
  # Stub method- there's no such thing as being "hosted" in a CloudFormation
46
- # environment. See {MU::Cloud::AWS.hosted?} instead.
104
+ # environment. See {MU::Cloud::AWS.config_example} instead.
47
105
  def self.config_example
48
106
  nil
49
107
  end
50
108
 
109
+ # Stub method- there's no such thing as being "hosted" in a CloudFormation
110
+ # environment. See {MU::Cloud::AWS.writeDeploySecret} instead.
111
+ def self.writeDeploySecret(deploy_id, value, name = nil, credentials: nil)
112
+ nil
113
+ end
114
+
51
115
  # Generate and return a skeletal CloudFormation resource entry for the
52
116
  # caller.
53
117
  # param type [String]: The resource type, in Mu parlance
@@ -140,6 +140,14 @@ module MU
140
140
  MU::Cloud::AWS::Alarm.validateConfig(server, configurator)
141
141
  end
142
142
 
143
+ # Does this resource type exist as a global (cloud-wide) artifact, or
144
+ # is it localized to a region/zone?
145
+ # @return [Boolean]
146
+ def self.isGlobal?
147
+ MU::Cloud::AWS::Alarm.isGlobal?
148
+ end
149
+
150
+
143
151
  end
144
152
  end
145
153
  end
@@ -161,6 +161,13 @@ module MU
161
161
  MU::Cloud::AWS::CacheCluster.validateConfig(server, configurator)
162
162
  end
163
163
 
164
+ # Does this resource type exist as a global (cloud-wide) artifact, or
165
+ # is it localized to a region/zone?
166
+ # @return [Boolean]
167
+ def self.isGlobal?
168
+ MU::Cloud::AWS::CacheCluster.isGlobal?
169
+ end
170
+
164
171
  end
165
172
  end
166
173
  end
@@ -111,6 +111,13 @@ module MU
111
111
  MU::Cloud::AWS::Collection.validateConfig(server, configurator)
112
112
  end
113
113
 
114
+ # Does this resource type exist as a global (cloud-wide) artifact, or
115
+ # is it localized to a region/zone?
116
+ # @return [Boolean]
117
+ def self.isGlobal?
118
+ MU::Cloud::AWS::Collection.isGlobal?
119
+ end
120
+
114
121
  end
115
122
  end
116
123
  end
@@ -271,6 +271,13 @@ module MU
271
271
  MU::Cloud::AWS::Database.validateConfig(server, configurator)
272
272
  end
273
273
 
274
+ # Does this resource type exist as a global (cloud-wide) artifact, or
275
+ # is it localized to a region/zone?
276
+ # @return [Boolean]
277
+ def self.isGlobal?
278
+ MU::Cloud::AWS::Database.isGlobal?
279
+ end
280
+
274
281
 
275
282
  end #class
276
283
  end #class
@@ -268,6 +268,13 @@ module MU
268
268
  MU::Cloud::AWS::DNSZone.validateConfig(server, configurator)
269
269
  end
270
270
 
271
+ # Does this resource type exist as a global (cloud-wide) artifact, or
272
+ # is it localized to a region/zone?
273
+ # @return [Boolean]
274
+ def self.isGlobal?
275
+ MU::Cloud::AWS::DNSZone.isGlobal?
276
+ end
277
+
271
278
  end
272
279
  end
273
280
  end
@@ -116,12 +116,12 @@ module MU
116
116
 
117
117
  begin
118
118
  if egress
119
- MU::Cloud::AWS.ec2(@config['region']).authorize_security_group_egress(
119
+ MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).authorize_security_group_egress(
120
120
  group_id: @cloud_id,
121
121
  ip_permissions: ec2_rule
122
122
  )
123
123
  else
124
- MU::Cloud::AWS.ec2(@config['region']).authorize_security_group_ingress(
124
+ MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).authorize_security_group_ingress(
125
125
  group_id: @cloud_id,
126
126
  ip_permissions: ec2_rule
127
127
  )
@@ -302,6 +302,13 @@ module MU
302
302
  MU::Cloud::AWS::FirewallRule.validateConfig(server, configurator)
303
303
  end
304
304
 
305
+ # Does this resource type exist as a global (cloud-wide) artifact, or
306
+ # is it localized to a region/zone?
307
+ # @return [Boolean]
308
+ def self.isGlobal?
309
+ MU::Cloud::AWS::FirewallRule.isGlobal?
310
+ end
311
+
305
312
  end #class
306
313
  end #class
307
314
  end
@@ -187,6 +187,13 @@ module MU
187
187
  MU::Cloud::AWS::LoadBalancer.validateConfig(server, configurator)
188
188
  end
189
189
 
190
+ # Does this resource type exist as a global (cloud-wide) artifact, or
191
+ # is it localized to a region/zone?
192
+ # @return [Boolean]
193
+ def self.isGlobal?
194
+ MU::Cloud::AWS::LoadBalancer.isGlobal?
195
+ end
196
+
190
197
  end
191
198
  end
192
199
  end
@@ -164,6 +164,13 @@ module MU
164
164
  MU::Cloud::AWS::Log.validateConfig(server, configurator)
165
165
  end
166
166
 
167
+ # Does this resource type exist as a global (cloud-wide) artifact, or
168
+ # is it localized to a region/zone?
169
+ # @return [Boolean]
170
+ def self.isGlobal?
171
+ MU::Cloud::AWS::Log.isGlobal?
172
+ end
173
+
167
174
  end
168
175
  end
169
176
  end
@@ -364,6 +364,13 @@ module MU
364
364
  MU::Cloud::AWS::Server.validateConfig(server, configurator)
365
365
  end
366
366
 
367
+ # Does this resource type exist as a global (cloud-wide) artifact, or
368
+ # is it localized to a region/zone?
369
+ # @return [Boolean]
370
+ def self.isGlobal?
371
+ MU::Cloud::AWS::Server.isGlobal?
372
+ end
373
+
367
374
  end #class
368
375
  end #class
369
376
  end
@@ -273,6 +273,13 @@ module MU
273
273
  MU::Cloud::AWS::ServerPool.validateConfig(server, configurator)
274
274
  end
275
275
 
276
+ # Does this resource type exist as a global (cloud-wide) artifact, or
277
+ # is it localized to a region/zone?
278
+ # @return [Boolean]
279
+ def self.isGlobal?
280
+ MU::Cloud::AWS::ServerPool.isGlobal?
281
+ end
282
+
276
283
  end
277
284
  end
278
285
  end
@@ -316,6 +316,13 @@ module MU
316
316
  MU::Cloud::AWS::VPC.validateConfig(server, configurator)
317
317
  end
318
318
 
319
+ # Does this resource type exist as a global (cloud-wide) artifact, or
320
+ # is it localized to a region/zone?
321
+ # @return [Boolean]
322
+ def self.isGlobal?
323
+ MU::Cloud::AWS::VPC.isGlobal?
324
+ end
325
+
319
326
  end #class
320
327
  end #class
321
328
  end
@@ -26,6 +26,7 @@ module MU
26
26
  @@default_project = nil
27
27
  @@myRegion_var = nil
28
28
  @@authorizers = {}
29
+ @@acct_to_profile_map = {}
29
30
 
30
31
  # Any cloud-specific instance methods we require our resource
31
32
  # implementations to have, above and beyond the ones specified by
@@ -59,11 +60,76 @@ module MU
59
60
  sample
60
61
  end
61
62
 
63
+ # Return the name strings of all known sets of credentials for this cloud
64
+ # @return [Array<String>]
65
+ def self.listCredentials
66
+ if !$MU_CFG['google']
67
+ return hosted? ? ["#default"] : nil
68
+ end
69
+
70
+ $MU_CFG['google'].keys
71
+ end
72
+
73
+ def self.adminBucketName(credentials = nil)
74
+ #XXX find a default if this particular account doesn't have a log_bucket_name configured
75
+ cfg = credConfig(credentials)
76
+ cfg['log_bucket_name']
77
+ end
78
+
79
+ def self.adminBucketUrl(credentials = nil)
80
+ "gs://"+adminBucketName(credentials)+"/"
81
+ end
82
+
83
+ # Return the $MU_CFG data associated with a particular profile/name/set of
84
+ # credentials. If no account name is specified, will return one flagged as
85
+ # default. Returns nil if GCP is not configured. Throws an exception if
86
+ # an account name is specified which does not exist.
87
+ # @param name [String]: The name of the key under 'aws' in mu.yaml to return
88
+ # @return [Hash,nil]
89
+ def self.credConfig(name = nil, name_only: false)
90
+ # If there's nothing in mu.yaml (which is wrong), but we're running
91
+ # on a machine hosted in GCP, fake it with that machine's service
92
+ # account and hope for the best.
93
+ if !$MU_CFG['google'] or !$MU_CFG['google'].is_a?(Hash) or $MU_CFG['google'].size == 0
94
+ return @@my_hosted_cfg if @@my_hosted_cfg
95
+
96
+ if hosted?
97
+ begin
98
+ # iam_data = JSON.parse(getAWSMetaData("iam/info"))
99
+ # if iam_data["InstanceProfileArn"] and !iam_data["InstanceProfileArn"].empty?
100
+ @@my_hosted_cfg = hosted_config
101
+ return name_only ? "#default" : @@my_hosted_cfg
102
+ # end
103
+ rescue JSON::ParserError => e
104
+ end
105
+ end
106
+
107
+ return nil
108
+ end
109
+
110
+ if name.nil?
111
+ $MU_CFG['google'].each_pair { |name, cfg|
112
+ if cfg['default']
113
+ return name_only ? name : cfg
114
+ end
115
+ }
116
+ else
117
+ if $MU_CFG['google'][name]
118
+ return name_only ? name : $MU_CFG['google'][name]
119
+ elsif @@acct_to_profile_map[name.to_s]
120
+ return name_only ? name : @@acct_to_profile_map[name.to_s]
121
+ end
122
+ # XXX whatever process might lead us to populate @@acct_to_profile_map with some mappings, like projectname -> account profile, goes here
123
+ raise MuError, "Google credential set #{name} was requested, but I see no such working credentials in mu.yaml"
124
+ end
125
+ end
126
+
62
127
  # If we've configured Google as a provider, or are simply hosted in GCP,
63
128
  # decide what our default region is.
64
- def self.myRegion
65
- if $MU_CFG['google'] and $MU_CFG['google']['region']
66
- @@myRegion_var = $MU_CFG['google']['region']
129
+ def self.myRegion(credentials = nil)
130
+ cfg = credConfig(credentials)
131
+ if cfg and cfg['region']
132
+ @@myRegion_var = cfg['region']
67
133
  elsif MU::Cloud::Google.hosted?
68
134
  zone = MU::Cloud::Google.getGoogleMetaData("instance/zone")
69
135
  @@myRegion_var = zone.gsub(/^.*?\/|\-\d+$/, "")
@@ -74,72 +140,93 @@ module MU
74
140
  # Plant a Mu deploy secret into a storage bucket somewhere for so our kittens can consume it
75
141
  # @param deploy_id [String]: The deploy for which we're writing the secret
76
142
  # @param value [String]: The contents of the secret
77
- def self.writeDeploySecret(deploy_id, value)
78
- name = deploy_id+"-secret"
143
+ def self.writeDeploySecret(deploy_id, value, name = nil, credentials: nil)
144
+ name ||= deploy_id+"-secret"
79
145
  begin
80
- MU.log "Writing #{name} to Cloud Storage bucket #{$MU_CFG['google']['log_bucket_name']}"
146
+ MU.log "Writing #{name} to Cloud Storage bucket #{adminBucketName(credentials)}"
147
+
81
148
  f = Tempfile.new(name) # XXX this is insecure and stupid
82
149
  f.write value
83
150
  f.close
84
151
  objectobj = MU::Cloud::Google.storage(:Object).new(
85
- bucket: $MU_CFG['google']['log_bucket_name'],
152
+ bucket: adminBucketName(credentials),
86
153
  name: name
87
154
  )
88
- ebs_key = MU::Cloud::Google.storage.insert_object(
89
- $MU_CFG['google']['log_bucket_name'],
155
+ ebs_key = MU::Cloud::Google.storage(credentials: credentials).insert_object(
156
+ adminBucketName(credentials),
90
157
  objectobj,
91
158
  upload_source: f.path
92
159
  )
93
160
  f.unlink
94
161
  rescue ::Google::Apis::ClientError => e
95
162
  # XXX comment for NCBI tests
96
- # raise MU::MommaCat::DeployInitializeError, "Got #{e.inspect} trying to write #{name} to #{$MU_CFG['google']['log_bucket_name']}"
163
+ # raise MU::MommaCat::DeployInitializeError, "Got #{e.inspect} trying to write #{name} to #{adminBucketName(credentials)}"
97
164
  end
98
165
  end
99
166
 
100
167
  # Remove the service account and various deploy secrets associated with a deployment. Intended for invocation from MU::Cleanup.
101
168
  # @param deploy_id [String]: The deploy for which we're granting the secret
102
169
  # @param noop [Boolean]: If true, will only print what would be done
103
- def self.removeDeploySecretsAndRoles(deploy_id = MU.deploy_id, flags: {}, noop: false)
104
- return if !$MU_CFG['google'] or !$MU_CFG['google']['project']
105
- flags["project"] ||= MU::Cloud::Google.defaultProject
170
+ def self.removeDeploySecretsAndRoles(deploy_id = MU.deploy_id, flags: {}, noop: false, credentials: nil)
171
+ cfg = credConfig(credentials)
172
+ return if !cfg or !cfg['project']
173
+ flags["project"] ||= cfg['project']
106
174
  name = deploy_id+"-secret"
107
-
108
175
  end
109
176
 
110
177
  # Grant access to appropriate Cloud Storage objects in our log/secret bucket for a deploy member.
111
178
  # @param acct [String]: The service account (by email addr) to which we'll grant access
112
179
  # @param deploy_id [String]: The deploy for which we're granting the secret
113
180
  # XXX add equivalent for AWS and call agnostically
114
- def self.grantDeploySecretAccess(acct, deploy_id = MU.deploy_id)
115
- name = deploy_id+"-secret"
181
+ def self.grantDeploySecretAccess(acct, deploy_id = MU.deploy_id, name = nil, credentials: nil)
182
+ name ||= deploy_id+"-secret"
183
+ aclobj = nil
184
+
185
+ retries = 0
116
186
  begin
117
- MU.log "Granting #{acct} access to list Cloud Storage bucket #{$MU_CFG['google']['log_bucket_name']}"
118
- MU::Cloud::Google.storage.insert_bucket_access_control(
119
- $MU_CFG['google']['log_bucket_name'],
187
+ MU.log "Granting #{acct} access to list Cloud Storage bucket #{adminBucketName(credentials)}"
188
+ MU::Cloud::Google.storage(credentials: credentials).insert_bucket_access_control(
189
+ adminBucketName(credentials),
120
190
  MU::Cloud::Google.storage(:BucketAccessControl).new(
121
- bucket: $MU_CFG['google']['log_bucket_name'],
191
+ bucket: adminBucketName(credentials),
122
192
  role: "READER",
123
193
  entity: "user-"+acct
124
194
  )
125
195
  )
126
196
 
127
197
  aclobj = MU::Cloud::Google.storage(:ObjectAccessControl).new(
128
- bucket: $MU_CFG['google']['log_bucket_name'],
198
+ bucket: adminBucketName(credentials),
129
199
  role: "READER",
130
200
  entity: "user-"+acct
131
201
  )
132
202
 
133
203
  [name, "log_vol_ebs_key"].each { |obj|
134
- MU.log "Granting #{acct} access to #{obj} in Cloud Storage bucket #{$MU_CFG['google']['log_bucket_name']}"
135
- MU::Cloud::Google.storage.insert_object_access_control(
136
- $MU_CFG['google']['log_bucket_name'],
204
+ MU.log "Granting #{acct} access to #{obj} in Cloud Storage bucket #{adminBucketName(credentials)}"
205
+ pp aclobj
206
+ MU::Cloud::Google.storage(credentials: credentials).insert_object_access_control(
207
+ adminBucketName(credentials),
137
208
  obj,
138
209
  aclobj
139
210
  )
140
211
  }
141
212
  rescue ::Google::Apis::ClientError => e
142
- raise MuError, "Got #{e.inspect} trying to set ACLs for #{deploy_id} in #{$MU_CFG['google']['log_bucket_name']}"
213
+ if e.inspect.match(/body: "Not Found"/)
214
+ raise MuError, "Google admin bucket #{adminBucketName(credentials)} or key #{name} does not appear to exist or is not visible with #{credentials ? credentials : "default"} credentials"
215
+ elsif e.inspect.match(/notFound: No such object:/)
216
+ if retries < 5
217
+ sleep 5
218
+ retries += 1
219
+ retry
220
+ else
221
+ raise e
222
+ end
223
+ elsif e.inspect.match(/The metadata for object "null" was edited during the operation/)
224
+ MU.log e.message+" - Google admin bucket #{adminBucketName(credentials)}/#{name} with #{credentials ? credentials : "default"} credentials", MU::WARN, details: aclobj
225
+ sleep 10
226
+ retry
227
+ else
228
+ raise MuError, "Got #{e.inspect} trying to set ACLs for #{deploy_id} in #{adminBucketName(credentials)}"
229
+ end
143
230
  end
144
231
  end
145
232
 
@@ -193,8 +280,8 @@ module MU
193
280
  # @param cert [String,OpenSSL::X509::Certificate]: An x509 certificate
194
281
  # @param key [String,OpenSSL::PKey]: An x509 private key
195
282
  # @return [Google::Apis::ComputeBeta::SslCertificate]
196
- def self.createSSLCertificate(name, cert, key, flags = {})
197
- flags["project"] ||= MU::Cloud::Google.defaultProject
283
+ def self.createSSLCertificate(name, cert, key, flags = {}, credentials: nil)
284
+ flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
198
285
  flags["description"] ||= MU.deploy_id
199
286
  certobj = ::Google::Apis::ComputeBeta::SslCertificate.new(
200
287
  name: name,
@@ -202,7 +289,7 @@ module MU
202
289
  private_key: key.to_s,
203
290
  description: flags["description"]
204
291
  )
205
- MU::Cloud::Google.compute.insert_ssl_certificate(flags["project"], certobj)
292
+ MU::Cloud::Google.compute(credentials: credentials).insert_ssl_certificate(flags["project"], certobj)
206
293
  end
207
294
 
208
295
  @@svc_account_name = nil
@@ -216,43 +303,48 @@ module MU
216
303
  # vault, feed them to the googleauth gem, and stash the results on hand
217
304
  # for consumption by the various GCP APIs.
218
305
  # @param scopes [Array<String>]: One or more scopes for which to authorizer the caller. Will vary depending on the API you're calling.
219
- def self.loadCredentials(scopes = nil)
220
- return @@authorizers[scopes.to_s] if @@authorizers[scopes.to_s]
306
+ def self.loadCredentials(scopes = nil, credentials: nil)
307
+ if @@authorizers[credentials] and @@authorizers[credentials][scopes.to_s]
308
+ return @@authorizers[credentials][scopes.to_s]
309
+ end
221
310
 
222
- if $MU_CFG.has_key?("google")
311
+ cfg = credConfig(credentials)
312
+
313
+ if cfg
223
314
  data = nil
315
+ @@authorizers[credentials] ||= {}
224
316
 
225
317
  def self.get_machine_credentials(scopes)
226
318
  @@svc_account_name = MU::Cloud::Google.getGoogleMetaData("instance/service-accounts/default/email")
227
319
  MU.log "We are hosted in GCP, so I will attempt to use the service account #{@@svc_account_name} to make API requests.", MU::DEBUG
228
320
 
229
- @@authorizers[scopes.to_s] = ::Google::Auth.get_application_default(scopes)
230
- @@authorizers[scopes.to_s].fetch_access_token!
321
+ @@authorizers[credentials][scopes.to_s] = ::Google::Auth.get_application_default(scopes)
322
+ @@authorizers[credentials][scopes.to_s].fetch_access_token!
231
323
  @@default_project ||= MU::Cloud::Google.getGoogleMetaData("project/project-id")
232
- @@authorizers[scopes.to_s]
324
+ @@authorizers[credentials][scopes.to_s]
233
325
  end
234
326
 
235
- if $MU_CFG["google"]["credentials_file"]
327
+ if cfg["credentials_file"]
236
328
  begin
237
- data = JSON.parse(File.read($MU_CFG["google"]["credentials_file"]))
329
+ data = JSON.parse(File.read(cfg["credentials_file"]))
238
330
  @@default_project ||= data["project_id"]
239
331
  creds = {
240
332
  :json_key_io => StringIO.new(MultiJson.dump(data)),
241
333
  :scope => scopes
242
334
  }
243
335
  @@svc_account_name = data["client_email"]
244
- @@authorizers[scopes.to_s] = ::Google::Auth::ServiceAccountCredentials.make_creds(creds)
245
- return @@authorizers[scopes.to_s]
336
+ @@authorizers[credentials][scopes.to_s] = ::Google::Auth::ServiceAccountCredentials.make_creds(creds)
337
+ return @@authorizers[credentials][scopes.to_s]
246
338
  rescue JSON::ParserError, Errno::ENOENT, Errno::EACCES => e
247
339
  if !MU::Cloud::Google.hosted?
248
- raise MuError, "Google Cloud credentials file #{$MU_CFG["google"]["credentials_file"]} is missing or invalid (#{e.message})"
340
+ raise MuError, "Google Cloud credentials file #{cfg["credentials_file"]} is missing or invalid (#{e.message})"
249
341
  end
250
- MU.log "Google Cloud credentials file #{$MU_CFG["google"]["credentials_file"]} is missing or invalid", MU::WARN, details: e.message
342
+ MU.log "Google Cloud credentials file #{cfg["credentials_file"]} is missing or invalid", MU::WARN, details: e.message
251
343
  return get_machine_credentials(scopes)
252
344
  end
253
- elsif $MU_CFG["google"]["credentials"]
345
+ elsif cfg["credentials"]
254
346
  begin
255
- vault, item = $MU_CFG["google"]["credentials"].split(/:/)
347
+ vault, item = cfg["credentials"].split(/:/)
256
348
  data = MU::Groomer::Chef.getSecret(vault: vault, item: item).to_h
257
349
  rescue MU::Groomer::Chef::MuNoSuchSecret
258
350
  if !MU::Cloud::Google.hosted?
@@ -268,8 +360,8 @@ module MU
268
360
  :scope => scopes
269
361
  }
270
362
  @@svc_account_name = data["client_email"]
271
- @@authorizers[scopes.to_s] = ::Google::Auth::ServiceAccountCredentials.make_creds(creds)
272
- return @@authorizers[scopes.to_s]
363
+ @@authorizers[credentials][scopes.to_s] = ::Google::Auth::ServiceAccountCredentials.make_creds(creds)
364
+ return @@authorizers[credentials][scopes.to_s]
273
365
  elsif MU::Cloud::Google.hosted?
274
366
  return get_machine_credentials(scopes)
275
367
  else
@@ -318,15 +410,17 @@ module MU
318
410
  # Our credentials map to a project, an organizational structure in Google
319
411
  # Cloud. This fetches the identifier of the project associated with our
320
412
  # default credentials.
321
- def self.defaultProject
322
- return myProject if !$MU_CFG['google'] or !$MU_CFG['google']['project']
323
- loadCredentials if !@@default_project
324
- @@default_project
413
+ def self.defaultProject(credentials = nil)
414
+ cfg = credConfig(credentials)
415
+ return myProject if !cfg or !cfg['project']
416
+ loadCredentials(credentials) if !@@authorizers[credentials]
417
+ cfg['project']
325
418
  end
326
419
 
327
420
  # List all Google Cloud Platform projects available to our credentials
328
- def self.listProjects
329
- return [] if !$MU_CFG['google'] or !$MU_CFG['google']['project']
421
+ def self.listProjects(credentials = nil)
422
+ cfg = credConfig(credentials)
423
+ return [] if !cfg or !cfg['project']
330
424
  result = MU::Cloud::Google.resource_manager.list_projects
331
425
  result.projects.reject! { |p| p.lifecycle_state == "DELETE_REQUESTED" }
332
426
  result.projects.map { |p| p.project_id }
@@ -335,13 +429,13 @@ module MU
335
429
  @@regions = {}
336
430
  # List all known Google Cloud Platform regions
337
431
  # @param us_only [Boolean]: Restrict results to United States only
338
- def self.listRegions(us_only = false)
339
- if !MU::Cloud::Google.defaultProject
432
+ def self.listRegions(us_only = false, credentials: nil)
433
+ if !MU::Cloud::Google.defaultProject(credentials)
340
434
  return []
341
435
  end
342
436
  if @@regions.size == 0
343
437
  begin
344
- result = MU::Cloud::Google.compute.list_regions(MU::Cloud::Google.defaultProject)
438
+ result = MU::Cloud::Google.compute(credentials: credentials).list_regions(MU::Cloud::Google.defaultProject(credentials))
345
439
  rescue ::Google::Apis::ClientError => e
346
440
  if e.message.match(/forbidden/)
347
441
  raise MuError, "Insufficient permissions to list Google Cloud region. The service account #{myServiceAccount} should probably have the project owner role."
@@ -366,7 +460,7 @@ module MU
366
460
 
367
461
 
368
462
  @@instance_types = nil
369
- # Query the GCP API for the list of valid EC2 instance types and some of
463
+ # Query the GCP API for the list of valid Compute instance types and some of
370
464
  # their attributes. We can use this in config validation and to help
371
465
  # "translate" machine types across cloud providers.
372
466
  # @param region [String]: Supported machine types can vary from region to region, so we look for the set we're interested in specifically
@@ -412,12 +506,12 @@ module MU
412
506
 
413
507
  # Google's Compute Service API
414
508
  # @param subclass [<Google::Apis::ComputeBeta>]: If specified, will return the class ::Google::Apis::ComputeBeta::subclass instead of an API client instance
415
- def self.compute(subclass = nil)
509
+ def self.compute(subclass = nil, credentials: nil)
416
510
  require 'google/apis/compute_beta'
417
511
 
418
512
  if subclass.nil?
419
- @@compute_api ||= MU::Cloud::Google::Endpoint.new(api: "ComputeBeta::ComputeService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/compute.readonly'])
420
- return @@compute_api
513
+ @@compute_api[credentials] ||= MU::Cloud::Google::Endpoint.new(api: "ComputeBeta::ComputeService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/compute.readonly'], credentials: credentials)
514
+ return @@compute_api[credentials]
421
515
  elsif subclass.is_a?(Symbol)
422
516
  return Object.const_get("::Google").const_get("Apis").const_get("ComputeBeta").const_get(subclass)
423
517
  end
@@ -425,12 +519,12 @@ module MU
425
519
 
426
520
  # Google's Storage Service API
427
521
  # @param subclass [<Google::Apis::StorageV1>]: If specified, will return the class ::Google::Apis::StorageV1::subclass instead of an API client instance
428
- def self.storage(subclass = nil)
522
+ def self.storage(subclass = nil, credentials: nil)
429
523
  require 'google/apis/storage_v1'
430
524
 
431
525
  if subclass.nil?
432
- @@storage_api ||= MU::Cloud::Google::Endpoint.new(api: "StorageV1::StorageService", scopes: ['https://www.googleapis.com/auth/cloud-platform'])
433
- return @@storage_api
526
+ @@storage_api[credentials] ||= MU::Cloud::Google::Endpoint.new(api: "StorageV1::StorageService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
527
+ return @@storage_api[credentials]
434
528
  elsif subclass.is_a?(Symbol)
435
529
  return Object.const_get("::Google").const_get("Apis").const_get("StorageV1").const_get(subclass)
436
530
  end
@@ -438,12 +532,12 @@ module MU
438
532
 
439
533
  # Google's IAM Service API
440
534
  # @param subclass [<Google::Apis::IamV1>]: If specified, will return the class ::Google::Apis::IamV1::subclass instead of an API client instance
441
- def self.iam(subclass = nil)
535
+ def self.iam(subclass = nil, credentials: nil)
442
536
  require 'google/apis/iam_v1'
443
537
 
444
538
  if subclass.nil?
445
- @@iam_api ||= MU::Cloud::Google::Endpoint.new(api: "IamV1::IamService", scopes: ['https://www.googleapis.com/auth/cloud-platform'])
446
- return @@iam_api
539
+ @@iam_api[credentials] ||= MU::Cloud::Google::Endpoint.new(api: "IamV1::IamService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
540
+ return @@iam_api[credentials]
447
541
  elsif subclass.is_a?(Symbol)
448
542
  return Object.const_get("::Google").const_get("Apis").const_get("IamV1").const_get(subclass)
449
543
  end
@@ -451,17 +545,17 @@ module MU
451
545
 
452
546
  # GCP's AdminDirectory Service API
453
547
  # @param subclass [<Google::Apis::AdminDirectoryV1>]: If specified, will return the class ::Google::Apis::AdminDirectoryV1::subclass instead of an API client instance
454
- def self.admin_directory(subclass = nil)
548
+ def self.admin_directory(subclass = nil, credentials: nil)
455
549
  require 'google/apis/admin_directory_v1'
456
550
 
457
551
  if subclass.nil?
458
552
  begin
459
- @@admin_directory_api ||= MU::Cloud::Google::Endpoint.new(api: "AdminDirectoryV1::DirectoryService", scopes: ['https://www.googleapis.com/auth/admin.directory.group.member.readonly', 'https://www.googleapis.com/auth/admin.directory.group.readonly'], masquerade: $MU_CFG['google']['masquerade_as'])
553
+ @@admin_directory_api[credentials] ||= MU::Cloud::Google::Endpoint.new(api: "AdminDirectoryV1::DirectoryService", scopes: ['https://www.googleapis.com/auth/admin.directory.group.member.readonly', 'https://www.googleapis.com/auth/admin.directory.group.readonly', 'https://www.googleapis.com/auth/admin.directory.user.readonly', 'https://www.googleapis.com/auth/admin.directory.domain.readonly', 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly', 'https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly', 'https://www.googleapis.com/auth/admin.directory.customer.readonly'], masquerade: $MU_CFG['google']['masquerade_as'], credentials: credentials)
460
554
  rescue Signet::AuthorizationError => e
461
555
  MU.log "Cannot masquerade as #{$MU_CFG['google']['masquerade_as']}", MU::ERROR, details: "You can only use masquerade_as with GSuite. For more information on delegating GSuite authority to a service account, see:\nhttps://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority"
462
556
  raise e
463
557
  end
464
- return @@admin_directory_api
558
+ return @@admin_directory_api[credentials]
465
559
  elsif subclass.is_a?(Symbol)
466
560
  return Object.const_get("::Google").const_get("Apis").const_get("AdminDirectoryV1").const_get(subclass)
467
561
  end
@@ -469,12 +563,12 @@ module MU
469
563
 
470
564
  # Google's Cloud Resource Manager API
471
565
  # @param subclass [<Google::Apis::CloudresourcemanagerV1>]: If specified, will return the class ::Google::Apis::CloudresourcemanagerV1::subclass instead of an API client instance
472
- def self.resource_manager(subclass = nil)
566
+ def self.resource_manager(subclass = nil, credentials: nil)
473
567
  require 'google/apis/cloudresourcemanager_v1'
474
568
 
475
569
  if subclass.nil?
476
- @@resource_api ||= MU::Cloud::Google::Endpoint.new(api: "CloudresourcemanagerV1::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform'])
477
- return @@resource_api
570
+ @@resource_api[credentials] ||= MU::Cloud::Google::Endpoint.new(api: "CloudresourcemanagerV1::CloudResourceManagerService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
571
+ return @@resource_api[credentials]
478
572
  elsif subclass.is_a?(Symbol)
479
573
  return Object.const_get("::Google").const_get("Apis").const_get("CloudresourcemanagerV1").const_get(subclass)
480
574
  end
@@ -482,12 +576,12 @@ module MU
482
576
 
483
577
  # Google's Container API
484
578
  # @param subclass [<Google::Apis::ContainerV1>]: If specified, will return the class ::Google::Apis::ContainerV1::subclass instead of an API client instance
485
- def self.container(subclass = nil)
579
+ def self.container(subclass = nil, credentials: nil)
486
580
  require 'google/apis/container_v1'
487
581
 
488
582
  if subclass.nil?
489
- @@container_api ||= MU::Cloud::Google::Endpoint.new(api: "ContainerV1::ContainerService", scopes: ['https://www.googleapis.com/auth/cloud-platform'])
490
- return @@container_api
583
+ @@container_api[credentials] ||= MU::Cloud::Google::Endpoint.new(api: "ContainerV1::ContainerService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
584
+ return @@container_api[credentials]
491
585
  elsif subclass.is_a?(Symbol)
492
586
  return Object.const_get("::Google").const_get("Apis").const_get("ContainerV1").const_get(subclass)
493
587
  end
@@ -495,12 +589,12 @@ module MU
495
589
 
496
590
  # Google's Service Manager API (the one you use to enable pre-project APIs)
497
591
  # @param subclass [<Google::Apis::ServicemanagementV1>]: If specified, will return the class ::Google::Apis::ServicemanagementV1::subclass instead of an API client instance
498
- def self.service_manager(subclass = nil)
592
+ def self.service_manager(subclass = nil, credentials: nil)
499
593
  require 'google/apis/servicemanagement_v1'
500
594
 
501
595
  if subclass.nil?
502
- @@service_api ||= MU::Cloud::Google::Endpoint.new(api: "ServicemanagementV1::ServiceManagementService", scopes: ['https://www.googleapis.com/auth/cloud-platform'])
503
- return @@service_api
596
+ @@service_api[credentials] ||= MU::Cloud::Google::Endpoint.new(api: "ServicemanagementV1::ServiceManagementService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
597
+ return @@service_api[credentials]
504
598
  elsif subclass.is_a?(Symbol)
505
599
  return Object.const_get("::Google").const_get("Apis").const_get("ServicemanagementV1").const_get(subclass)
506
600
  end
@@ -508,12 +602,12 @@ module MU
508
602
 
509
603
  # Google's SQL Service API
510
604
  # @param subclass [<Google::Apis::SqladminV1beta4>]: If specified, will return the class ::Google::Apis::SqladminV1beta4::subclass instead of an API client instance
511
- def self.sql(subclass = nil)
605
+ def self.sql(subclass = nil, credentials: nil)
512
606
  require 'google/apis/sqladmin_v1beta4'
513
607
 
514
608
  if subclass.nil?
515
- @@sql_api ||= MU::Cloud::Google::Endpoint.new(api: "SqladminV1beta4::SQLAdminService", scopes: ['https://www.googleapis.com/auth/cloud-platform'])
516
- return @@sql_api
609
+ @@sql_api[credentials] ||= MU::Cloud::Google::Endpoint.new(api: "SqladminV1beta4::SQLAdminService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
610
+ return @@sql_api[credentials]
517
611
  elsif subclass.is_a?(Symbol)
518
612
  return Object.const_get("::Google").const_get("Apis").const_get("SqladminV1beta4").const_get(subclass)
519
613
  end
@@ -521,12 +615,12 @@ module MU
521
615
 
522
616
  # Google's StackDriver Logging Service API
523
617
  # @param subclass [<Google::Apis::LoggingV2>]: If specified, will return the class ::Google::Apis::LoggingV2::subclass instead of an API client instance
524
- def self.logging(subclass = nil)
618
+ def self.logging(subclass = nil, credentials: nil)
525
619
  require 'google/apis/logging_v2'
526
620
 
527
621
  if subclass.nil?
528
- @@logging_api ||= MU::Cloud::Google::Endpoint.new(api: "LoggingV2::LoggingService", scopes: ['https://www.googleapis.com/auth/cloud-platform'])
529
- return @@logging_api
622
+ @@logging_api[credentials] ||= MU::Cloud::Google::Endpoint.new(api: "LoggingV2::LoggingService", scopes: ['https://www.googleapis.com/auth/cloud-platform'], credentials: credentials)
623
+ return @@logging_api[credentials]
530
624
  elsif subclass.is_a?(Symbol)
531
625
  return Object.const_get("::Google").const_get("Apis").const_get("LoggingV2").const_get(subclass)
532
626
  end
@@ -540,34 +634,40 @@ module MU
540
634
  # codebase.
541
635
  class Endpoint
542
636
  @api = nil
637
+ @credentials = nil
638
+ attr_reader :issuer
543
639
 
544
640
  # Create a Google Cloud Platform API client
545
641
  # @param api [String]: Which API are we wrapping?
546
642
  # @param scopes [Array<String>]: Google auth scopes applicable to this API
547
- def initialize(api: "ComputeBeta::ComputeService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/compute.readonly'], masquerade: nil)
643
+ def initialize(api: "ComputeBeta::ComputeService", scopes: ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/compute.readonly'], masquerade: nil, credentials: nil)
644
+ @credentials = credentials
548
645
  @api = Object.const_get("Google::Apis::#{api}").new
549
- @api.authorization = MU::Cloud::Google.loadCredentials(scopes)
646
+ @api.authorization = MU::Cloud::Google.loadCredentials(scopes, credentials: credentials)
550
647
  if masquerade
551
648
  @api.authorization.sub = masquerade
552
649
  @api.authorization.fetch_access_token!
553
650
  end
651
+ @issuer = @api.authorization.issuer
554
652
  end
555
653
 
556
- # Generic wrapper for deleting Compute resources
654
+ # Generic wrapper for deleting Compute resources, which are consistent
655
+ # enough that we can get away with this.
557
656
  # @param type [String]: The type of resource, typically the string you'll find in all of the API calls referring to it
558
657
  # @param project [String]: The project in which we should look for the resources
559
658
  # @param region [String]: The region in which to loop for the resources
560
659
  # @param noop [Boolean]: If true, will only log messages about resources to be deleted, without actually deleting them
561
660
  # @param filter [String]: The Compute API filter string to use to isolate appropriate resources
562
- def delete(type, project, region = nil, noop = false, filter = "description eq #{MU.deploy_id}")
661
+ def delete(type, project, region = nil, noop = false, filter = "description eq #{MU.deploy_id}", credentials: nil)
563
662
  list_sym = "list_#{type.sub(/y$/, "ie")}s".to_sym
564
663
  resp = nil
565
664
  begin
566
665
  if region
567
- resp = MU::Cloud::Google.compute.send(list_sym, project, region, filter: filter)
666
+ resp = MU::Cloud::Google.compute(credentials: @credentials).send(list_sym, project, region, filter: filter)
568
667
  else
569
- resp = MU::Cloud::Google.compute.send(list_sym, project, filter: filter)
668
+ resp = MU::Cloud::Google.compute(credentials: @credentials).send(list_sym, project, filter: filter)
570
669
  end
670
+
571
671
  rescue ::Google::Apis::ClientError => e
572
672
  return if e.message.match(/^notFound: /)
573
673
  end
@@ -587,26 +687,27 @@ module MU
587
687
  resp = nil
588
688
  failed = false
589
689
  if region
590
- resp = MU::Cloud::Google.compute.send(delete_sym, project, region, obj.name)
690
+ resp = MU::Cloud::Google.compute(credentials: @credentials).send(delete_sym, project, region, obj.name)
591
691
  else
592
- resp = MU::Cloud::Google.compute.send(delete_sym, project, obj.name)
692
+ resp = MU::Cloud::Google.compute(credentials: @credentials).send(delete_sym, project, obj.name)
593
693
  end
694
+
594
695
  if resp.error and resp.error.errors and resp.error.errors.size > 0
595
696
  failed = true
596
697
  retries += 1
597
- if resp.error.errors.first.code == "RESOURCE_IN_USE_BY_ANOTHER_RESOURCE" and retries < 3
598
- sleep 10
698
+ if resp.error.errors.first.code == "RESOURCE_IN_USE_BY_ANOTHER_RESOURCE" and retries < 6
699
+ sleep 15
599
700
  else
600
701
  MU.log "Error deleting #{type.gsub(/_/, " ")} #{obj.name}", MU::ERR, details: resp.error.errors
601
702
  raise MuError, "Failed to delete #{type.gsub(/_/, " ")} #{obj.name}"
602
703
  end
603
704
  else
604
- # TODO validate that the resource actually went away, because it seems not to do so very reliably
705
+ failed = false
605
706
  end
606
- failed = false
707
+ # TODO validate that the resource actually went away, because it seems not to do so very reliably
607
708
  rescue ::Google::Apis::ClientError => e
608
709
  raise e if !e.message.match(/^notFound: /)
609
- end while failed and retries < 3
710
+ end while failed and retries < 6
610
711
  end
611
712
  }
612
713
  }
@@ -644,10 +745,13 @@ module MU
644
745
  rescue ::Google::Apis::ClientError => e
645
746
  if e.message.match(/^invalidParameter:/)
646
747
  MU.log "#{method_sym.to_s}: "+e.message, MU::ERR, details: arguments
748
+ # uncomment for debugging stuff; this can occur in benign situations so we don't normally want it logging
749
+ elsif e.message.match(/^forbidden:/)
750
+ MU.log "Using credentials #{@credentials}: #{method_sym.to_s}: "+e.message, MU::ERR, details: caller
647
751
  end
648
752
  if retries <= 1 and e.message.match(/^accessNotConfigured/)
649
753
  enable_obj = nil
650
- project = arguments.size > 0 ? arguments.first.to_s : MU::Cloud::Google.defaultProject
754
+ project = arguments.size > 0 ? arguments.first.to_s : MU::Cloud::Google.defaultProject(@credentials)
651
755
  enable_obj = MU::Cloud::Google.service_manager(:EnableServiceRequest).new(
652
756
  consumer_id: "project:"+project
653
757
  )
@@ -659,7 +763,7 @@ module MU
659
763
  MU.setLogging(MU::Logger::NORMAL)
660
764
  MU.log "Attempting to enable #{svc_name} in project #{project}, then waiting for 30s", MU::WARN
661
765
  MU.setLogging(save_verbosity)
662
- MU::Cloud::Google.service_manager.enable_service(svc_name, enable_obj)
766
+ MU::Cloud::Google.service_manager(credentials: @credentials).enable_service(svc_name, enable_obj)
663
767
  sleep 30
664
768
  retries += 1
665
769
  retry
@@ -698,7 +802,7 @@ module MU
698
802
  if retval.status != "DONE"
699
803
  sleep 7
700
804
  begin
701
- resp = MU::Cloud::Google.compute.get_global_operation(
805
+ resp = MU::Cloud::Google.compute(credentials: @credentials).get_global_operation(
702
806
  arguments.first, # there's always a project id
703
807
  retval.name
704
808
  )
@@ -754,17 +858,17 @@ module MU
754
858
  end
755
859
  end
756
860
  return retval
757
- rescue ::Google::Apis::ServerError, ::Google::Apis::ClientError => e
861
+ rescue ::Google::Apis::ServerError, ::Google::Apis::ClientError, ::Google::Apis::TransmissionError => e
758
862
  if e.class.name == "Google::Apis::ClientError" and
759
863
  (!method_sym.to_s.match(/^insert_/) or !e.message.match(/^notFound: /) or
760
864
  (e.message.match(/^notFound: /) and method_sym.to_s.match(/^insert_/))
761
865
  )
762
- if e.message.match(/^notFound: /) and method_sym.to_s.match(/^insert_/)
866
+ if e.message.match(/^notFound: /) and method_sym.to_s.match(/^insert_/) and retval
763
867
  logreq = MU::Cloud::Google.logging(:ListLogEntriesRequest).new(
764
868
  resource_names: ["projects/"+arguments.first],
765
869
  filter: %Q{labels."compute.googleapis.com/resource_id"="#{retval.target_id}" OR labels."ssl_certificate_id"="#{retval.target_id}"} # XXX I guess we need to cover all of the possible keys, ugh
766
870
  )
767
- logs = MU::Cloud::Google.logging.list_entry_log_entries(logreq)
871
+ logs = MU::Cloud::Google.logging(credentials: @credentials).list_entry_log_entries(logreq)
768
872
  details = nil
769
873
  if logs.entries
770
874
  details = logs.entries.map { |e| e.json_payload }
@@ -786,7 +890,7 @@ module MU
786
890
  debuglevel = MU::WARN
787
891
  interval = 40 + Random.rand(15) - 5
788
892
  # elsif retries > 100
789
- # raise MuError, "Exhausted retries after #{retries} attempts while calling EC2's #{method_sym} in #{@region}. Args were: #{arguments}"
893
+ # raise MuError, "Exhausted retries after #{retries} attempts while calling Compute's #{method_sym} in #{@region}. Args were: #{arguments}"
790
894
  end
791
895
 
792
896
  MU.log "Got #{e.inspect} calling Google's #{method_sym}, waiting #{interval.to_s}s and retrying. Called from: #{caller[1]}", debuglevel, details: arguments
@@ -797,15 +901,15 @@ module MU
797
901
  end
798
902
  end
799
903
 
800
- @@compute_api = nil
801
- @@container_api = nil
802
- @@storage_api = nil
803
- @@sql_api = nil
804
- @@iam_api = nil
805
- @@logging_api = nil
806
- @@resource_api = nil
807
- @@service_api = nil
808
- @@admin_directory_api = nil
904
+ @@compute_api = {}
905
+ @@container_api = {}
906
+ @@storage_api = {}
907
+ @@sql_api = {}
908
+ @@iam_api = {}
909
+ @@logging_api = {}
910
+ @@resource_api = {}
911
+ @@service_api = {}
912
+ @@admin_directory_api = {}
809
913
  end
810
914
  end
811
915
  end