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

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