cloud-mu 3.1.4 → 3.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/ansible/roles/mu-windows/README.md +33 -0
  3. data/ansible/roles/mu-windows/defaults/main.yml +2 -0
  4. data/ansible/roles/mu-windows/handlers/main.yml +2 -0
  5. data/ansible/roles/mu-windows/meta/main.yml +53 -0
  6. data/ansible/roles/mu-windows/tasks/main.yml +20 -0
  7. data/ansible/roles/mu-windows/tests/inventory +2 -0
  8. data/ansible/roles/mu-windows/tests/test.yml +5 -0
  9. data/ansible/roles/mu-windows/vars/main.yml +2 -0
  10. data/cloud-mu.gemspec +4 -2
  11. data/cookbooks/mu-tools/recipes/selinux.rb +2 -1
  12. data/cookbooks/mu-tools/recipes/windows-client.rb +140 -144
  13. data/cookbooks/mu-tools/resources/windows_users.rb +44 -43
  14. data/extras/image-generators/AWS/win2k12.yaml +16 -13
  15. data/extras/image-generators/AWS/win2k16.yaml +16 -13
  16. data/extras/image-generators/AWS/win2k19.yaml +19 -0
  17. data/modules/mu.rb +72 -9
  18. data/modules/mu/adoption.rb +14 -2
  19. data/modules/mu/cloud.rb +111 -10
  20. data/modules/mu/clouds/aws.rb +23 -7
  21. data/modules/mu/clouds/aws/container_cluster.rb +640 -692
  22. data/modules/mu/clouds/aws/dnszone.rb +49 -45
  23. data/modules/mu/clouds/aws/firewall_rule.rb +177 -214
  24. data/modules/mu/clouds/aws/role.rb +17 -8
  25. data/modules/mu/clouds/aws/search_domain.rb +1 -1
  26. data/modules/mu/clouds/aws/server.rb +734 -1027
  27. data/modules/mu/clouds/aws/userdata/windows.erb +2 -1
  28. data/modules/mu/clouds/aws/vpc.rb +297 -786
  29. data/modules/mu/clouds/aws/vpc_subnet.rb +286 -0
  30. data/modules/mu/clouds/google/bucket.rb +1 -1
  31. data/modules/mu/clouds/google/container_cluster.rb +21 -17
  32. data/modules/mu/clouds/google/function.rb +8 -2
  33. data/modules/mu/clouds/google/server.rb +102 -32
  34. data/modules/mu/clouds/google/vpc.rb +1 -1
  35. data/modules/mu/config.rb +12 -1
  36. data/modules/mu/config/server.yml +1 -0
  37. data/modules/mu/defaults/AWS.yaml +51 -28
  38. data/modules/mu/groomers/ansible.rb +54 -17
  39. data/modules/mu/groomers/chef.rb +13 -7
  40. data/modules/mu/master/ssl.rb +0 -1
  41. data/modules/mu/mommacat.rb +8 -0
  42. data/modules/tests/ecs.yaml +23 -0
  43. data/modules/tests/includes-and-params.yaml +2 -1
  44. data/modules/tests/server-with-scrub-muisms.yaml +1 -0
  45. data/modules/tests/win2k12.yaml +25 -0
  46. data/modules/tests/win2k16.yaml +25 -0
  47. data/modules/tests/win2k19.yaml +25 -0
  48. data/requirements.txt +1 -0
  49. metadata +50 -4
  50. data/extras/image-generators/AWS/windows.yaml +0 -18
  51. data/modules/tests/needwork/win2k12.yaml +0 -13
@@ -0,0 +1,286 @@
1
+ # Copyright:: Copyright (c) 2020 eGlobalTech, Inc., all rights reserved
2
+ #
3
+ # Licensed under the BSD-3 license (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License in the root of the project or at
6
+ #
7
+ # http://egt-labs.com/mu/LICENSE.html
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module MU
16
+ class Cloud
17
+ class AWS
18
+
19
+ # Creation of Virtual Private Clouds and associated artifacts (routes, subnets, etc).
20
+ class VPC < MU::Cloud::VPC
21
+
22
+ # Subnets are almost a first-class resource. So let's kinda sorta treat
23
+ # them like one. This should only be invoked on objects that already
24
+ # exists in the cloud layer.
25
+ class Subnet < MU::Cloud::AWS::VPC
26
+
27
+ attr_reader :cloud_id
28
+ attr_reader :ip_block
29
+ attr_reader :mu_name
30
+ attr_reader :name
31
+ attr_reader :az
32
+ attr_reader :cloud_desc
33
+
34
+ # @param parent [MU::Cloud::AWS::VPC]: The parent VPC of this subnet.
35
+ # @param config [Hash<String>]:
36
+ def initialize(parent, config)
37
+ @parent = parent
38
+ @config = MU::Config.manxify(config)
39
+ @cloud_id = config['cloud_id']
40
+ @mu_name = config['mu_name']
41
+ @name = config['name']
42
+ @deploydata = config # This is a dummy for the sake of describe()
43
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_subnets(subnet_ids: [@cloud_id]).subnets.first
44
+ @az = resp.availability_zone
45
+ @ip_block = resp.cidr_block
46
+ @cloud_desc = resp # XXX this really isn't the cloud implementation's business
47
+
48
+ end
49
+
50
+ # Return the cloud identifier for the default route of this subnet.
51
+ # @return [String,nil]
52
+ def defaultRoute
53
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_route_tables(
54
+ filters: [{name: "association.subnet-id", values: [@cloud_id]}]
55
+ )
56
+ if resp.route_tables.size == 0 # use default route table for the VPC
57
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_route_tables(
58
+ filters: [{name: "vpc-id", values: [@parent.cloud_id]}]
59
+ )
60
+ end
61
+ resp.route_tables.each { |route_table|
62
+ route_table.routes.each { |route|
63
+ if route.destination_cidr_block =="0.0.0.0/0" and route.state != "blackhole"
64
+ return route.instance_id if !route.instance_id.nil?
65
+ return route.gateway_id if !route.gateway_id.nil?
66
+ return route.vpc_peering_connection_id if !route.vpc_peering_connection_id.nil?
67
+ return route.network_interface_id if !route.network_interface_id.nil?
68
+ end
69
+ }
70
+ }
71
+ return nil
72
+ end
73
+
74
+ # Is this subnet privately-routable only, or public?
75
+ # @return [Boolean]
76
+ def private?
77
+ return false if @cloud_id.nil?
78
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_route_tables(
79
+ filters: [{name: "association.subnet-id", values: [@cloud_id]}]
80
+ )
81
+ if resp.route_tables.size == 0 # use default route table for the VPC
82
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_route_tables(
83
+ filters: [{name: "vpc-id", values: [@parent.cloud_id]}]
84
+ )
85
+ end
86
+ resp.route_tables.each { |route_table|
87
+ route_table.routes.each { |route|
88
+ return false if !route.gateway_id.nil? and route.gateway_id != "local" # you can have an IgW and route it to a subset of IPs instead of 0.0.0.0/0
89
+ if route.destination_cidr_block == "0.0.0.0/0"
90
+ return true if !route.instance_id.nil?
91
+ return true if route.nat_gateway_id
92
+ end
93
+ }
94
+ }
95
+ return true
96
+ end
97
+ end # VPC::Subnet class
98
+
99
+ private
100
+
101
+ def create_subnets
102
+ return [] if @config['subnets'].nil? or @config['subnets'].empty?
103
+ nat_gateways = []
104
+
105
+ @eip_allocation_ids ||= []
106
+
107
+ subnetthreads = Array.new
108
+
109
+ azs = MU::Cloud::AWS.listAZs(region: @config['region'], credentials: @config['credentials'])
110
+ @config['subnets'].each { |subnet|
111
+ subnet_name = @config['name']+"-"+subnet['name']
112
+ az = subnet['availability_zone'] ? subnet['availability_zone'] : azs.op
113
+ MU.log "Creating Subnet #{subnet_name} (#{subnet['ip_block']}) in #{az}", details: subnet
114
+
115
+ subnetthreads << Thread.new {
116
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).create_subnet(
117
+ vpc_id: @cloud_id,
118
+ cidr_block: subnet['ip_block'],
119
+ availability_zone: az
120
+ ).subnet
121
+ subnet_id = subnet['subnet_id'] = resp.subnet_id
122
+
123
+ tag_me(subnet_id, @mu_name+"-"+subnet['name'])
124
+
125
+ loop_if = Proc.new {
126
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_subnets(subnet_ids: [subnet_id]).subnets.first
127
+ (!resp or resp.state != "available")
128
+ }
129
+
130
+ MU.retrier([Aws::EC2::Errors::InvalidSubnetIDNotFound, NoMethodError], wait: 5, loop_if: loop_if) { |retries, _wait|
131
+ MU.log "Waiting for Subnet #{subnet_name} (#{subnet_id}) to become available", MU::NOTICE if retries > 0 and (retries % 3) == 0
132
+ }
133
+
134
+ if !subnet['route_table'].nil?
135
+ routes = {}
136
+ @config['route_tables'].each { |tbl|
137
+ routes[tbl['name']] = tbl
138
+ }
139
+ if routes[subnet['route_table']].nil?
140
+ raise "Subnet #{subnet_name} references nonexistent route #{subnet['route_table']}"
141
+ end
142
+ MU.log "Associating Route Table '#{subnet['route_table']}' (#{routes[subnet['route_table']]['route_table_id']}) with #{subnet_name}"
143
+ MU.retrier([Aws::EC2::Errors::InvalidRouteTableIDNotFound], wait: 10, max: 10) {
144
+ MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).associate_route_table(
145
+ route_table_id: routes[subnet['route_table']]['route_table_id'],
146
+ subnet_id: subnet_id
147
+ )
148
+ }
149
+ end
150
+
151
+ if subnet.has_key?("map_public_ips")
152
+ MU.retrier([Aws::EC2::Errors::InvalidSubnetIDNotFound], wait: 10, max: 10) {
153
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).modify_subnet_attribute(
154
+ subnet_id: subnet_id,
155
+ map_public_ip_on_launch: {
156
+ value: subnet['map_public_ips'],
157
+ }
158
+ )
159
+ }
160
+ end
161
+
162
+ if subnet['is_public'] and subnet['create_nat_gateway']
163
+ nat_gateways << create_nat_gateway(subnet)
164
+ end
165
+
166
+ if subnet["enable_traffic_logging"]
167
+ loggroup = @deploy.findLitterMate(name: @config['name']+"loggroup", type: "logs")
168
+ logrole = @deploy.findLitterMate(name: @config['name']+"logrole", type: "roles")
169
+ MU.log "Enabling traffic logging on Subnet #{subnet_name} in VPC #{@mu_name} to log group #{loggroup.mu_name}"
170
+ MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).create_flow_logs(
171
+ resource_ids: [subnet_id],
172
+ resource_type: "Subnet",
173
+ traffic_type: subnet["traffic_type_to_log"],
174
+ log_group_name: loggroup.mu_name,
175
+ deliver_logs_permission_arn: logrole.cloudobj.arn
176
+ )
177
+ end
178
+ }
179
+ }
180
+
181
+ subnetthreads.each { |t|
182
+ t.join
183
+ }
184
+
185
+ nat_gateways
186
+ end
187
+
188
+ def allocate_eip_for_nat
189
+ MU::MommaCat.lock("nat-gateway-eipalloc")
190
+
191
+ eips = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_addresses(
192
+ filters: [
193
+ {
194
+ name: "domain",
195
+ values: ["vpc"]
196
+ }
197
+ ]
198
+ ).addresses
199
+
200
+ allocation_id = nil
201
+ eips.each { |eip|
202
+ next if !eip.association_id.nil? and !eip.association_id.empty?
203
+ if (eip.private_ip_address.nil? || eip.private_ip_address.empty?) and MU::MommaCat.lock(eip.allocation_id, true, true)
204
+ if !@eip_allocation_ids.include?(eip.allocation_id)
205
+ allocation_id = eip.allocation_id
206
+ break
207
+ end
208
+ end
209
+ }
210
+
211
+ if allocation_id.nil?
212
+ allocation_id = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).allocate_address(domain: "vpc").allocation_id
213
+ MU::MommaCat.lock(allocation_id, false, true)
214
+ end
215
+
216
+ @eip_allocation_ids << allocation_id
217
+
218
+ MU::MommaCat.unlock("nat-gateway-eipalloc")
219
+
220
+ allocation_id
221
+ end
222
+
223
+ def create_nat_gateway(subnet)
224
+ allocation_id = allocate_eip_for_nat
225
+
226
+ nat_gateway_id = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).create_nat_gateway(
227
+ subnet_id: subnet['subnet_id'],
228
+ allocation_id: allocation_id,
229
+ ).nat_gateway.nat_gateway_id
230
+
231
+ ensure_unlock = Proc.new { MU::MommaCat.unlock(allocation_id, true) }
232
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_nat_gateways(nat_gateway_ids: [nat_gateway_id]).nat_gateways.first
233
+ loop_if = Proc.new {
234
+ resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_nat_gateways(nat_gateway_ids: [nat_gateway_id]).nat_gateways.first
235
+ resp.class != Aws::EC2::Types::NatGateway or resp.state == "pending"
236
+ }
237
+
238
+ MU.retrier([Aws::EmptyStructure, NoMethodError], wait: 5, max: 30, always: ensure_unlock, loop_if: loop_if) { |retries, _wait|
239
+ MU.log "Waiting for nat gateway #{nat_gateway_id} to become available (EIP allocation: #{allocation_id})" if retries % 5 == 0
240
+ }
241
+
242
+ raise MuError, "NAT Gateway failed #{nat_gateway_id}: #{resp}" if resp.state == "failed"
243
+
244
+ tag_me(nat_gateway_id)
245
+
246
+ {'id' => nat_gateway_id, 'availability_zone' => subnet['availability_zone']}
247
+ end
248
+
249
+ # Remove all subnets associated with the currently loaded deployment.
250
+ # @param noop [Boolean]: If true, will only print what would be done
251
+ # @param tagfilters [Array<Hash>]: EC2 tags to filter against when search for resources to purge
252
+ # @param region [String]: The cloud provider region
253
+ # @return [void]
254
+ def self.purge_subnets(noop = false, tagfilters = [{name: "tag:MU-ID", values: [MU.deploy_id]}], region: MU.curRegion, credentials: nil)
255
+ resp = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_subnets(
256
+ filters: tagfilters
257
+ )
258
+ subnets = resp.data.subnets
259
+
260
+ return if subnets.nil? or subnets.size == 0
261
+
262
+ subnets.each { |subnet|
263
+ on_retry = Proc.new {
264
+ MU::Cloud::AWS::VPC.purge_interfaces(noop, [{name: "subnet-id", values: [subnet.subnet_id]}], region: region, credentials: credentials)
265
+ }
266
+
267
+ MU.log "Deleting Subnet #{subnet.subnet_id}"
268
+ MU.retrier([Aws::EC2::Errors::DependencyViolation], ignoreme: [Aws::EC2::Errors::InvalidSubnetIDNotFound], max: 20, on_retry: on_retry) { |_retries, wait|
269
+ begin
270
+ if subnet.state != "available"
271
+ MU.log "Waiting for #{subnet.subnet_id} to be in a removable state...", MU::NOTICE
272
+ sleep wait
273
+ else
274
+ MU::Cloud::AWS.ec2(credentials: credentials, region: region).delete_subnet(subnet_id: subnet.subnet_id) if !noop
275
+ end
276
+ end while subnet.state != "available"
277
+ }
278
+ }
279
+ end
280
+ private_class_method :purge_subnets
281
+
282
+ end # VPC class
283
+
284
+ end #class
285
+ end
286
+ end #module
@@ -305,7 +305,7 @@ module MU
305
305
  schema = {
306
306
  "storage_class" => {
307
307
  "type" => "string",
308
- "enum" => ["MULTI_REGIONAL", "REGIONAL", "STANDARD", "NEARLINE", "COLDLINE", "DURABLE_REDUCED_AVAILABILITY"],
308
+ "enum" => ["MULTI_REGIONAL", "REGIONAL", "STANDARD", "NEARLINE", "COLDLINE", "DURABLE_REDUCED_AVAILABILITY", "ARCHIVE"],
309
309
  "default" => "STANDARD"
310
310
  },
311
311
  "bucket_wide_acls" => {
@@ -567,22 +567,24 @@ module MU
567
567
  bok['kubernetes']['network_policy_addon'] = true
568
568
  end
569
569
 
570
- if cloud_desc.ip_allocation_policy.use_ip_aliases
571
- bok['ip_aliases'] = true
572
- end
573
- if cloud_desc.ip_allocation_policy.cluster_ipv4_cidr_block
574
- bok['pod_ip_block'] = cloud_desc.ip_allocation_policy.cluster_ipv4_cidr_block
575
- end
576
- if cloud_desc.ip_allocation_policy.services_ipv4_cidr_block
577
- bok['services_ip_block'] = cloud_desc.ip_allocation_policy.services_ipv4_cidr_block
578
- end
570
+ if cloud_desc.ip_allocation_policy
571
+ if cloud_desc.ip_allocation_policy.use_ip_aliases
572
+ bok['ip_aliases'] = true
573
+ end
574
+ if cloud_desc.ip_allocation_policy.cluster_ipv4_cidr_block
575
+ bok['pod_ip_block'] = cloud_desc.ip_allocation_policy.cluster_ipv4_cidr_block
576
+ end
577
+ if cloud_desc.ip_allocation_policy.services_ipv4_cidr_block
578
+ bok['services_ip_block'] = cloud_desc.ip_allocation_policy.services_ipv4_cidr_block
579
+ end
579
580
 
580
- if cloud_desc.ip_allocation_policy.create_subnetwork
581
- bok['custom_subnet'] = {
582
- "name" => (cloud_desc.ip_allocation_policy.subnetwork_name || cloud_desc.subnetwork)
583
- }
584
- if cloud_desc.ip_allocation_policy.node_ipv4_cidr_block
585
- bok['custom_subnet']['node_ip_block'] = cloud_desc.ip_allocation_policy.node_ipv4_cidr_block
581
+ if cloud_desc.ip_allocation_policy.create_subnetwork
582
+ bok['custom_subnet'] = {
583
+ "name" => (cloud_desc.ip_allocation_policy.subnetwork_name || cloud_desc.subnetwork)
584
+ }
585
+ if cloud_desc.ip_allocation_policy.node_ipv4_cidr_block
586
+ bok['custom_subnet']['node_ip_block'] = cloud_desc.ip_allocation_policy.node_ipv4_cidr_block
587
+ end
586
588
  end
587
589
  end
588
590
 
@@ -1029,7 +1031,8 @@ module MU
1029
1031
  type: "habitats"
1030
1032
  )
1031
1033
  if cluster['service_account']['name'] and
1032
- !cluster['service_account']['id']
1034
+ !cluster['service_account']['id'] and
1035
+ !cluster['service_account']['deploy_id']
1033
1036
  cluster['dependencies'] ||= []
1034
1037
  cluster['dependencies'] << {
1035
1038
  "type" => "user",
@@ -1188,7 +1191,8 @@ module MU
1188
1191
  labels["name"] = MU::Cloud::Google.nameStr(@mu_name)
1189
1192
 
1190
1193
  labelset = MU::Cloud::Google.container(:SetLabelsRequest).new(
1191
- resource_labels: labels
1194
+ resource_labels: labels,
1195
+ label_fingerprint: cloud_desc.label_fingerprint
1192
1196
  )
1193
1197
  MU::Cloud::Google.container(credentials: @config['credentials']).set_project_location_cluster_resource_labels(@cloud_id, labelset)
1194
1198
  end
@@ -340,7 +340,7 @@ module example.com/cloudfunction
340
340
  end
341
341
 
342
342
  codefile = bok["project"]+"_"+bok["region"]+"_"+bok["name"]+".zip"
343
- MU::Cloud::Google::Function.downloadPackage(@cloud_id, codefile, credentials: @config['credentials'])
343
+ return nil if !MU::Cloud::Google::Function.downloadPackage(@cloud_id, codefile, credentials: @config['credentials'])
344
344
  bok['code'] = {
345
345
  'zip_file' => codefile
346
346
  }
@@ -417,7 +417,12 @@ module example.com/cloudfunction
417
417
  bucket = Regexp.last_match[1]
418
418
  path = Regexp.last_match[2]
419
419
 
420
- MU::Cloud::Google.storage(credentials: credentials).get_object(bucket, path, download_dest: zipfile)
420
+ begin
421
+ MU::Cloud::Google.storage(credentials: credentials).get_object(bucket, path, download_dest: zipfile)
422
+ rescue ::Google::Apis::ClientError => e
423
+ MU.log "Couldn't retrieve gs://#{bucket}/#{path} for #{function_id}", MU::WARN, details: e.inspect
424
+ return false
425
+ end
421
426
  elsif cloud_desc.source_upload_url
422
427
  resp = MU::Cloud::Google.function(credentials: credentials).generate_function_download_url(
423
428
  function_id
@@ -428,6 +433,7 @@ module example.com/cloudfunction
428
433
  f.close
429
434
  end
430
435
  end
436
+ true
431
437
  end
432
438
 
433
439
  # Upload a zipfile to our admin Cloud Storage bucket, for use by
@@ -164,19 +164,26 @@ module MU
164
164
  def self.diskConfig(config, create = true, disk_as_url = true, credentials: nil)
165
165
  disks = []
166
166
  if config['image_id'].nil? and config['basis'].nil?
167
- pp config.keys
168
167
  raise MuError, "Can't generate disk configuration for server #{config['name']} without an image ID or basis specified"
169
168
  end
170
169
 
171
170
  img = fetchImage(config['image_id'] || config['basis']['launch_config']['image_id'], credentials: credentials)
172
171
 
173
- # XXX slurp settings from /dev/sda or w/e by convention?
172
+ # if img.source_disk and img.source_disk.match(/projects\/([^\/]+)\/zones\/([^\/]+)\/disks\/(.*)/)
173
+ # _junk, proj, az, name = Regexp.last_match
174
+ # disk_desc = MU::Cloud::Google.compute(credentials: credentials).get_disk(proj, az, name)
175
+ # pp disk_desc
176
+ # raise "nah"
177
+ # end
178
+
174
179
  disktype = "projects/#{config['project']}/zones/#{config['availability_zone']}/diskTypes/pd-standard"
175
- disktype = "pd-standard" if !disk_as_url
176
- # disk_type: projects/project/zones/#{config['availability_zone']}/diskTypes/pd-standard Other values include pd-ssd and local-ssd
180
+
181
+
182
+ disktype.gsub!(/.*?\/([^\/])$/, '\1') if !disk_as_url
183
+
177
184
  imageobj = MU::Cloud::Google.compute(:AttachedDiskInitializeParams).new(
178
185
  source_image: img.self_link,
179
- disk_size_gb: 10, # this is binary? 2gb, that says
186
+ disk_size_gb: img.disk_size_gb,
180
187
  disk_type: disktype,
181
188
  )
182
189
  disks << MU::Cloud::Google.compute(:AttachedDisk).new(
@@ -329,7 +336,7 @@ next if !create
329
336
  end
330
337
  metadata["startup-script"] = @userdata if @userdata and !@userdata.empty?
331
338
 
332
- deploykey = @config['ssh_user']+":"+@deploy.ssh_public_key
339
+ deploykey = @config["ssh_user"]+":"+@deploy.ssh_public_key
333
340
  if metadata["ssh-keys"]
334
341
  metadata["ssh-keys"] += "\n"+deploykey
335
342
  else
@@ -502,7 +509,8 @@ next if !create
502
509
 
503
510
  if @config['ssh_user'].nil?
504
511
  if windows?
505
- @config['ssh_user'] = "Administrator"
512
+ @config['ssh_user'] = @config['windows_admin_user']
513
+ @config['ssh_user'] ||= "Administrator"
506
514
  else
507
515
  @config['ssh_user'] = "root"
508
516
  end
@@ -800,29 +808,6 @@ next if !create
800
808
 
801
809
  # punchAdminNAT
802
810
 
803
- # MU::Cloud::AWS::Server.tagVolumes(@cloud_id)
804
-
805
- # If we have a loadbalancer configured, attach us to it
806
- # if !@config['loadbalancers'].nil?
807
- # if @loadbalancers.nil?
808
- # raise MuError, "#{@mu_name} is configured to use LoadBalancers, but none have been loaded by dependencies()"
809
- # end
810
- # @loadbalancers.each { |lb|
811
- # lb.registerNode(@cloud_id)
812
- # }
813
- # end
814
-
815
- # Let us into any databases we depend on.
816
- # This is probelmtic with autscaling - old ips are not removed, and access to the database can easily be given at the BoK level
817
- # if @dependencies.has_key?("database")
818
- # @dependencies['database'].values.each { |db|
819
- # db.allowHost(@deploydata["private_ip_address"]+"/32")
820
- # if @deploydata["public_ip_address"]
821
- # db.allowHost(@deploydata["public_ip_address"]+"/32")
822
- # end
823
- # }
824
- # end
825
-
826
811
  @groomer.saveDeployData
827
812
 
828
813
  begin
@@ -1017,9 +1002,93 @@ next if !create
1017
1002
  end
1018
1003
 
1019
1004
  # return [String]: A password string.
1020
- def getWindowsAdminPassword
1005
+ def getWindowsAdminPassword(use_cache: true)
1006
+ @config['windows_auth_vault'] ||= {
1007
+ "vault" => @mu_name,
1008
+ "item" => "windows_credentials",
1009
+ "password_field" => "password"
1010
+ }
1011
+
1012
+ if use_cache
1013
+ begin
1014
+ win_admin_password = @groomer.getSecret(
1015
+ vault: @config['windows_auth_vault']['vault'],
1016
+ item: @config['windows_auth_vault']['item'],
1017
+ field: @config["windows_auth_vault"]["password_field"]
1018
+ )
1019
+ MU.log "RETURNINATING FROM CACHE", MU::WARN, details: win_admin_password
1020
+ return win_admin_password if win_admin_password
1021
+ rescue MU::Groomer::MuNoSuchSecret, MU::Groomer::RunError
1022
+ end
1023
+ end
1024
+
1025
+ require 'openssl/oaep'
1026
+ timeout = 300
1027
+
1028
+ serial_out = nil
1029
+ key = OpenSSL::PKey::RSA.generate 2048
1030
+
1031
+ missing_response = Proc.new {
1032
+ !serial_out or !serial_out.contents or serial_out.contents.empty? or JSON.parse(serial_out.contents)["userName"] != @config['windows_admin_username']
1033
+ }
1034
+
1035
+ did_metadata = false
1036
+ MU.retrier(loop_if: missing_response, wait: 10, max: timeout/10) {
1037
+ serial_out = MU::Cloud::Google.compute(credentials: @credentials).get_instance_serial_port_output(@project_id, @config['availability_zone'], @cloud_id, port: 4)
1038
+
1039
+ if missing_response.call and
1040
+ !cloud_desc(use_cache: false).metadata.items.map { |i| i.key }.include?("windows-keys")
1041
+ keybytes = Base64.decode64(key.public_key.export.gsub(/-----(?:BEGIN|END) PUBLIC KEY-----/, ''))
1042
+ modulus = keybytes.byteslice(33,256)
1043
+ exponent = keybytes.byteslice(291,3)
1044
+ keydata = {
1045
+ "userName" => @config['windows_admin_username'],
1046
+ "modulus" => Base64.strict_encode64(modulus),
1047
+ "exponent" => Base64.strict_encode64(exponent),
1048
+ "email" => MU.muCfg['mu_admin_email'],
1049
+ "expireOn" => (Time.now.utc+timeout).strftime('%Y-%m-%dT%H:%M:%SZ')
1050
+ }
1051
+
1052
+ new_items = cloud_desc.metadata.items.map { |item|
1053
+ MU::Cloud::Google.compute(:Metadata)::Item.new(
1054
+ key: item.key,
1055
+ value: item.value
1056
+ )
1057
+ }
1058
+ new_items.reject! { |item| item.key == "windows-keys" }
1059
+ new_items << MU::Cloud::Google.compute(:Metadata)::Item.new(
1060
+ key: "windows-keys",
1061
+ value: JSON.generate(keydata)
1062
+ )
1063
+ new_metadata = MU::Cloud::Google.compute(:Metadata).new(
1064
+ fingerprint: cloud_desc(use_cache: false).metadata.fingerprint,
1065
+ items: new_items
1066
+ )
1067
+
1068
+ MU::Cloud::Google.compute(credentials: @credentials).set_instance_metadata(@project_id, @config['availability_zone'], @cloud_id, new_metadata)
1069
+ end
1070
+ }
1071
+
1072
+ return nil if missing_response.call
1073
+
1074
+ pwdata = JSON.parse(serial_out.contents)
1075
+ if pwdata['encryptedPassword'] and pwdata['userName'] == @config['windows_admin_username']
1076
+ decrypted_pw = key.private_decrypt_oaep(Base64.strict_decode64(pwdata['encryptedPassword']))
1077
+ creds = {
1078
+ "username" => @config['windows_admin_username'],
1079
+ "password" => decrypted_pw,
1080
+ "sshd_username" => "sshd_service",
1081
+ "sshd_password" => decrypted_pw
1082
+ }
1083
+ @groomer.saveSecret(vault: @mu_name, item: "windows_credentials", data: creds, permissions: "name:#{@mu_name}")
1084
+
1085
+ return decrypted_pw
1086
+ end
1087
+
1088
+ nil
1021
1089
  end
1022
1090
 
1091
+
1023
1092
  # Add a volume to this instance
1024
1093
  # @param dev [String]: Device name to use when attaching to instance
1025
1094
  # @param size [String]: Size (in gb) of the new volume
@@ -1272,7 +1341,7 @@ next if !create
1272
1341
  "roles" => MU::Cloud::Google::User.schema(config)[1]["roles"],
1273
1342
  "windows_admin_username" => {
1274
1343
  "type" => "string",
1275
- "default" => "Administrator"
1344
+ "default" => "muadmin"
1276
1345
  },
1277
1346
  "create_image" => {
1278
1347
  "properties" => {
@@ -1438,6 +1507,7 @@ next if !create
1438
1507
  end
1439
1508
 
1440
1509
  if server['service_account']
1510
+ server['service_account'] = server['service_account'].to_h
1441
1511
  server['service_account']['cloud'] = "Google"
1442
1512
  server['service_account']['habitat'] ||= server['project']
1443
1513
  found = MU::Config::Ref.get(server['service_account'])