cloud-mu 3.1.4 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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'])