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.
- checksums.yaml +4 -4
- data/Berksfile +16 -54
- data/Berksfile.lock +14 -62
- data/bin/mu-aws-setup +131 -108
- data/bin/mu-configure +311 -74
- data/bin/mu-gcp-setup +84 -62
- data/bin/mu-load-config.rb +46 -2
- data/bin/mu-self-update +11 -9
- data/bin/mu-upload-chef-artifacts +4 -4
- data/{mu.gemspec → cloud-mu.gemspec} +2 -2
- data/cookbooks/awscli/Berksfile +8 -0
- data/cookbooks/mu-activedirectory/Berksfile +11 -0
- data/cookbooks/mu-firewall/Berksfile +9 -0
- data/cookbooks/mu-firewall/metadata.rb +1 -1
- data/cookbooks/mu-glusterfs/Berksfile +10 -0
- data/cookbooks/mu-jenkins/Berksfile +14 -0
- data/cookbooks/mu-master/Berksfile +23 -0
- data/cookbooks/mu-master/attributes/default.rb +1 -1
- data/cookbooks/mu-master/metadata.rb +2 -2
- data/cookbooks/mu-master/recipes/default.rb +1 -1
- data/cookbooks/mu-master/recipes/init.rb +7 -3
- data/cookbooks/mu-master/recipes/ssl-certs.rb +1 -0
- data/cookbooks/mu-mongo/Berksfile +10 -0
- data/cookbooks/mu-openvpn/Berksfile +11 -0
- data/cookbooks/mu-php54/Berksfile +13 -0
- data/cookbooks/mu-splunk/Berksfile +10 -0
- data/cookbooks/mu-tools/Berksfile +21 -0
- data/cookbooks/mu-tools/files/default/Mu_CA.pem +15 -15
- data/cookbooks/mu-utility/Berksfile +9 -0
- data/cookbooks/mu-utility/metadata.rb +2 -1
- data/cookbooks/nagios/Berksfile +7 -4
- data/cookbooks/s3fs/Berksfile +9 -0
- data/environments/dev.json +6 -6
- data/environments/prod.json +6 -6
- data/modules/mu.rb +20 -42
- data/modules/mu/cleanup.rb +102 -100
- data/modules/mu/cloud.rb +90 -28
- data/modules/mu/clouds/aws.rb +449 -218
- data/modules/mu/clouds/aws/alarm.rb +29 -17
- data/modules/mu/clouds/aws/cache_cluster.rb +78 -64
- data/modules/mu/clouds/aws/collection.rb +25 -18
- data/modules/mu/clouds/aws/container_cluster.rb +73 -66
- data/modules/mu/clouds/aws/database.rb +124 -116
- data/modules/mu/clouds/aws/dnszone.rb +27 -20
- data/modules/mu/clouds/aws/firewall_rule.rb +30 -22
- data/modules/mu/clouds/aws/folder.rb +18 -3
- data/modules/mu/clouds/aws/function.rb +77 -23
- data/modules/mu/clouds/aws/group.rb +19 -12
- data/modules/mu/clouds/aws/habitat.rb +153 -0
- data/modules/mu/clouds/aws/loadbalancer.rb +59 -52
- data/modules/mu/clouds/aws/log.rb +30 -23
- data/modules/mu/clouds/aws/msg_queue.rb +29 -20
- data/modules/mu/clouds/aws/notifier.rb +222 -0
- data/modules/mu/clouds/aws/role.rb +178 -90
- data/modules/mu/clouds/aws/search_domain.rb +40 -24
- data/modules/mu/clouds/aws/server.rb +169 -137
- data/modules/mu/clouds/aws/server_pool.rb +60 -83
- data/modules/mu/clouds/aws/storage_pool.rb +59 -31
- data/modules/mu/clouds/aws/user.rb +36 -27
- data/modules/mu/clouds/aws/userdata/linux.erb +101 -93
- data/modules/mu/clouds/aws/vpc.rb +250 -189
- data/modules/mu/clouds/azure.rb +132 -0
- data/modules/mu/clouds/cloudformation.rb +65 -1
- data/modules/mu/clouds/cloudformation/alarm.rb +8 -0
- data/modules/mu/clouds/cloudformation/cache_cluster.rb +7 -0
- data/modules/mu/clouds/cloudformation/collection.rb +7 -0
- data/modules/mu/clouds/cloudformation/database.rb +7 -0
- data/modules/mu/clouds/cloudformation/dnszone.rb +7 -0
- data/modules/mu/clouds/cloudformation/firewall_rule.rb +9 -2
- data/modules/mu/clouds/cloudformation/loadbalancer.rb +7 -0
- data/modules/mu/clouds/cloudformation/log.rb +7 -0
- data/modules/mu/clouds/cloudformation/server.rb +7 -0
- data/modules/mu/clouds/cloudformation/server_pool.rb +7 -0
- data/modules/mu/clouds/cloudformation/vpc.rb +7 -0
- data/modules/mu/clouds/google.rb +214 -110
- data/modules/mu/clouds/google/container_cluster.rb +42 -24
- data/modules/mu/clouds/google/database.rb +15 -6
- data/modules/mu/clouds/google/firewall_rule.rb +17 -25
- data/modules/mu/clouds/google/group.rb +13 -5
- data/modules/mu/clouds/google/habitat.rb +105 -0
- data/modules/mu/clouds/google/loadbalancer.rb +28 -20
- data/modules/mu/clouds/google/server.rb +93 -354
- data/modules/mu/clouds/google/server_pool.rb +18 -10
- data/modules/mu/clouds/google/user.rb +22 -14
- data/modules/mu/clouds/google/vpc.rb +97 -69
- data/modules/mu/config.rb +133 -38
- data/modules/mu/config/alarm.rb +25 -0
- data/modules/mu/config/cache_cluster.rb +5 -3
- data/modules/mu/config/cache_cluster.yml +23 -0
- data/modules/mu/config/database.rb +25 -16
- data/modules/mu/config/database.yml +3 -3
- data/modules/mu/config/function.rb +1 -2
- data/modules/mu/config/{project.rb → habitat.rb} +10 -10
- data/modules/mu/config/notifier.rb +85 -0
- data/modules/mu/config/notifier.yml +9 -0
- data/modules/mu/config/role.rb +1 -1
- data/modules/mu/config/search_domain.yml +2 -2
- data/modules/mu/config/server.rb +13 -1
- data/modules/mu/config/server.yml +3 -3
- data/modules/mu/config/server_pool.rb +3 -1
- data/modules/mu/config/storage_pool.rb +3 -1
- data/modules/mu/config/storage_pool.yml +19 -0
- data/modules/mu/config/vpc.rb +70 -8
- data/modules/mu/groomers/chef.rb +2 -3
- data/modules/mu/kittens.rb +500 -122
- data/modules/mu/master.rb +5 -5
- data/modules/mu/mommacat.rb +151 -91
- data/modules/tests/super_complex_bok.yml +12 -0
- data/modules/tests/super_simple_bok.yml +12 -0
- data/spec/mu/clouds/azure_spec.rb +82 -0
- data/spec/spec_helper.rb +105 -0
- metadata +26 -5
- data/modules/mu/clouds/aws/notification.rb +0 -139
- 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.
|
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
|
data/modules/mu/clouds/google.rb
CHANGED
@@ -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
|
-
|
66
|
-
|
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
|
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 #{
|
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:
|
152
|
+
bucket: adminBucketName(credentials),
|
86
153
|
name: name
|
87
154
|
)
|
88
|
-
ebs_key = MU::Cloud::Google.storage.insert_object(
|
89
|
-
|
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 #{
|
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
|
-
|
105
|
-
|
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
|
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 #{
|
118
|
-
MU::Cloud::Google.storage.insert_bucket_access_control(
|
119
|
-
|
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:
|
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:
|
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 #{
|
135
|
-
|
136
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
327
|
+
if cfg["credentials_file"]
|
236
328
|
begin
|
237
|
-
data = JSON.parse(File.read(
|
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 #{
|
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 #{
|
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
|
345
|
+
elsif cfg["credentials"]
|
254
346
|
begin
|
255
|
-
vault, item =
|
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
|
-
|
323
|
-
|
324
|
-
|
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
|
-
|
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
|
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 <
|
598
|
-
sleep
|
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
|
-
|
705
|
+
failed = false
|
605
706
|
end
|
606
|
-
|
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 <
|
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
|
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 =
|
801
|
-
@@container_api =
|
802
|
-
@@storage_api =
|
803
|
-
@@sql_api =
|
804
|
-
@@iam_api =
|
805
|
-
@@logging_api =
|
806
|
-
@@resource_api =
|
807
|
-
@@service_api =
|
808
|
-
@@admin_directory_api =
|
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
|