cloud-mu 3.1.6 → 3.4.0

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/bin/mu-adopt +15 -12
  4. data/bin/mu-azure-tests +57 -0
  5. data/bin/mu-cleanup +2 -4
  6. data/bin/mu-configure +37 -1
  7. data/bin/mu-deploy +3 -3
  8. data/bin/mu-findstray-tests +25 -0
  9. data/bin/mu-gen-docs +2 -4
  10. data/bin/mu-load-config.rb +2 -1
  11. data/bin/mu-run-tests +37 -12
  12. data/cloud-mu.gemspec +4 -4
  13. data/cookbooks/mu-tools/attributes/default.rb +7 -0
  14. data/cookbooks/mu-tools/libraries/helper.rb +87 -3
  15. data/cookbooks/mu-tools/recipes/apply_security.rb +39 -23
  16. data/cookbooks/mu-tools/recipes/aws_api.rb +13 -0
  17. data/cookbooks/mu-tools/recipes/google_api.rb +4 -0
  18. data/cookbooks/mu-tools/recipes/rsyslog.rb +8 -1
  19. data/cookbooks/mu-tools/resources/disk.rb +33 -12
  20. data/cookbooks/mu-tools/resources/mommacat_request.rb +1 -2
  21. data/cookbooks/mu-tools/templates/centos-8/sshd_config.erb +215 -0
  22. data/extras/clean-stock-amis +10 -2
  23. data/extras/generate-stock-images +7 -3
  24. data/extras/image-generators/AWS/centos7.yaml +19 -16
  25. data/extras/image-generators/AWS/{rhel7.yaml → rhel71.yaml} +0 -0
  26. data/extras/image-generators/AWS/{win2k12.yaml → win2k12r2.yaml} +0 -0
  27. data/modules/mommacat.ru +2 -2
  28. data/modules/mu.rb +84 -97
  29. data/modules/mu/adoption.rb +359 -59
  30. data/modules/mu/cleanup.rb +67 -44
  31. data/modules/mu/cloud.rb +108 -1754
  32. data/modules/mu/cloud/database.rb +49 -0
  33. data/modules/mu/cloud/dnszone.rb +44 -0
  34. data/modules/mu/cloud/machine_images.rb +212 -0
  35. data/modules/mu/cloud/providers.rb +81 -0
  36. data/modules/mu/cloud/resource_base.rb +929 -0
  37. data/modules/mu/cloud/server.rb +40 -0
  38. data/modules/mu/cloud/server_pool.rb +1 -0
  39. data/modules/mu/cloud/ssh_sessions.rb +228 -0
  40. data/modules/mu/cloud/winrm_sessions.rb +237 -0
  41. data/modules/mu/cloud/wrappers.rb +178 -0
  42. data/modules/mu/config.rb +122 -80
  43. data/modules/mu/config/alarm.rb +2 -6
  44. data/modules/mu/config/bucket.rb +32 -3
  45. data/modules/mu/config/cache_cluster.rb +2 -2
  46. data/modules/mu/config/cdn.rb +100 -0
  47. data/modules/mu/config/collection.rb +1 -1
  48. data/modules/mu/config/container_cluster.rb +2 -2
  49. data/modules/mu/config/database.rb +84 -105
  50. data/modules/mu/config/database.yml +1 -2
  51. data/modules/mu/config/dnszone.rb +5 -4
  52. data/modules/mu/config/doc_helpers.rb +4 -5
  53. data/modules/mu/config/endpoint.rb +2 -1
  54. data/modules/mu/config/firewall_rule.rb +3 -19
  55. data/modules/mu/config/folder.rb +1 -1
  56. data/modules/mu/config/function.rb +17 -8
  57. data/modules/mu/config/group.rb +1 -1
  58. data/modules/mu/config/habitat.rb +1 -1
  59. data/modules/mu/config/job.rb +89 -0
  60. data/modules/mu/config/loadbalancer.rb +57 -11
  61. data/modules/mu/config/log.rb +1 -1
  62. data/modules/mu/config/msg_queue.rb +1 -1
  63. data/modules/mu/config/nosqldb.rb +1 -1
  64. data/modules/mu/config/notifier.rb +8 -19
  65. data/modules/mu/config/ref.rb +81 -9
  66. data/modules/mu/config/role.rb +1 -1
  67. data/modules/mu/config/schema_helpers.rb +30 -34
  68. data/modules/mu/config/search_domain.rb +1 -1
  69. data/modules/mu/config/server.rb +5 -13
  70. data/modules/mu/config/server_pool.rb +3 -7
  71. data/modules/mu/config/storage_pool.rb +1 -1
  72. data/modules/mu/config/tail.rb +10 -0
  73. data/modules/mu/config/user.rb +1 -1
  74. data/modules/mu/config/vpc.rb +13 -17
  75. data/modules/mu/defaults/AWS.yaml +106 -106
  76. data/modules/mu/defaults/Azure.yaml +1 -0
  77. data/modules/mu/defaults/Google.yaml +1 -0
  78. data/modules/mu/deploy.rb +33 -19
  79. data/modules/mu/groomer.rb +15 -0
  80. data/modules/mu/groomers/chef.rb +3 -0
  81. data/modules/mu/logger.rb +120 -144
  82. data/modules/mu/master.rb +22 -1
  83. data/modules/mu/mommacat.rb +71 -26
  84. data/modules/mu/mommacat/daemon.rb +23 -14
  85. data/modules/mu/mommacat/naming.rb +82 -3
  86. data/modules/mu/mommacat/search.rb +59 -16
  87. data/modules/mu/mommacat/storage.rb +119 -48
  88. data/modules/mu/{clouds → providers}/README.md +1 -1
  89. data/modules/mu/{clouds → providers}/aws.rb +248 -62
  90. data/modules/mu/{clouds → providers}/aws/alarm.rb +3 -3
  91. data/modules/mu/{clouds → providers}/aws/bucket.rb +275 -41
  92. data/modules/mu/{clouds → providers}/aws/cache_cluster.rb +14 -50
  93. data/modules/mu/providers/aws/cdn.rb +782 -0
  94. data/modules/mu/{clouds → providers}/aws/collection.rb +5 -5
  95. data/modules/mu/{clouds → providers}/aws/container_cluster.rb +65 -63
  96. data/modules/mu/providers/aws/database.rb +1747 -0
  97. data/modules/mu/{clouds → providers}/aws/dnszone.rb +26 -12
  98. data/modules/mu/providers/aws/endpoint.rb +1072 -0
  99. data/modules/mu/{clouds → providers}/aws/firewall_rule.rb +39 -32
  100. data/modules/mu/{clouds → providers}/aws/folder.rb +1 -1
  101. data/modules/mu/{clouds → providers}/aws/function.rb +291 -133
  102. data/modules/mu/{clouds → providers}/aws/group.rb +18 -20
  103. data/modules/mu/{clouds → providers}/aws/habitat.rb +3 -3
  104. data/modules/mu/providers/aws/job.rb +469 -0
  105. data/modules/mu/{clouds → providers}/aws/loadbalancer.rb +77 -47
  106. data/modules/mu/{clouds → providers}/aws/log.rb +5 -5
  107. data/modules/mu/{clouds → providers}/aws/msg_queue.rb +14 -11
  108. data/modules/mu/{clouds → providers}/aws/nosqldb.rb +96 -5
  109. data/modules/mu/{clouds → providers}/aws/notifier.rb +135 -63
  110. data/modules/mu/{clouds → providers}/aws/role.rb +112 -78
  111. data/modules/mu/{clouds → providers}/aws/search_domain.rb +172 -41
  112. data/modules/mu/{clouds → providers}/aws/server.rb +120 -145
  113. data/modules/mu/{clouds → providers}/aws/server_pool.rb +42 -60
  114. data/modules/mu/{clouds → providers}/aws/storage_pool.rb +21 -38
  115. data/modules/mu/{clouds → providers}/aws/user.rb +12 -16
  116. data/modules/mu/{clouds → providers}/aws/userdata/README.md +0 -0
  117. data/modules/mu/{clouds → providers}/aws/userdata/linux.erb +5 -4
  118. data/modules/mu/{clouds → providers}/aws/userdata/windows.erb +0 -0
  119. data/modules/mu/{clouds → providers}/aws/vpc.rb +141 -73
  120. data/modules/mu/{clouds → providers}/aws/vpc_subnet.rb +0 -0
  121. data/modules/mu/{clouds → providers}/azure.rb +4 -1
  122. data/modules/mu/{clouds → providers}/azure/container_cluster.rb +1 -5
  123. data/modules/mu/{clouds → providers}/azure/firewall_rule.rb +8 -1
  124. data/modules/mu/{clouds → providers}/azure/habitat.rb +0 -0
  125. data/modules/mu/{clouds → providers}/azure/loadbalancer.rb +0 -0
  126. data/modules/mu/{clouds → providers}/azure/role.rb +0 -0
  127. data/modules/mu/{clouds → providers}/azure/server.rb +32 -24
  128. data/modules/mu/{clouds → providers}/azure/user.rb +1 -1
  129. data/modules/mu/{clouds → providers}/azure/userdata/README.md +0 -0
  130. data/modules/mu/{clouds → providers}/azure/userdata/linux.erb +0 -0
  131. data/modules/mu/{clouds → providers}/azure/userdata/windows.erb +0 -0
  132. data/modules/mu/{clouds → providers}/azure/vpc.rb +4 -6
  133. data/modules/mu/{clouds → providers}/cloudformation.rb +1 -1
  134. data/modules/mu/{clouds → providers}/cloudformation/alarm.rb +3 -3
  135. data/modules/mu/{clouds → providers}/cloudformation/cache_cluster.rb +3 -3
  136. data/modules/mu/{clouds → providers}/cloudformation/collection.rb +3 -3
  137. data/modules/mu/{clouds → providers}/cloudformation/database.rb +6 -17
  138. data/modules/mu/{clouds → providers}/cloudformation/dnszone.rb +3 -3
  139. data/modules/mu/{clouds → providers}/cloudformation/firewall_rule.rb +3 -3
  140. data/modules/mu/{clouds → providers}/cloudformation/loadbalancer.rb +3 -3
  141. data/modules/mu/{clouds → providers}/cloudformation/log.rb +3 -3
  142. data/modules/mu/{clouds → providers}/cloudformation/server.rb +7 -7
  143. data/modules/mu/{clouds → providers}/cloudformation/server_pool.rb +5 -5
  144. data/modules/mu/{clouds → providers}/cloudformation/vpc.rb +3 -3
  145. data/modules/mu/{clouds → providers}/docker.rb +0 -0
  146. data/modules/mu/{clouds → providers}/google.rb +15 -6
  147. data/modules/mu/{clouds → providers}/google/bucket.rb +2 -2
  148. data/modules/mu/{clouds → providers}/google/container_cluster.rb +29 -14
  149. data/modules/mu/{clouds → providers}/google/database.rb +2 -9
  150. data/modules/mu/{clouds → providers}/google/firewall_rule.rb +3 -3
  151. data/modules/mu/{clouds → providers}/google/folder.rb +5 -9
  152. data/modules/mu/{clouds → providers}/google/function.rb +4 -4
  153. data/modules/mu/{clouds → providers}/google/group.rb +9 -17
  154. data/modules/mu/{clouds → providers}/google/habitat.rb +4 -8
  155. data/modules/mu/{clouds → providers}/google/loadbalancer.rb +2 -2
  156. data/modules/mu/{clouds → providers}/google/role.rb +46 -35
  157. data/modules/mu/{clouds → providers}/google/server.rb +26 -11
  158. data/modules/mu/{clouds → providers}/google/server_pool.rb +11 -11
  159. data/modules/mu/{clouds → providers}/google/user.rb +32 -22
  160. data/modules/mu/{clouds → providers}/google/userdata/README.md +0 -0
  161. data/modules/mu/{clouds → providers}/google/userdata/linux.erb +0 -0
  162. data/modules/mu/{clouds → providers}/google/userdata/windows.erb +0 -0
  163. data/modules/mu/{clouds → providers}/google/vpc.rb +38 -3
  164. data/modules/tests/aws-jobs-functions.yaml +46 -0
  165. data/modules/tests/centos6.yaml +15 -0
  166. data/modules/tests/centos7.yaml +15 -0
  167. data/modules/tests/centos8.yaml +12 -0
  168. data/modules/tests/ecs.yaml +2 -2
  169. data/modules/tests/eks.yaml +1 -1
  170. data/modules/tests/functions/node-function/lambda_function.js +10 -0
  171. data/modules/tests/functions/python-function/lambda_function.py +12 -0
  172. data/modules/tests/microservice_app.yaml +288 -0
  173. data/modules/tests/rds.yaml +108 -0
  174. data/modules/tests/regrooms/rds.yaml +123 -0
  175. data/modules/tests/server-with-scrub-muisms.yaml +1 -1
  176. data/modules/tests/super_complex_bok.yml +2 -2
  177. data/modules/tests/super_simple_bok.yml +2 -2
  178. data/spec/mu/clouds/azure_spec.rb +2 -2
  179. metadata +126 -98
  180. data/modules/mu/clouds/aws/database.rb +0 -1974
  181. data/modules/mu/clouds/aws/endpoint.rb +0 -596
@@ -152,7 +152,7 @@ module MU
152
152
  instance_name = MU.deploy_id+"-"+@config['name']+"-"+resource.logical_resource_id
153
153
  MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", instance_name, credentials: @config['credentials'])
154
154
 
155
- instance = MU::Cloud::AWS::Server.notifyDeploy(
155
+ instance = MU::Cloud.resourceClass("AWS", "Server").notifyDeploy(
156
156
  @config['name']+"-"+resource.logical_resource_id,
157
157
  resource.physical_resource_id
158
158
  )
@@ -170,7 +170,7 @@ module MU
170
170
 
171
171
  mu_zone, _junk = MU::Cloud::DNSZone.find(name: "mu")
172
172
  if !mu_zone.nil?
173
- MU::Cloud::AWS::DNSZone.genericMuDNSEntry(instance_name, instance["private_ip_address"], MU::Cloud::Server)
173
+ MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(instance_name, instance["private_ip_address"], MU::Cloud::Server)
174
174
  else
175
175
  MU::Master.addInstanceToEtcHosts(instance["public_ip_address"], instance_name)
176
176
  end
@@ -178,7 +178,7 @@ module MU
178
178
  when "AWS::EC2::SecurityGroup"
179
179
  MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
180
180
  MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @config['credentials'])
181
- MU::Cloud::AWS::FirewallRule.notifyDeploy(
181
+ MU::Cloud.resourceClass("AWS", "FirewallRule").notifyDeploy(
182
182
  @config['name']+"-"+resource.logical_resource_id,
183
183
  resource.physical_resource_id
184
184
  )
@@ -242,7 +242,7 @@ module MU
242
242
  # @param region [String]: The cloud provider region
243
243
  # @param wait [Boolean]: Block on the removal of this stack; AWS deletion will continue in the background otherwise if false.
244
244
  # @return [void]
245
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, wait: false, credentials: nil, flags: {})
245
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, wait: false, credentials: nil, flags: {})
246
246
  MU.log "AWS::Collection.cleanup: need to support flags['known']", MU::DEBUG, details: flags
247
247
  MU.log "Placeholder: AWS Collection artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
248
248
 
@@ -251,7 +251,7 @@ module MU
251
251
  resp.stacks.each { |stack|
252
252
  ok = false
253
253
  stack.tags.each { |tag|
254
- ok = true if (tag.key == "MU-ID") and tag.value == MU.deploy_id
254
+ ok = true if (tag.key == "MU-ID") and tag.value == deploy_id
255
255
  }
256
256
  if ok
257
257
  MU.log "Deleting CloudFormation stack #{stack.stack_name})"
@@ -287,6 +287,7 @@ MU.log c.name, MU::NOTICE, details: t
287
287
  # @return [OpenStruct]
288
288
  def cloud_desc(use_cache: true)
289
289
  return @cloud_desc_cache if @cloud_desc_cache and use_cache
290
+ return nil if !@cloud_id
290
291
  @cloud_desc_cache = if @config['flavor'] == "EKS" or
291
292
  (@config['flavor'] == "Fargate" and !@config['containers'])
292
293
  resp = MU::Cloud::AWS.eks(region: @config['region'], credentials: @config['credentials']).describe_cluster(
@@ -326,7 +327,7 @@ MU.log c.name, MU::NOTICE, details: t
326
327
  end
327
328
 
328
329
  @@eks_versions = {}
329
- @@eks_version_semaphore = Mutex.new
330
+ @@eks_version_semaphores = {}
330
331
  # Use the AWS SSM API to fetch the current version of the Amazon Linux
331
332
  # ECS-optimized AMI, so we can use it as a default AMI for ECS deploys.
332
333
  # @param flavor [String]: ECS or EKS
@@ -339,24 +340,22 @@ MU.log c.name, MU::NOTICE, details: t
339
340
  names: ["/aws/service/#{flavor.downcase}/optimized-ami/amazon-linux/recommended"]
340
341
  )
341
342
  else
342
- @@eks_version_semaphore.synchronize {
343
+ @@eks_version_semaphores[region] ||= Mutex.new
344
+
345
+ @@eks_version_semaphores[region].synchronize {
343
346
  if !@@eks_versions[region]
344
347
  @@eks_versions[region] ||= []
345
348
  versions = {}
346
- resp = nil
347
- next_token = nil
348
- begin
349
- resp = MU::Cloud::AWS.ssm(region: region).get_parameters_by_path(
350
- path: "/aws/service/#{flavor.downcase}",
351
- recursive: true,
352
- next_token: next_token
353
- )
354
- resp.parameters.each { |p|
355
- p.name.match(/\/aws\/service\/eks\/optimized-ami\/([^\/]+?)\//)
356
- versions[Regexp.last_match[1]] = true
357
- }
358
- next_token = resp.next_token
359
- end while !next_token.nil?
349
+ resp = MU::Cloud::AWS.ssm(region: region).get_parameters_by_path(
350
+ path: "/aws/service/#{flavor.downcase}/optimized-ami",
351
+ recursive: true,
352
+ max_results: 10 # as high as it goes, ugh
353
+ )
354
+
355
+ resp.parameters.each { |p|
356
+ p.name.match(/\/aws\/service\/eks\/optimized-ami\/([^\/]+?)\//)
357
+ versions[Regexp.last_match[1]] = true
358
+ }
360
359
  @@eks_versions[region] = versions.keys.sort { |a, b| MU.version_sort(a, b) }
361
360
  end
362
361
  }
@@ -376,15 +375,31 @@ MU.log c.name, MU::NOTICE, details: t
376
375
  nil
377
376
  end
378
377
 
378
+ @@supported_eks_region_cache = []
379
+ @@eks_region_semaphore = Mutex.new
380
+
379
381
  # Return the list of regions where we know EKS is supported.
380
382
  def self.EKSRegions(credentials = nil)
381
- eks_regions = []
382
- MU::Cloud::AWS.listRegions(credentials: credentials).each { |r|
383
- ami = getStandardImage("EKS", r)
384
- eks_regions << r if ami
385
- }
383
+ @@eks_region_semaphore.synchronize {
384
+ if @@supported_eks_region_cache and !@@supported_eks_region_cache.empty?
385
+ return @@supported_eks_region_cache
386
+ end
387
+ start = Time.now
388
+ # the SSM API is painfully slow for large result sets, so thread
389
+ # these and do them in parallel
390
+ @@supported_eks_region_cache = []
391
+ region_threads = []
392
+ MU::Cloud::AWS.listRegions(credentials: credentials).each { |region|
393
+ region_threads << Thread.new(region) { |r|
394
+ r_start = Time.now
395
+ ami = getStandardImage("EKS", r)
396
+ @@supported_eks_region_cache << r if ami
397
+ }
398
+ }
399
+ region_threads.each { |t| t.join }
386
400
 
387
- eks_regions
401
+ @@supported_eks_region_cache
402
+ }
388
403
  end
389
404
 
390
405
  # Does this resource type exist as a global (cloud-wide) artifact, or
@@ -405,30 +420,32 @@ MU.log c.name, MU::NOTICE, details: t
405
420
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
406
421
  # @param region [String]: The cloud provider region
407
422
  # @return [void]
408
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
423
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
409
424
  MU.log "AWS::ContainerCluster.cleanup: need to support flags['known']", MU::DEBUG, details: flags
410
425
  MU.log "Placeholder: AWS ContainerCluster artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
411
426
 
412
- purge_ecs_clusters(noop: noop, region: region, credentials: credentials)
427
+ purge_ecs_clusters(noop: noop, region: region, credentials: credentials, deploy_id: deploy_id)
413
428
 
414
- purge_eks_clusters(noop: noop, region: region, credentials: credentials)
429
+ purge_eks_clusters(noop: noop, region: region, credentials: credentials, deploy_id: deploy_id)
415
430
 
416
431
  end
417
432
 
418
- def self.purge_eks_clusters(noop: false, region: MU.curRegion, credentials: nil)
419
- return if !MU::Cloud::AWS::ContainerCluster.EKSRegions.include?(region)
433
+ def self.purge_eks_clusters(noop: false, region: MU.curRegion, credentials: nil, deploy_id: MU.deploy_id)
420
434
  resp = begin
421
435
  MU::Cloud::AWS.eks(credentials: credentials, region: region).list_clusters
422
436
  rescue Aws::EKS::Errors::AccessDeniedException
423
437
  # EKS isn't actually live in this region, even though SSM lists
424
438
  # base images for it
439
+ if @@supported_eks_region_cache
440
+ @@supported_eks_region_cache.delete(region)
441
+ end
425
442
  return
426
443
  end
427
444
 
428
445
  return if !resp or !resp.clusters
429
446
 
430
447
  resp.clusters.each { |cluster|
431
- if cluster.match(/^#{MU.deploy_id}-/)
448
+ if cluster.match(/^#{deploy_id}-/)
432
449
 
433
450
  desc = MU::Cloud::AWS.eks(credentials: credentials, region: region).describe_cluster(
434
451
  name: cluster
@@ -466,19 +483,20 @@ MU.log c.name, MU::NOTICE, details: t
466
483
  MU.log "Waiting for EKS cluster #{cluster} to finish deleting (status #{status})", MU::NOTICE
467
484
  end
468
485
  }
469
- # MU::Cloud::AWS::Server.removeIAMProfile(cluster)
486
+ # MU::Cloud.resourceClass("AWS", "Server").removeIAMProfile(cluster)
470
487
  end
471
488
  }
472
489
  end
473
490
  private_class_method :purge_eks_clusters
474
491
 
475
- def self.purge_ecs_clusters(noop: false, region: MU.curRegion, credentials: nil)
492
+ def self.purge_ecs_clusters(noop: false, region: MU.curRegion, credentials: nil, deploy_id: MU.deploy_id)
493
+ start = Time.now
476
494
  resp = MU::Cloud::AWS.ecs(credentials: credentials, region: region).list_clusters
477
495
 
478
496
  return if !resp or !resp.cluster_arns or resp.cluster_arns.empty?
479
497
 
480
498
  resp.cluster_arns.each { |arn|
481
- if arn.match(/:cluster\/(#{MU.deploy_id}[^:]+)$/)
499
+ if arn.match(/:cluster\/(#{deploy_id}[^:]+)$/)
482
500
  cluster = Regexp.last_match[1]
483
501
 
484
502
  svc_resp = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_services(
@@ -524,7 +542,7 @@ MU.log c.name, MU::NOTICE, details: t
524
542
  }
525
543
 
526
544
  tasks = MU::Cloud::AWS.ecs(region: region, credentials: credentials).list_task_definitions(
527
- family_prefix: MU.deploy_id
545
+ family_prefix: deploy_id
528
546
  )
529
547
 
530
548
  if tasks and tasks.task_definition_arns
@@ -1214,18 +1232,18 @@ MU.log c.name, MU::NOTICE, details: t
1214
1232
  # @return [Boolean]: True if validation succeeded, False otherwise
1215
1233
  def self.validateConfig(cluster, configurator)
1216
1234
  ok = true
1217
-
1218
- cluster['size'] = MU::Cloud::AWS::Server.validateInstanceType(cluster["instance_type"], cluster["region"])
1235
+ start = Time.now
1236
+ cluster['size'] = MU::Cloud.resourceClass("AWS", "Server").validateInstanceType(cluster["instance_type"], cluster["region"])
1219
1237
  ok = false if cluster['size'].nil?
1220
1238
 
1221
1239
  cluster["flavor"] = "EKS" if cluster["flavor"].match(/^Kubernetes$/i)
1222
1240
 
1223
- if cluster["flavor"] == "ECS" and cluster["kubernetes"] and !MU::Cloud::AWS.isGovCloud?(cluster["region"]) and !cluster["containers"] and MU::Cloud::AWS::ContainerCluster.EKSRegions.include?(cluster['region'])
1241
+ if cluster["flavor"] == "ECS" and cluster["kubernetes"] and !MU::Cloud::AWS.isGovCloud?(cluster["region"]) and !cluster["containers"] and MU::Cloud::AWS::ContainerCluster.EKSRegions(cluster['credentials']).include?(cluster['region'])
1224
1242
  cluster["flavor"] = "EKS"
1225
1243
  MU.log "Setting flavor of ContainerCluster '#{cluster['name']}' to EKS ('kubernetes' stanza was specified)", MU::NOTICE
1226
1244
  end
1227
1245
 
1228
- if cluster["flavor"] == "EKS" and !MU::Cloud::AWS::ContainerCluster.EKSRegions.include?(cluster['region'])
1246
+ if cluster["flavor"] == "EKS" and !MU::Cloud::AWS::ContainerCluster.EKSRegions(cluster['credentials']).include?(cluster['region'])
1229
1247
  MU.log "EKS is only available in some regions", MU::ERR, details: MU::Cloud::AWS::ContainerCluster.EKSRegions
1230
1248
  ok = false
1231
1249
  end
@@ -1295,7 +1313,7 @@ MU.log c.name, MU::NOTICE, details: t
1295
1313
  end
1296
1314
 
1297
1315
  if !created_generic_loggroup
1298
- cluster["dependencies"] << { "type" => "log", "name" => logname }
1316
+ MU::Config.addDependency(cluster, logname, "log")
1299
1317
  logdesc = {
1300
1318
  "name" => logname,
1301
1319
  "region" => cluster["region"],
@@ -1334,10 +1352,7 @@ MU.log c.name, MU::NOTICE, details: t
1334
1352
  }
1335
1353
  configurator.insertKitten(roledesc, "roles")
1336
1354
 
1337
- cluster["dependencies"] << {
1338
- "type" => "role",
1339
- "name" => rolename
1340
- }
1355
+ MU::Config.addDependency(cluster, rolename, "role")
1341
1356
  end
1342
1357
 
1343
1358
  created_generic_loggroup = true
@@ -1366,11 +1381,7 @@ MU.log c.name, MU::NOTICE, details: t
1366
1381
  role["tags"] = cluster["tags"] if !cluster["tags"].nil?
1367
1382
  role["optional_tags"] = cluster["optional_tags"] if !cluster["optional_tags"].nil?
1368
1383
  configurator.insertKitten(role, "roles")
1369
- cluster['dependencies'] << {
1370
- "type" => "role",
1371
- "name" => cluster["name"]+"pods",
1372
- "phase" => "groom"
1373
- }
1384
+ MU::Config.addDependency(cluster, cluster["name"]+"pods", "role", phase: "groom")
1374
1385
  if !MU::Master.kubectl
1375
1386
  MU.log "Since I can't find a kubectl executable, you will have to handle all service account, user, and role bindings manually!", MU::WARN
1376
1387
  end
@@ -1482,12 +1493,7 @@ MU.log c.name, MU::NOTICE, details: t
1482
1493
  "AmazonEKS_CNI_Policy",
1483
1494
  "AmazonEC2ContainerRegistryReadOnly"
1484
1495
  ]
1485
- worker_pool["dependencies"] = [
1486
- {
1487
- "type" => "container_cluster",
1488
- "name" => cluster['name']
1489
- }
1490
- ]
1496
+ MU::Config.addDependency(worker_pool, cluster["name"], "container_cluster")
1491
1497
  worker_pool["run_list"] = ["recipe[mu-tools::eks]"]
1492
1498
  worker_pool["run_list"].concat(cluster["run_list"]) if cluster["run_list"]
1493
1499
  MU::Config::Server.common_properties.keys.each { |k|
@@ -1495,16 +1501,14 @@ MU.log c.name, MU::NOTICE, details: t
1495
1501
  worker_pool[k] = cluster[k]
1496
1502
  end
1497
1503
  }
1498
-
1504
+ else
1505
+ worker_pool["groom"] = false # don't meddle with ECS workers unnecessarily
1499
1506
  end
1500
1507
 
1501
1508
  configurator.insertKitten(worker_pool, "server_pools")
1502
1509
 
1503
1510
  if cluster["flavor"] == "ECS"
1504
- cluster["dependencies"] << {
1505
- "name" => cluster["name"]+"workers",
1506
- "type" => "server_pool",
1507
- }
1511
+ MU::Config.addDependency(cluster, cluster["name"]+"workers", "server_pool")
1508
1512
  end
1509
1513
 
1510
1514
  end
@@ -1526,11 +1530,7 @@ MU.log c.name, MU::NOTICE, details: t
1526
1530
  role["tags"] = cluster["tags"] if !cluster["tags"].nil?
1527
1531
  role["optional_tags"] = cluster["optional_tags"] if !cluster["optional_tags"].nil?
1528
1532
  configurator.insertKitten(role, "roles")
1529
- cluster['dependencies'] << {
1530
- "type" => "role",
1531
- "name" => cluster["name"]+"controlplane",
1532
- "phase" => "groom"
1533
- }
1533
+ MU::Config.addDependency(cluster, cluster["name"]+"controlplane", "role", phase: "groom")
1534
1534
  end
1535
1535
 
1536
1536
  ok
@@ -1749,7 +1749,7 @@ MU.log c.name, MU::NOTICE, details: t
1749
1749
  @deploy.findLitterMate(type: "server_pools", name: @config["name"]+"workers")
1750
1750
  end
1751
1751
  serverpool.listNodes.each { |mynode|
1752
- resources = resource_lookup[node.cloud_desc.instance_type]
1752
+ resources = resource_lookup[mynode.cloud_desc.instance_type]
1753
1753
  threads << Thread.new(mynode) { |node|
1754
1754
  ident_doc = nil
1755
1755
  ident_doc_sig = nil
@@ -1950,6 +1950,8 @@ MU.log c.name, MU::NOTICE, details: t
1950
1950
  task_params[:network_mode] = "awsvpc"
1951
1951
  task_params[:cpu] = cpu_total.to_i.to_s
1952
1952
  task_params[:memory] = mem_total.to_i.to_s
1953
+ elsif @config['vpc']
1954
+ task_params[:network_mode] = "awsvpc"
1953
1955
  end
1954
1956
 
1955
1957
  MU.log "Registering task definition #{service_name} with #{container_definitions.size.to_s} containers"
@@ -0,0 +1,1747 @@
1
+ ## Copyright:: Copyright (c) 2014 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
+ autoload :Net, 'net/ssh/gateway'
16
+
17
+ module MU
18
+ class Cloud
19
+ class AWS
20
+ # A database as configured in {MU::Config::BasketofKittens::databases}
21
+ class Database < MU::Cloud::Database
22
+
23
+ # Map legal storage values for each disk type and database engine so
24
+ # our validator can check them for us.
25
+ STORAGE_RANGES = {
26
+ "io1" => {
27
+ "postgres" => 100..65536,
28
+ "mysql" => 100..65536,
29
+ "mariadb" => 100..65536,
30
+ "oracle-se1" => 100..65536,
31
+ "oracle-se2" => 100..65536,
32
+ "oracle-se" => 100..65536,
33
+ "oracle-ee" => 100..65536,
34
+ "sqlserver-ex" => 100..16384,
35
+ "sqlserver-web" => 100..16384,
36
+ "sqlserver-ee" => 200..16384,
37
+ "sqlserver-se" => 200..16384
38
+ },
39
+ "gp2" => {
40
+ "postgres" => 20..65536,
41
+ "mysql" => 20..65536,
42
+ "mariadb" => 20..65536,
43
+ "oracle-se1" => 20..65536,
44
+ "oracle-se2" => 20..65536,
45
+ "oracle-se" => 20..65536,
46
+ "oracle-ee" => 20..65536,
47
+ "sqlserver-ex" => 20..16384,
48
+ "sqlserver-web" => 20..16384,
49
+ "sqlserver-ee" => 200..16384,
50
+ "sqlserver-se" => 200..16384
51
+ },
52
+ "standard" => {
53
+ "postgres" => 5..3072,
54
+ "mysql" => 5..3072,
55
+ "mariadb" => 5..3072,
56
+ "oracle-se1" => 10..3072,
57
+ "oracle-se2" => 10..3072,
58
+ "oracle-se" => 10..3072,
59
+ "oracle-ee" => 10..3072,
60
+ "sqlserver-ex" => 20..1024, # ???
61
+ "sqlserver-web" => 20..1024, # ???
62
+ "sqlserver-ee" => 200..4096, # ???
63
+ "sqlserver-se" => 200..4096 # ???
64
+ }
65
+ }.freeze
66
+
67
+ # List of parameters that are legal to set in +modify_db_instance+ and +modify_db_cluster+
68
+ MODIFIABLE = {
69
+ "instance" => [
70
+ :allocated_storage,
71
+ :db_instance_class,
72
+ :db_subnet_group_name,
73
+ :db_security_groups,
74
+ :vpc_security_group_ids,
75
+ :master_user_password,
76
+ :db_parameter_group_name,
77
+ :backup_retention_period,
78
+ :preferred_backup_window,
79
+ :preferred_maintenance_window,
80
+ :multi_az,
81
+ :engine_version,
82
+ :allow_major_version_upgrade,
83
+ :auto_minor_version_upgrade,
84
+ :license_model,
85
+ :iops,
86
+ :option_group_name,
87
+ :new_db_instance_identifier,
88
+ :storage_type,
89
+ :tde_credential_arn,
90
+ :tde_credential_password,
91
+ :ca_certificate_identifier,
92
+ :domain,
93
+ :copy_tags_to_snapshot,
94
+ :monitoring_interval,
95
+ :db_port_number,
96
+ :publicly_accessible,
97
+ :monitoring_role_arn,
98
+ :domain_iam_role_name,
99
+ :promotion_tier,
100
+ :enable_iam_database_authentication,
101
+ :enable_performance_insights,
102
+ :performance_insights_kms_key_id,
103
+ :performance_insights_retention_period,
104
+ :cloudwatch_logs_export_configuration,
105
+ :processor_features,
106
+ :use_default_processor_features,
107
+ :deletion_protection,
108
+ :max_allocated_storage,
109
+ :certificate_rotation_restart
110
+ ],
111
+ "cluster" => [
112
+ :new_db_cluster_identifier,
113
+ :backup_retention_period,
114
+ :db_cluster_parameter_group_name,
115
+ :vpc_security_group_ids,
116
+ :port,
117
+ :master_user_password,
118
+ :option_group_name,
119
+ :preferred_backup_window,
120
+ :preferred_maintenance_window,
121
+ :enable_iam_database_authentication,
122
+ :backtrack_window,
123
+ :cloudwatch_logs_export_configuration,
124
+ :engine_version,
125
+ :allow_major_version_upgrade,
126
+ :db_instance_parameter_group_name,
127
+ :domain,
128
+ :domain_iam_role_name,
129
+ :scaling_configuration,
130
+ :deletion_protection,
131
+ :enable_http_endpoint,
132
+ :copy_tags_to_snapshot,
133
+ ]
134
+ }
135
+
136
+ # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us.
137
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
138
+ def initialize(**args)
139
+ super
140
+ @config["groomer"] = MU::Config.defaultGroomer unless @config["groomer"]
141
+ @groomclass = MU::Groomer.loadGroomer(@config["groomer"])
142
+
143
+ @mu_name ||=
144
+ if @config and @config['engine'] and @config["engine"].match(/^sqlserver/)
145
+ @deploy.getResourceName(@config["name"], max_length: 15)
146
+ else
147
+ @deploy.getResourceName(@config["name"], max_length: 63)
148
+ end
149
+
150
+ @mu_name.gsub(/(--|-$)/i, "").gsub(/(_)/, "-").gsub!(/^[^a-z]/i, "")
151
+ if @config.has_key?("parameter_group_family")
152
+ @config["parameter_group_name"] ||= @mu_name
153
+ end
154
+
155
+ if args[:from_cloud_desc] and args[:from_cloud_desc].is_a?(Aws::RDS::Types::DBCluster)
156
+ @config['create_cluster'] = true
157
+ end
158
+ if @config['source']
159
+ @config["source"] = MU::Config::Ref.get(@config["source"])
160
+ elsif @config["read_replica_of"]
161
+ @config["source"] = MU::Config::Ref.get(@config["read_replica_of"])
162
+ end
163
+ end
164
+
165
+ # Called automatically by {MU::Deploy#createResources}
166
+ # @return [String]: The cloud provider's identifier for this database instance.
167
+ def create
168
+ # RDS is picky, we can't just use our regular node names for things like
169
+ # the default schema or username. And it varies from engine to engine.
170
+ basename = @config["name"]+@deploy.timestamp+MU.seed.downcase
171
+ basename.gsub!(/[^a-z0-9]/i, "")
172
+ @config["db_name"] = MU::Cloud::AWS::Database.getName(basename, type: "dbname", config: @config)
173
+ @config['master_user'] = MU::Cloud::AWS::Database.getName(basename, type: "dbuser", config: @config) unless @config['master_user']
174
+ @cloud_id = @mu_name
175
+
176
+ # Lets make sure automatic backups are enabled when DB instance is deployed in Multi-AZ so failover actually works. Maybe default to 1 instead?
177
+ if @config['multi_az_on_create'] or @config['multi_az_on_deploy'] or @config["create_cluster"]
178
+ if @config["backup_retention_period"].nil? or @config["backup_retention_period"] == 0
179
+ @config["backup_retention_period"] = 35
180
+ MU.log "Multi-AZ deployment specified but backup retention period disabled or set to 0. Changing to #{@config["backup_retention_period"]} ", MU::WARN
181
+ end
182
+
183
+ if @config["preferred_backup_window"].nil?
184
+ @config["preferred_backup_window"] = "05:00-05:30"
185
+ MU.log "Multi-AZ deployment specified but no backup window specified. Changing to #{@config["preferred_backup_window"]} ", MU::WARN
186
+ end
187
+ end
188
+
189
+ @config["snapshot_id"] =
190
+ if @config["creation_style"] == "existing_snapshot"
191
+ getExistingSnapshot ? getExistingSnapshot : createNewSnapshot
192
+ elsif @config["creation_style"] == "new_snapshot"
193
+ createNewSnapshot
194
+ end
195
+
196
+ @config["subnet_group_name"] = @mu_name if @vpc
197
+
198
+ if @config["create_cluster"]
199
+ getPassword
200
+ manageSubnetGroup
201
+
202
+ if @config.has_key?("parameter_group_family")
203
+ manageDbParameterGroup(true)
204
+ end
205
+
206
+ @config["cluster_identifier"] ||= @cloud_id
207
+
208
+ if @config['creation_style'] == "point_in_time"
209
+ create_point_in_time
210
+ else
211
+ create_basic
212
+ end
213
+
214
+ wait_until_available
215
+
216
+ if %w{existing_snapshot new_snapshot point_in_time}.include?(@config["creation_style"])
217
+ modify_db_cluster_struct = {
218
+ db_cluster_identifier: @cloud_id,
219
+ apply_immediately: true,
220
+ backup_retention_period: @config["backup_retention_period"],
221
+ db_cluster_parameter_group_name: @config["parameter_group_name"],
222
+ master_user_password: @config["password"],
223
+ preferred_backup_window: @config["preferred_backup_window"]
224
+ }
225
+
226
+ modify_db_cluster_struct[:preferred_maintenance_window] = @config["preferred_maintenance_window"] if @config["preferred_maintenance_window"]
227
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).modify_db_cluster(modify_db_cluster_struct)
228
+ wait_until_available
229
+ end
230
+
231
+ do_naming
232
+ elsif @config["add_cluster_node"]
233
+ add_cluster_node
234
+ else
235
+ add_basic
236
+ end
237
+ end
238
+
239
+ # Canonical Amazon Resource Number for this resource
240
+ # @return [String]
241
+ def arn
242
+ cloud_desc.db_instance_arn
243
+ end
244
+
245
+ # Locate an existing Database or Databases and return an array containing matching AWS resource descriptors for those that match.
246
+ # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching Databases
247
+ def self.find(**args)
248
+ found = {}
249
+
250
+ if args[:cloud_id]
251
+ if !args[:cluster]
252
+ begin
253
+ resp = MU::Cloud::AWS.rds(region: args[:region], credentials: args[:credentials]).describe_db_instances(db_instance_identifier: args[:cloud_id]).db_instances.first
254
+ return { args[:cloud_id] => resp } if resp
255
+ rescue Aws::RDS::Errors::DBInstanceNotFound
256
+ MU.log "No results found looking for RDS instance #{args[:cloud_id]}", MU::DEBUG
257
+ end
258
+ end
259
+ begin
260
+ resp = MU::Cloud::AWS.rds(region: args[:region], credentials: args[:credentials]).describe_db_clusters(db_cluster_identifier: args[:cloud_id]).db_clusters.first
261
+ rescue Aws::RDS::Errors::DBClusterNotFoundFault
262
+ MU.log "No results found looking for RDS cluster #{args[:cloud_id]}", MU::DEBUG
263
+ end
264
+ return { args[:cloud_id] => resp } if resp
265
+
266
+ else
267
+ fetch = Proc.new { |noun|
268
+ resp = MU::Cloud::AWS.rds(credentials: args[:credentials], region: args[:region]).send("describe_db_#{noun}s".to_sym)
269
+ resp.send("db_#{noun}s").each { |db|
270
+ found[db.send("db_#{noun}_identifier".to_sym)] = db
271
+ }
272
+ }
273
+ if args[:cluster] or !args.has_key?(:cluster)
274
+ fetch.call("cluster")
275
+ end
276
+ if !args[:cluster]
277
+ fetch.call("instance")
278
+ end
279
+ if args[:tag_key] and args[:tag_value]
280
+ keep = []
281
+ found.each_pair { |id, desc|
282
+ noun = desc.is_a?(Aws::RDS::Types::DBCluster) ? "cluster" : "db"
283
+ resp = MU::Cloud::AWS.rds(credentials: args[:credentials], region: args[:region]).list_tags_for_resource(
284
+ resource_name: MU::Cloud::AWS::Database.getARN(id, noun, "rds", region: args[:region], credentials: args[:credentials])
285
+ )
286
+ if resp and resp.tag_list
287
+ resp.tag_list.each { |tag|
288
+ if tag.key == args[:tag_key] and tag.value == args[:tag_value]
289
+ keep << id
290
+ break
291
+ end
292
+ }
293
+ end
294
+ }
295
+ found.reject! { |k, _v| !keep.include?(k) }
296
+ end
297
+ end
298
+
299
+ return found
300
+ end
301
+
302
+ # Reverse-map our cloud description into a runnable config hash.
303
+ # We assume that any values we have in +@config+ are placeholders, and
304
+ # calculate our own accordingly based on what's live in the cloud.
305
+ def toKitten(**_args)
306
+ bok = {
307
+ "cloud" => "AWS",
308
+ "region" => @config['region'],
309
+ "credentials" => @credentials,
310
+ "cloud_id" => @cloud_id,
311
+ }
312
+
313
+ # Don't adopt cluster members, they'll be picked up by the parent
314
+ # cluster
315
+ if !@config["create_cluster"] and cloud_desc.db_cluster_identifier and !cloud_desc.db_cluster_identifier.empty?
316
+ return nil
317
+ end
318
+
319
+ noun = @config["create_cluster"] ? "cluster" : "db"
320
+ tags = MU::Cloud::AWS.rds(credentials: @credentials, region: @config['region']).list_tags_for_resource(
321
+ resource_name: MU::Cloud::AWS::Database.getARN(@cloud_id, noun, "rds", region: @config['region'], credentials: @credentials)
322
+ ).tag_list
323
+ if tags and !tags.empty?
324
+ bok['tags'] = MU.structToHash(tags, stringify_keys: true)
325
+ bok['name'] = MU::Adoption.tagsToName(bok['tags'])
326
+ end
327
+ bok["name"] ||= @cloud_id
328
+ bok['engine'] = cloud_desc.engine
329
+ bok['engine_version'] = cloud_desc.engine_version
330
+ bok['master_user'] = cloud_desc.master_username
331
+ bok['backup_retention_period'] = cloud_desc.backup_retention_period
332
+ bok["create_cluster"] = true if @config['create_cluster']
333
+
334
+ params = if bok['create_cluster']
335
+ MU::Cloud::AWS.rds(credentials: @credentials, region: @config['region']).describe_db_cluster_parameters(
336
+ db_cluster_parameter_group_name: cloud_desc.db_cluster_parameter_group
337
+ ).parameters
338
+ else
339
+ MU::Cloud::AWS.rds(credentials: @credentials, region: @config['region']).describe_db_parameters(
340
+ db_parameter_group_name: cloud_desc.db_parameter_groups.first.db_parameter_group_name
341
+ ).parameters
342
+ end
343
+
344
+ params.reject! { |p| ["engine-default", "system"].include?(p.source) }
345
+ if params and params.size > 0
346
+ bok[(bok['create_cluster'] ? "cluster_" : "")+'parameter_group_parameters'] = params.map { |p|
347
+ { "key" => p.parameter_name, "value" => p.parameter_value }
348
+ }
349
+ end
350
+
351
+ bok['add_firewall_rules'] = cloud_desc.vpc_security_groups.map { |sg|
352
+ MU::Config::Ref.get(
353
+ id: sg.vpc_security_group_id,
354
+ cloud: "AWS",
355
+ credentials: @credentials,
356
+ region: @config['region'],
357
+ type: "firewall_rules",
358
+ )
359
+ }
360
+ bok['preferred_backup_window'] = cloud_desc.preferred_backup_window
361
+ bok['preferred_maintenance_window'] = cloud_desc.preferred_maintenance_window
362
+ bok['backup_retention_period'] = cloud_desc.backup_retention_period if cloud_desc.backup_retention_period > 1
363
+ bok['multi_az_on_groom'] = true if cloud_desc.multi_az
364
+ bok['storage_encrypted'] = true if cloud_desc.storage_encrypted
365
+
366
+ if bok['create_cluster']
367
+ bok['cluster_node_count'] = cloud_desc.db_cluster_members.size
368
+ bok['cluster_mode'] = cloud_desc.engine_mode
369
+ bok['port'] = cloud_desc.port
370
+
371
+ sizes = []
372
+ vpcs = []
373
+ # we have no sensible way to handle heterogenous cluster members, so
374
+ # for now just assume they're all the same
375
+ cloud_desc.db_cluster_members.each { |db|
376
+ member = MU::Cloud::AWS::Database.find(cloud_id: db.db_instance_identifier, region: @config['region'], credentials: @credentials).values.first
377
+
378
+ sizes << member.db_instance_class
379
+ if member.db_subnet_group and member.db_subnet_group.vpc_id
380
+ vpcs << member.db_subnet_group
381
+ end
382
+ bok
383
+ }
384
+ sizes.uniq!
385
+ vpcs.uniq!
386
+ bok['size'] = sizes.sort.first if !sizes.empty?
387
+ if !vpcs.empty?
388
+ myvpc = MU::MommaCat.findStray("AWS", "vpc", cloud_id: vpcs.sort.first.vpc_id, credentials: @credentials, region: @config['region'], dummy_ok: true, no_deploy_search: true).first
389
+ bok['vpc'] = myvpc.getReference(vpcs.sort.first.subnets.map { |s| s.subnet_identifier })
390
+ end
391
+ else
392
+ bok['size'] = cloud_desc.db_instance_class
393
+ bok['auto_minor_version_upgrade'] = true if cloud_desc.auto_minor_version_upgrade
394
+ if cloud_desc.db_subnet_group
395
+ myvpc = MU::MommaCat.findStray("AWS", "vpc", cloud_id: cloud_desc.db_subnet_group.vpc_id, credentials: @credentials, region: @config['region'], dummy_ok: true, no_deploy_search: true).first
396
+ bok['vpc'] = myvpc.getReference(cloud_desc.db_subnet_group.subnets.map { |s| s.subnet_identifier })
397
+ end
398
+ bok['storage_type'] = cloud_desc.storage_type
399
+ bok['storage'] = cloud_desc.allocated_storage
400
+ bok['license_model'] = cloud_desc.license_model
401
+ bok['publicly_accessible'] = true if cloud_desc.publicly_accessible
402
+ bok['port'] = cloud_desc.endpoint.port
403
+
404
+ if cloud_desc.read_replica_source_db_instance_identifier
405
+ bok['read_replica_of'] = MU::Config::Ref.get(
406
+ id: cloud_desc.read_replica_source_db_instance_identifier.split(/:/).last,
407
+ name: cloud_desc.read_replica_source_db_instance_identifier.split(/:/).last,
408
+ cloud: "AWS",
409
+ region: cloud_desc.read_replica_source_db_instance_identifier.split(/:/)[3],
410
+ credentials: @credentials,
411
+ type: "databases",
412
+ )
413
+ end
414
+ end
415
+
416
+ if cloud_desc.enabled_cloudwatch_logs_exports and
417
+ cloud_desc.enabled_cloudwatch_logs_exports.size > 0
418
+ bok['cloudwatch_logs'] = cloud_desc.enabled_cloudwatch_logs_exports
419
+ end
420
+
421
+ bok
422
+ end
423
+
424
+ # Construct an Amazon Resource Name for an RDS resource. The RDS API is
425
+ # peculiar, and we often need this identifier in order to do things that
426
+ # the other APIs can do with shorthand.
427
+ # @param resource [String]: The name of the resource
428
+ # @param resource_type [String]: The type of the resource (one of `db, es, og, pg, ri, secgrp, snapshot, subgrp`)
429
+ # @param client_type [String]: The name of the client (eg. elasticache, rds, ec2, s3)
430
+ # @param region [String]: The region in which the resource resides.
431
+ # @param account_number [String]: The account in which the resource resides.
432
+ # @return [String]
433
+ def self.getARN(resource, resource_type, client_type, region: MU.curRegion, account_number: nil, credentials: nil)
434
+ account_number ||= MU::Cloud::AWS.credToAcct(credentials)
435
+ aws_str = MU::Cloud::AWS.isGovCloud?(region) ? "aws-us-gov" : "aws"
436
+ "arn:#{aws_str}:#{client_type}:#{region}:#{account_number}:#{resource_type}:#{resource}"
437
+ end
438
+
439
+ # Construct all our tags.
440
+ # @return [Array]: All our standard tags and any custom tags.
441
+ def allTags
442
+ @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
443
+ end
444
+
445
+ # Create a subnet group for a database.
446
+ def manageSubnetGroup
447
+ # Finding subnets, creating security groups/adding holes, create subnet group
448
+ subnet_ids = []
449
+
450
+ dependencies
451
+ raise MuError.new "Didn't find the VPC specified for #{@mu_name}", details: @config["vpc"].to_h unless @vpc
452
+
453
+ mySubnets.each { |subnet|
454
+ next if @config["publicly_accessible"] and subnet.private?
455
+ subnet_ids << subnet.cloud_id
456
+ }
457
+
458
+ if @config['creation_style'] == "existing"
459
+ srcdb_vpc = @config['source'].kitten.cloud_desc.db_subnet_group.vpc_id
460
+ if srcdb_vpc != @vpc.cloud_id
461
+ MU.log "#{self} is deploying into #{@vpc.cloud_id}, but our source database, #{@config['identifier']}, is in #{srcdb_vpc}", MU::ERR
462
+ raise MuError, "Can't use 'existing' to deploy into a different VPC from the source database; try 'new_snapshot' instead"
463
+ end
464
+ end
465
+
466
+ if subnet_ids.empty?
467
+ raise MuError, "Couldn't find subnets in #{@vpc} to add to #{@config["subnet_group_name"]}. Make sure the subnets are valid and publicly_accessible is set correctly"
468
+ else
469
+ resp = begin
470
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).describe_db_subnet_groups(
471
+ db_subnet_group_name: @config["subnet_group_name"]
472
+ )
473
+ # XXX ensure subnet group matches our config?
474
+ rescue ::Aws::RDS::Errors::DBSubnetGroupNotFoundFault
475
+ # Create subnet group
476
+ resp = MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).create_db_subnet_group(
477
+ db_subnet_group_name: @config["subnet_group_name"],
478
+ db_subnet_group_description: @config["subnet_group_name"],
479
+ subnet_ids: subnet_ids,
480
+ tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
481
+ )
482
+ # The API forces it to lowercase, for some reason? Maybe not
483
+ # always? Just rely on what it says.
484
+ @config["subnet_group_name"] = resp.db_subnet_group.db_subnet_group_name
485
+ resp
486
+ end
487
+
488
+ myFirewallRules.each { |sg|
489
+ next if sg.cloud_desc.vpc_id != @vpc.cloud_id
490
+ @config["vpc_security_group_ids"] ||= []
491
+ @config["vpc_security_group_ids"] << sg.cloud_id
492
+ }
493
+ end
494
+
495
+ allowBastionAccess
496
+ end
497
+
498
+ # Create a database parameter group.
499
+ def manageDbParameterGroup(cluster = false, create: true)
500
+ return if !@config["parameter_group_name"]
501
+ name_param = cluster ? :db_cluster_parameter_group_name : :db_parameter_group_name
502
+ fieldname = cluster ? "cluster_parameter_group_parameters" : "db_parameter_group_parameters"
503
+
504
+ params = {
505
+ db_parameter_group_family: @config["parameter_group_family"],
506
+ description: "Parameter group for #{@mu_name}",
507
+ tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
508
+ }
509
+ params[name_param] = @config["parameter_group_name"]
510
+
511
+ if create
512
+ MU.log "Creating a #{cluster ? "cluster" : "database" } parameter group #{@config["parameter_group_name"]}"
513
+
514
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).send(cluster ? :create_db_cluster_parameter_group : :create_db_parameter_group, params)
515
+ end
516
+
517
+
518
+ if @config[fieldname] and !@config[fieldname].empty?
519
+
520
+ old_values = MU::Cloud::AWS.rds(credentials: @credentials, region: @config['region']).send(cluster ? :describe_db_cluster_parameters : :describe_db_parameters, { name_param => @config["parameter_group_name"] } ).parameters
521
+ old_values.map! { |p| [p.parameter_name, p.parameter_value] }.flatten
522
+ old_values = old_values.to_h
523
+
524
+ params = []
525
+ @config[fieldname].each { |item|
526
+ next if old_values[item["name"]] == item['value']
527
+ params << {parameter_name: item['name'], parameter_value: item['value'], apply_method: item['apply_method']}
528
+ }
529
+ return if params.empty?
530
+
531
+ MU.log "Modifying parameter group #{@config["parameter_group_name"]}", MU::NOTICE, details: params.map { |p| { p[:parameter_name] => p[:parameter_value] } }
532
+
533
+ MU.retrier([Aws::RDS::Errors::InvalidDBParameterGroupState], wait: 30, max: 10) {
534
+ if cluster
535
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).modify_db_cluster_parameter_group(
536
+ db_cluster_parameter_group_name: @config["parameter_group_name"],
537
+ parameters: params
538
+ )
539
+ else
540
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).modify_db_parameter_group(
541
+ db_parameter_group_name: @config["parameter_group_name"],
542
+ parameters: params
543
+ )
544
+ end
545
+ }
546
+ end
547
+ end
548
+
549
+ # Called automatically by {MU::Deploy#createResources}
550
+ def groom
551
+ cloud_desc(use_cache: false)
552
+ manageSubnetGroup if @vpc
553
+ manageDbParameterGroup(@config["create_cluster"], create: false)
554
+
555
+ noun = @config['create_cluster'] ? "cluster" : "instance"
556
+
557
+ mods = {
558
+ "db_#{noun}_identifier".to_sym => @cloud_id
559
+ }
560
+
561
+ basicParams.each_pair { |k, v|
562
+ next if v.nil? or !MODIFIABLE[noun].include?(k)
563
+ if cloud_desc.respond_to?(k) and cloud_desc.send(k) != v
564
+ mods[k] = v
565
+ end
566
+ }
567
+
568
+ existing_sgs = cloud_desc.vpc_security_groups.map { |sg|
569
+ sg.vpc_security_group_id
570
+ }.sort
571
+
572
+ if !@config["add_cluster_node"] and !@config["member_of_cluster"] and
573
+ @config["vpc_security_group_ids"] and
574
+ existing_sgs != @config["vpc_security_group_ids"].sort
575
+ mods[:vpc_security_group_ids] = @config["vpc_security_group_ids"]
576
+ end
577
+
578
+
579
+ if @config['cloudwatch_logs'] and cloud_desc.enabled_cloudwatch_logs_exports.sort != @config['cloudwatch_logs'].sort
580
+ mods[:cloudwatch_logs_export_configuration] = {
581
+ enable_log_types: @config['cloudwatch_logs'],
582
+ disable_log_types: cloud_desc.enabled_cloudwatch_logs_exports - @config['cloudwatch_logs']
583
+ }
584
+ end
585
+
586
+ if @config["create_cluster"]
587
+ @config['cluster_node_count'] ||= 1
588
+ if @config['cluster_mode'] == "serverless"
589
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).modify_current_db_cluster_capacity(
590
+ db_cluster_identifier: @cloud_id,
591
+ capacity: @config['cluster_node_count']
592
+ )
593
+ end
594
+ else
595
+ # Run SQL on deploy
596
+ if @config['run_sql_on_deploy']
597
+ run_sql_commands
598
+ end
599
+
600
+ if !cloud_desc.multi_az and (@config['multi_az_on_deploy'] or @config['multi_az_on_create'])
601
+ mods[:multi_az] = true
602
+ end
603
+
604
+ # XXX how do we guard this? do we?
605
+ # master_user_password: @config["password"],
606
+ # end
607
+
608
+ # XXX it's a stupid array
609
+ # db_parameter_group_name: @config["parameter_group_name"],
610
+ end
611
+
612
+ if mods.size > 1
613
+ MU.log "Modifying RDS instance #{@cloud_id}", MU::NOTICE, details: mods
614
+ mods[:apply_immediately] = true
615
+ wait_until_available
616
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @credentials).send("modify_db_#{noun}".to_sym, mods)
617
+ wait_until_available
618
+ end
619
+
620
+ end
621
+
622
+ # Generate database user, database identifier, database name based on engine-specific constraints
623
+ # @return [String]: Name
624
+ def self.getName(basename, type: 'dbname', config: nil)
625
+ if type == 'dbname'
626
+ # Apply engine-specific db name constraints
627
+ if config["engine"] =~ /^oracle/
628
+ (MU.seed.downcase+config["name"])[0..7]
629
+ elsif config["engine"] =~ /^sqlserver/
630
+ nil
631
+ elsif config["engine"] =~ /^mysql/
632
+ basename[0..63]
633
+ elsif config["engine"] =~ /^aurora/
634
+ (MU.seed.downcase+config["name"])[0..7]
635
+ else
636
+ basename
637
+ end
638
+ elsif type == 'dbuser'
639
+ # Apply engine-specific master username constraints
640
+ if config["engine"] =~ /^oracle/
641
+ basename[0..29].gsub(/[^a-z0-9]/i, "")
642
+ elsif config["engine"] =~ /^sqlserver/
643
+ basename[0..127].gsub(/[^a-z0-9]/i, "")
644
+ elsif config["engine"] =~ /^(mysql|maria)/
645
+ basename[0..15].gsub(/[^a-z0-9]/i, "")
646
+ elsif config["engine"] =~ /^aurora/
647
+ basename[0..15].gsub(/[^a-z0-9]/i, "")
648
+ else
649
+ basename.gsub(/[^a-z0-9]/i, "")
650
+ end
651
+ end
652
+ end
653
+
654
+ # Permit a host to connect to the given database instance.
655
+ # @param cidr [String]: The CIDR-formatted IP address or block to allow access.
656
+ # @return [void]
657
+ def allowHost(cidr)
658
+ # If we're an old, Classic-style database with RDS-specific
659
+ # authorization, punch holes in that.
660
+ if !cloud_desc.db_security_groups.empty?
661
+ cloud_desc.db_security_groups.each { |rds_sg|
662
+ begin
663
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).authorize_db_security_group_ingress(
664
+ db_security_group_name: rds_sg.db_security_group_name,
665
+ cidrip: cidr
666
+ )
667
+ rescue Aws::RDS::Errors::AuthorizationAlreadyExists
668
+ MU.log "CIDR #{cidr} already in database instance #{@cloud_id} security group", MU::WARN
669
+ end
670
+ }
671
+ end
672
+
673
+ # Otherwise go get our generic EC2 ruleset and punch a hole in it
674
+ myFirewallRules.each { |sg|
675
+ sg.addRule([cidr], proto: "tcp", port: cloud_desc.endpoint.port)
676
+ break
677
+ }
678
+ end
679
+
680
+ # Return the metadata for this ContainerCluster
681
+ # @return [Hash]
682
+ def notify
683
+ deploy_struct = MU.structToHash(cloud_desc, stringify_keys: true)
684
+ deploy_struct['cloud_id'] = @cloud_id
685
+ deploy_struct["region"] ||= @config['region']
686
+ deploy_struct["db_name"] ||= @config['db_name']
687
+ deploy_struct
688
+ end
689
+
690
+ # Generate a snapshot from the database described in this instance.
691
+ # @return [String]: The cloud provider's identifier for the snapshot.
692
+ def createNewSnapshot
693
+ snap_id = @deploy.getResourceName(@config["name"]) + Time.new.strftime("%M%S").to_s
694
+ src_ref = MU::Config::Ref.get(@config["source"])
695
+ src_ref.kitten(@deploy)
696
+ if !src_ref.id
697
+ raise MuError.new "#{@mu_name} failed to get an id from reference for creating a snapshot", details: @config['source']
698
+ end
699
+ params = {
700
+ :tags => @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
701
+ }
702
+ if @config["create_cluster"]
703
+ params[:db_cluster_snapshot_identifier] = snap_id
704
+ params[:db_cluster_identifier] = src_ref.id
705
+ else
706
+ params[:db_snapshot_identifier] = snap_id
707
+ params[:db_instance_identifier] = src_ref.id
708
+ end
709
+
710
+ MU.retrier([Aws::RDS::Errors::InvalidDBInstanceState, Aws::RDS::Errors::InvalidDBClusterStateFault], wait: 60, max: 10) {
711
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).send("create_db_#{@config['create_cluster'] ? "cluster_" : ""}snapshot".to_sym, params)
712
+ }
713
+
714
+ loop_if = Proc.new {
715
+ if @config["create_cluster"]
716
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).describe_db_cluster_snapshots(db_cluster_snapshot_identifier: snap_id).db_cluster_snapshots.first.status != "available"
717
+ else
718
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).describe_db_snapshots(db_snapshot_identifier: snap_id).db_snapshots.first.status != "available"
719
+ end
720
+ }
721
+
722
+ MU.retrier(wait: 15, loop_if: loop_if) { |retries, _wait|
723
+ MU.log "Waiting for RDS snapshot of #{src_ref.id} to be ready...", MU::NOTICE if retries % 20 == 0
724
+ }
725
+
726
+ return snap_id
727
+ end
728
+
729
+ # Fetch the latest snapshot of the database described in this instance.
730
+ # @return [String]: The cloud provider's identifier for the snapshot.
731
+ def getExistingSnapshot
732
+ src_ref = MU::Config::Ref.get(@config["source"])
733
+ resp =
734
+ if @config["create_cluster"]
735
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).describe_db_cluster_snapshots(db_cluster_snapshot_identifier: src_ref.id)
736
+ else
737
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).describe_db_snapshots(db_snapshot_identifier: src_ref.id)
738
+ end
739
+
740
+ snapshots = @config["create_cluster"] ? resp.db_cluster_snapshots : resp.db_snapshots
741
+
742
+ if snapshots.empty?
743
+ nil
744
+ else
745
+ sorted_snapshots = snapshots.sort_by { |snap| snap.snapshot_create_time }
746
+ @config["create_cluster"] ? sorted_snapshots.last.db_cluster_snapshot_identifier : sorted_snapshots.last.db_snapshot_identifier
747
+ end
748
+ end
749
+
750
+ # Does this resource type exist as a global (cloud-wide) artifact, or
751
+ # is it localized to a region/zone?
752
+ # @return [Boolean]
753
+ def self.isGlobal?
754
+ false
755
+ end
756
+
757
+ # Denote whether this resource implementation is experiment, ready for
758
+ # testing, or ready for production use.
759
+ def self.quality
760
+ MU::Cloud::RELEASE
761
+ end
762
+
763
+ # @return [Array<Thread>]
764
+ def self.threaded_resource_purge(describe_method, list_method, id_method, arn_type, region, credentials, ignoremaster, known: [], deploy_id: MU.deploy_id)
765
+ deletia = []
766
+
767
+ resp = MU::Cloud::AWS.rds(credentials: credentials, region: region).send(describe_method)
768
+ resp.send(list_method).each { |resource|
769
+ begin
770
+ arn = MU::Cloud::AWS::Database.getARN(resource.send(id_method), arn_type, "rds", region: region, credentials: credentials)
771
+ tags = MU::Cloud::AWS.rds(credentials: credentials, region: region).list_tags_for_resource(resource_name: arn).tag_list
772
+ rescue Aws::RDS::Errors::InvalidParameterValue
773
+ MU.log "Failed to fetch ARN of type #{arn_type} or tags of resource via #{id_method}", MU::WARN, details: [resource, arn]
774
+ next
775
+ end
776
+
777
+ if should_delete?(tags, resource.send(id_method), ignoremaster, deploy_id, MU.mu_public_ip, known)
778
+ deletia << resource.send(id_method)
779
+ end
780
+ }
781
+
782
+ threads = []
783
+ deletia.each { |id|
784
+ threads << Thread.new(id) { |resource_id|
785
+ yield(resource_id)
786
+ }
787
+ }
788
+
789
+ threads
790
+ end
791
+
792
+ # Called by {MU::Cleanup}. Locates resources that were created by the
793
+ # currently-loaded deployment, and purges them.
794
+ # @param noop [Boolean]: If true, will only print what would be done
795
+ # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
796
+ # @param region [String]: The cloud provider region in which to operate
797
+ # @return [void]
798
+ def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, region: MU.curRegion, flags: {})
799
+
800
+ threads = []
801
+
802
+ ["instance", "cluster"].each { |type|
803
+ threads.concat threaded_resource_purge("describe_db_#{type}s".to_sym, "db_#{type}s".to_sym, "db_#{type}_identifier".to_sym, (type == "instance" ? "db" : "cluster"), region, credentials, ignoremaster, known: flags['known'], deploy_id: deploy_id) { |id|
804
+ terminate_rds_instance(nil, noop: noop, skipsnapshots: flags["skipsnapshots"], region: region, deploy_id: deploy_id, cloud_id: id, mu_name: id.upcase, credentials: credentials, cluster: (type == "cluster"), known: flags['known'])
805
+
806
+ }
807
+ }
808
+ threads.each { |t|
809
+ t.join
810
+ }
811
+
812
+ threads = threaded_resource_purge(:describe_db_subnet_groups, :db_subnet_groups, :db_subnet_group_name, "subgrp", region, credentials, ignoremaster, known: flags['known'], deploy_id: deploy_id) { |id|
813
+ MU.log "Deleting RDS subnet group #{id}"
814
+ MU.retrier([Aws::RDS::Errors::InvalidDBSubnetGroupStateFault], wait: 30, max: 5, ignoreme: [Aws::RDS::Errors::DBSubnetGroupNotFoundFault]) {
815
+ MU::Cloud::AWS.rds(region: region).delete_db_subnet_group(db_subnet_group_name: id) if !noop
816
+ }
817
+ }
818
+
819
+ ["db", "db_cluster"].each { |type|
820
+ threads.concat threaded_resource_purge("describe_#{type}_parameter_groups".to_sym, "#{type}_parameter_groups".to_sym, "#{type}_parameter_group_name".to_sym, (type == "db" ? "pg" : "cluster-pg"), region, credentials, ignoremaster, known: flags['known'], deploy_id: deploy_id) { |id|
821
+ MU.log "Deleting RDS #{type} parameter group #{id}"
822
+ MU.retrier([Aws::RDS::Errors::InvalidDBParameterGroupState], wait: 30, max: 5, ignoreme: [Aws::RDS::Errors::DBParameterGroupNotFound]) {
823
+ MU::Cloud::AWS.rds(region: region).send("delete_#{type}_parameter_group", { "#{type}_parameter_group_name".to_sym => id }) if !noop
824
+ }
825
+ }
826
+ }
827
+
828
+ # Wait for all of the databases subnet/parameter groups to finish cleanup before proceeding
829
+ threads.each { |t|
830
+ t.join
831
+ }
832
+ end
833
+
834
+ # Cloud-specific configuration properties.
835
+ # @param _config [MU::Config]: The calling MU::Config object
836
+ # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
837
+ def self.schema(_config)
838
+ toplevel_required = []
839
+ rds_parameters_primitive = {
840
+ "type" => "array",
841
+ "minItems" => 1,
842
+ "items" => {
843
+ "description" => "The database parameter group parameter to change and when to apply the change.",
844
+ "type" => "object",
845
+ "title" => "Database Parameter",
846
+ "required" => ["name", "value"],
847
+ "additionalProperties" => false,
848
+ "properties" => {
849
+ "name" => {
850
+ "type" => "string"
851
+ },
852
+ "value" => {
853
+ "type" => "string"
854
+ },
855
+ "apply_method" => {
856
+ "enum" => ["pending-reboot", "immediate"],
857
+ "default" => "immediate",
858
+ "type" => "string"
859
+ }
860
+ }
861
+ }
862
+ }
863
+
864
+
865
+ schema = {
866
+ "db_parameter_group_parameters" => rds_parameters_primitive,
867
+ "cluster_parameter_group_parameters" => rds_parameters_primitive,
868
+ "parameter_group_family" => {
869
+ "type" => "String",
870
+ "description" => "An RDS parameter group family. See also https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithParamGroups.html"
871
+ },
872
+ "cluster_mode" => {
873
+ "type" => "string",
874
+ "description" => "The DB engine mode of the DB cluster",
875
+ "enum" => ["provisioned", "serverless", "parallelquery", "global", "multimaster"],
876
+ "default" => "provisioned"
877
+ },
878
+ "storage_type" => {
879
+ "enum" => ["standard", "gp2", "io1"],
880
+ "type" => "string",
881
+ "default" => "gp2"
882
+ },
883
+ "cloudwatch_logs" => {
884
+ "type" => "array",
885
+ "items" => {
886
+ "type" => "string",
887
+ "enum" => ["audit", "error", "general", "slowquery", "profiler", "postgresql", "alert", "listener", "trace", "upgrade", "agent"]
888
+ }
889
+ },
890
+ "serverless_scaling" => {
891
+ "type" => "object",
892
+ "description" => "Scaling configuration for a +serverless+ Aurora cluster",
893
+ "default" => {
894
+ "auto_pause" => false,
895
+ "min_capacity" => 2,
896
+ "max_capacity" => 2
897
+ },
898
+ "properties" => {
899
+ "auto_pause" => {
900
+ "type" => "boolean",
901
+ "description" => "A value that specifies whether to allow or disallow automatic pause for an Aurora DB cluster in serverless DB engine mode",
902
+ "default" => false
903
+ },
904
+ "min_capacity" => {
905
+ "type" => "integer",
906
+ "description" => "The minimum capacity for an Aurora DB cluster in serverless DB engine mode.",
907
+ "default" => 2,
908
+ "enum" => [2, 4, 8, 16, 32, 64, 128, 256]
909
+ },
910
+ "max_capacity" => {
911
+ "type" => "integer",
912
+ "description" => "The maximum capacity for an Aurora DB cluster in serverless DB engine mode.",
913
+ "default" => 2,
914
+ "enum" => [2, 4, 8, 16, 32, 64, 128, 256]
915
+ },
916
+ "seconds_until_auto_pause" => {
917
+ "type" => "integer",
918
+ "description" => "A DB cluster can be paused only when it's idle (it has no connections). If a DB cluster is paused for more than seven days, the DB cluster might be backed up with a snapshot. In this case, the DB cluster is restored when there is a request to connect to it.",
919
+ "default" => 86400
920
+ }
921
+ }
922
+ },
923
+ "license_model" => {
924
+ "type" => "string",
925
+ "enum" => ["license-included", "bring-your-own-license", "general-public-license", "postgresql-license"]
926
+ },
927
+ "ingress_rules" => MU::Cloud.resourceClass("AWS", "FirewallRule").ingressRuleAddtlSchema
928
+ }
929
+ [toplevel_required, schema]
930
+ end
931
+
932
+ @@engine_cache= {}
933
+ def self.get_supported_engines(region = MU.myRegion, credentials = nil, engine: nil)
934
+ @@engine_cache ||= {}
935
+ @@engine_cache[credentials] ||= {}
936
+ @@engine_cache[credentials][region] ||= {}
937
+
938
+ if !@@engine_cache[credentials][region].empty?
939
+ return engine ? @@engine_cache[credentials][region][engine] : @@engine_cache[credentials][region]
940
+ end
941
+
942
+ engines = {}
943
+
944
+ resp = MU::Cloud::AWS.rds(credentials: credentials, region: region).describe_db_engine_versions
945
+
946
+ if resp and resp.db_engine_versions
947
+ resp.db_engine_versions.each { |version|
948
+ engines[version.engine] ||= {
949
+ "versions" => [],
950
+ "families" => [],
951
+ "features" => {},
952
+ "raw" => {}
953
+ }
954
+ engines[version.engine]['versions'] << version.engine_version
955
+ engines[version.engine]['families'] << version.db_parameter_group_family
956
+ engines[version.engine]['raw'][version.engine_version] = version
957
+ [:supports_read_replica, :supports_log_exports_to_cloudwatch_logs].each { |feature|
958
+ if version.respond_to?(feature) and version.send(feature) == true
959
+ engines[version.engine]['features'][version.engine_version] ||= []
960
+ engines[version.engine]['features'][version.engine_version] << feature
961
+ end
962
+ }
963
+
964
+ }
965
+ engines.each_key { |e|
966
+ engines[e]["versions"].uniq!
967
+ engines[e]["versions"].sort! { |a, b| MU.version_sort(a, b) }
968
+ engines[e]["families"].uniq!
969
+ }
970
+
971
+ else
972
+ MU.log "Failed to get list of valid RDS engine versions in #{db['region']}, proceeding without proper validation", MU::WARN
973
+ end
974
+
975
+ @@engine_cache[credentials][region] = engines
976
+ return engine ? @@engine_cache[credentials][region][engine] : @@engine_cache[credentials][region]
977
+ end
978
+ private_class_method :get_supported_engines
979
+
980
+ # Make sure any source database/cluster/snapshot we've asked for exists
981
+ # and is valid.
982
+ def self.validate_source_data(db)
983
+ ok = true
984
+
985
+ if db['creation_style'] == "existing_snapshot" and
986
+ !db['create_cluster'] and
987
+ db['source'] and db["source"]["id"] and db['source']["id"].match(/:cluster-snapshot:/)
988
+ MU.log "Database #{db['name']}: Existing snapshot #{db["source"]["id"]} looks like a cluster snapshot, but create_cluster is not set. Add 'create_cluster: true' if you're building an RDS cluster.", MU::ERR
989
+ ok = false
990
+ elsif db["creation_style"] == "existing" or db["creation_style"] == "new_snapshot"
991
+ begin
992
+ MU::Cloud::AWS.rds(region: db['region']).describe_db_instances(
993
+ db_instance_identifier: db['source']['id']
994
+ )
995
+ rescue Aws::RDS::Errors::DBInstanceNotFound
996
+ MU.log "Source database was specified for #{db['name']}, but no such database exists in #{db['region']}", MU::ERR, db['source']
997
+ ok = false
998
+ end
999
+ end
1000
+
1001
+ ok
1002
+ end
1003
+ private_class_method :validate_source_data
1004
+
1005
+ def self.validate_master_password(db)
1006
+ maxlen = case db['engine']
1007
+ when "mariadb", "mysql"
1008
+ 41
1009
+ when "postgresql"
1010
+ 41
1011
+ when /oracle/
1012
+ 30
1013
+ when /sqlserver/
1014
+ 128
1015
+ else
1016
+ return true
1017
+ end
1018
+
1019
+ pw = if !db['password'].nil?
1020
+ db['password']
1021
+ elsif db['auth_vault'] and !db['auth_vault'].empty?
1022
+ groomclass = MU::Groomer.loadGroomer(db['groomer'])
1023
+ pw = groomclass.getSecret(
1024
+ vault: db['auth_vault']['vault'],
1025
+ item: db['auth_vault']['item'],
1026
+ field: db['auth_vault']['password_field']
1027
+ )
1028
+ return true if pw.nil?
1029
+ pw
1030
+ end
1031
+
1032
+ if pw and (pw.length < 8 or pw.match(/[\/\\@\s]/) or pw.length > maxlen)
1033
+ MU.log "Database password specified in 'password' or 'auth_vault' doesn't meet RDS requirements. Must be between 8 and #{maxlen} chars and have only ASCII characters other than /, @, \", or [space].", MU::ERR
1034
+ return false
1035
+ end
1036
+
1037
+ true
1038
+ end
1039
+ private_class_method :validate_master_password
1040
+
1041
+ # Cloud-specific pre-processing of {MU::Config::BasketofKittens::databases}, bare and unvalidated.
1042
+ # @param db [Hash]: The resource to process and validate
1043
+ # @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a ember
1044
+ # @return [Boolean]: True if validation succeeded, False otherwise
1045
+ def self.validateConfig(db, _configurator)
1046
+ ok = true
1047
+
1048
+ ok = false if !validate_source_data(db)
1049
+
1050
+ ok = false if !validate_engine(db)
1051
+
1052
+ ok = false if !valid_read_replica?(db)
1053
+
1054
+ ok = false if !valid_cloudwatch_logs?(db)
1055
+
1056
+ db["license_model"] ||=
1057
+ if ["postgres", "postgresql", "aurora-postgresql"].include?(db["engine"])
1058
+ "postgresql-license"
1059
+ elsif ["mysql", "mariadb"].include?(db["engine"])
1060
+ "general-public-license"
1061
+ else
1062
+ "license-included"
1063
+ end
1064
+
1065
+ ok = false if !validate_master_password(db)
1066
+
1067
+ if db["multi_az_on_create"] and db["multi_az_on_deploy"]
1068
+ MU.log "Both of multi_az_on_create and multi_az_on_deploy cannot be true", MU::ERR
1069
+ ok = false
1070
+ end
1071
+
1072
+ if (db["db_parameter_group_parameters"] or db["cluster_parameter_group_parameters"]) and db["parameter_group_family"].nil?
1073
+ engine = get_supported_engines(db['region'], db['credentials'], engine: db['engine'])
1074
+ db["parameter_group_family"] = engine['raw'][db['engine_version']].db_parameter_group_family
1075
+ end
1076
+
1077
+ # Adding rules for Database instance storage. This varies depending on storage type and database type.
1078
+ if !db["storage"].nil? and !db["create_cluster"] and !db["add_cluster_node"] and !STORAGE_RANGES[db["storage_type"]][db['engine']].include?(db["storage"])
1079
+ MU.log "Database storage size is set to #{db["storage"]}. #{db["engine"]} only supports storage sizes from #{STORAGE_RANGES[db["storage_type"]][db['engine']]} GB for #{db["storage_type"]} volumes.", MU::ERR
1080
+ ok = false
1081
+ end
1082
+
1083
+ ok = false if !validate_network_cfg(db)
1084
+
1085
+ ok
1086
+ end
1087
+
1088
+ private
1089
+
1090
+ def genericParams
1091
+ params = if @config['create_cluster']
1092
+ paramhash = {
1093
+ db_cluster_identifier: @cloud_id,
1094
+ engine: @config["engine"],
1095
+ vpc_security_group_ids: @config["vpc_security_group_ids"],
1096
+ tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
1097
+ }
1098
+
1099
+ if @vpc and @config["subnet_group_name"]
1100
+ paramhash[:db_subnet_group_name] = @config["subnet_group_name"]
1101
+ end
1102
+
1103
+ if @config['cloudwatch_logs']
1104
+ paramhash[:enable_cloudwatch_logs_exports ] = @config['cloudwatch_logs']
1105
+ end
1106
+ if @config['cluster_mode']
1107
+ paramhash[:engine_mode] = @config['cluster_mode']
1108
+ if @config['cluster_mode'] == "serverless"
1109
+ paramhash[:scaling_configuration] = {
1110
+ :auto_pause => @config['serverless_scaling']['auto_pause'],
1111
+ :min_capacity => @config['serverless_scaling']['min_capacity'],
1112
+ :max_capacity => @config['serverless_scaling']['max_capacity'],
1113
+ :seconds_until_auto_pause => @config['serverless_scaling']['seconds_until_auto_pause']
1114
+ }
1115
+ end
1116
+ end
1117
+ paramhash
1118
+ else
1119
+ {
1120
+ db_instance_identifier: @cloud_id,
1121
+ db_instance_class: @config["size"],
1122
+ engine: @config["engine"],
1123
+ auto_minor_version_upgrade: @config["auto_minor_version_upgrade"],
1124
+ license_model: @config["license_model"],
1125
+ db_subnet_group_name: @config["subnet_group_name"],
1126
+ vpc_security_group_ids: @config["vpc_security_group_ids"],
1127
+ publicly_accessible: @config["publicly_accessible"],
1128
+ copy_tags_to_snapshot: true,
1129
+ tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
1130
+ }
1131
+ end
1132
+
1133
+ if %w{existing_snapshot new_snapshot}.include?(@config["creation_style"])
1134
+ if @config['create_cluster']
1135
+ params[:snapshot_identifier] = @config["snapshot_id"]
1136
+ else
1137
+ params[:db_snapshot_identifier] = @config["snapshot_id"]
1138
+ end
1139
+ end
1140
+
1141
+ params
1142
+ end
1143
+
1144
+
1145
+ def self.validate_network_cfg(db)
1146
+ ok = true
1147
+
1148
+ if !db['vpc']
1149
+ db["vpc"] = MU::Cloud.resourceClass("AWS", "VPC").defaultVpc(db['region'], db['credentials'])
1150
+ if db['vpc'] and !(db['engine'].match(/sqlserver/) and db['create_read_replica'])
1151
+ MU.log "Using default VPC for database '#{db['name']}; this sets 'publicly_accessible' to true.", MU::WARN
1152
+ db['publicly_accessible'] = true
1153
+ end
1154
+ else
1155
+ if db["vpc"]["subnet_pref"] == "all_public" and !db['publicly_accessible'] and (db["vpc"]['subnets'].nil? or db["vpc"]['subnets'].empty?)
1156
+ MU.log "Setting publicly_accessible to true on database '#{db['name']}', since deploying into public subnets.", MU::WARN
1157
+ db['publicly_accessible'] = true
1158
+ elsif db["vpc"]["subnet_pref"] == "all_private" and db['publicly_accessible']
1159
+ MU.log "Setting publicly_accessible to false on database '#{db['name']}', since deploying into private subnets.", MU::NOTICE
1160
+ db['publicly_accessible'] = false
1161
+ end
1162
+ if db['engine'].match(/sqlserver/) and db['create_read_replica']
1163
+ MU.log "SQL Server does not support read replicas in VPC deployments", MU::ERR
1164
+ ok = false
1165
+ end
1166
+ end
1167
+
1168
+ ok
1169
+ end
1170
+ private_class_method :validate_network_cfg
1171
+
1172
+ def self.valid_read_replica?(db)
1173
+ if !db['create_read_replica'] and !db['read_replica_of']
1174
+ return true
1175
+ end
1176
+
1177
+ engine = get_supported_engines(db['region'], db['credentials'], engine: db['engine'])
1178
+ if engine.nil? or !engine['features'] or !engine['features'][db['engine_version']]
1179
+ return true # we can't be sure, so let the API sort it out later
1180
+ end
1181
+
1182
+ if !engine['features'][db['engine_version']].include?(:supports_read_replica)
1183
+ MU.log "Engine #{db['engine']} #{db['engine_version']} does not appear to support read replicas", MU::ERR
1184
+ return false
1185
+ end
1186
+ true
1187
+ end
1188
+ private_class_method :valid_read_replica?
1189
+
1190
+ def self.valid_cloudwatch_logs?(db)
1191
+ return true if !db['cloudwatch_logs']
1192
+ engine = get_supported_engines(db['region'], db['credentials'], engine: db['engine'])
1193
+ if engine.nil? or !engine['features'] or !engine['features'][db['engine_version']] or !engine['features'][db['engine_version']].include?(:supports_read_replica)
1194
+ MU.log "CloudWatch Logs not supported for #{db['engine']} #{db['engine_version']}", MU::ERR
1195
+ return false
1196
+ end
1197
+
1198
+ ok = true
1199
+ db['cloudwatch_logs'].each { |logtype|
1200
+ if !engine['raw'][db['engine_version']].exportable_log_types.include?(logtype)
1201
+ ok = false
1202
+ MU.log "CloudWatch Log type #{logtype} is not valid for #{db['engine']} #{db['engine_version']}. List of valid types:", MU::ERR, details: engine['raw'][db['engine_version']].exportable_log_types
1203
+ end
1204
+ }
1205
+
1206
+ ok
1207
+ end
1208
+ private_class_method :valid_cloudwatch_logs?
1209
+
1210
+ def self.validate_engine(db)
1211
+ ok = true
1212
+
1213
+ if db['create_cluster'] or db["member_of_cluster"] or db["add_cluster_node"] or (db['engine'] and db['engine'].match(/aurora/))
1214
+ case db['engine']
1215
+ when "mysql", "aurora", "aurora-mysql"
1216
+ if (db['engine_version'] and db["engine_version"].match(/^5\.6/)) or db["cluster_mode"] == "serverless"
1217
+ db["engine"] = "aurora"
1218
+ db["engine_version"] = "5.6"
1219
+ db['publicly_accessible'] = false
1220
+ else
1221
+ db["engine"] = "aurora-mysql"
1222
+ end
1223
+ when /postgres/
1224
+ db["engine"] = "aurora-postgresql"
1225
+ else
1226
+ ok = false
1227
+ MU.log "#{db['engine']} is not supported for clustering", MU::ERR
1228
+ end
1229
+ db["create_cluster"] = true if !(db["member_of_cluster"] or db["add_cluster_node"])
1230
+ end
1231
+
1232
+ db["engine"] = "oracle-se2" if db["engine"] == "oracle"
1233
+ db["engine"] = "sqlserver-ex" if db["engine"] == "sqlserver"
1234
+
1235
+ engine_cfg = get_supported_engines(db['region'], db['credentials'], engine: db['engine'])
1236
+
1237
+ if !engine_cfg or engine_cfg['versions'].empty? or engine_cfg['families'].empty?
1238
+ MU.log "RDS engine #{db['engine']} reports no supported versions in #{db['region']}", MU::ERR, details: engine_cfg
1239
+ return false
1240
+ end
1241
+
1242
+ # Resolve or default our engine version to something reasonable
1243
+ db['engine_version'] ||= engine_cfg['versions'].last
1244
+ if !engine_cfg['versions'].include?(db["engine_version"])
1245
+ db['engine_version'] = engine_cfg['versions'].grep(/^#{Regexp.quote(db["engine_version"])}/).last
1246
+ end
1247
+ if !engine_cfg['versions'].include?(db["engine_version"])
1248
+ MU.log "RDS engine '#{db['engine']}' version '#{db['engine_version']}' is not supported in #{db['region']}", MU::ERR, details: { "Known-good versions:" => engine_cfg['versions'].uniq.sort }
1249
+ ok = false
1250
+ end
1251
+
1252
+ if db["parameter_group_family"] and
1253
+ !engine_cfg['families'].include?(db['parameter_group_family'])
1254
+ MU.log "RDS engine '#{db['engine']}' parameter group family '#{db['parameter_group_family']}' is not supported.", MU::ERR, details: engine_cfg['families'].uniq.sort
1255
+ ok = false
1256
+ end
1257
+
1258
+ ok
1259
+ end
1260
+ private_class_method :validate_engine
1261
+
1262
+ def add_basic
1263
+
1264
+ getPassword
1265
+ if @config['source'].nil? or @config['region'] != @config['source'].region
1266
+ manageSubnetGroup if @vpc
1267
+ else
1268
+ MU.log "Note: Read Replicas automatically reside in the same subnet group as the source database, if they're both in the same region. This replica may not land in the VPC you intended.", MU::WARN
1269
+ end
1270
+
1271
+ if @config.has_key?("parameter_group_family")
1272
+ manageDbParameterGroup
1273
+ end
1274
+
1275
+ createDb
1276
+ end
1277
+
1278
+
1279
+ def add_cluster_node
1280
+ cluster = MU::Config::Ref.get(@config["member_of_cluster"]).kitten(@deploy)
1281
+ if cluster.nil? or cluster.cloud_id.nil?
1282
+ raise MuError.new "Failed to resolve parent cluster of #{@mu_name}", details: @config["member_of_cluster"].to_h
1283
+ end
1284
+
1285
+ @config['cluster_identifier'] = cluster.cloud_id.downcase
1286
+
1287
+ # We're overriding @config["subnet_group_name"] because we need each cluster member to use the cluster's subnet group instead of a unique subnet group
1288
+ @config["subnet_group_name"] = cluster.cloud_desc.db_subnet_group if @vpc
1289
+ @config["creation_style"] = "new" if @config["creation_style"] != "new"
1290
+ if @config.has_key?("parameter_group_family")
1291
+ manageDbParameterGroup
1292
+ end
1293
+
1294
+ createDb
1295
+ end
1296
+
1297
+ def basicParams
1298
+ params = genericParams
1299
+ params[:storage_encrypted] = @config["storage_encrypted"]
1300
+ params[:master_user_password] = @config['password']
1301
+ params[:engine_version] = @config["engine_version"]
1302
+ params[:vpc_security_group_ids] = @config["vpc_security_group_ids"]
1303
+ params[:preferred_maintenance_window] = @config["preferred_maintenance_window"] if @config["preferred_maintenance_window"]
1304
+ params[:backup_retention_period] = @config["backup_retention_period"] if @config["backup_retention_period"]
1305
+
1306
+ if @config['create_cluster']
1307
+ params[:database_name] = @config["db_name"]
1308
+ params[:db_cluster_parameter_group_name] = @config["parameter_group_name"] if @config["parameter_group_name"]
1309
+ else
1310
+ params[:enable_cloudwatch_logs_exports] = @config['cloudwatch_logs'] if @config['cloudwatch_logs'] and !@config['cloudwatch_logs'].empty?
1311
+ params[:db_name] = @config["db_name"] if !@config['add_cluster_node']
1312
+ params[:db_parameter_group_name] = @config["parameter_group_name"] if @config["parameter_group_name"]
1313
+ end
1314
+
1315
+ if @config['create_cluster'] or @config['add_cluster_node']
1316
+ params[:db_cluster_identifier] = @config["cluster_identifier"]
1317
+ else
1318
+ params[:storage_type] = @config["storage_type"]
1319
+ params[:allocated_storage] = @config["storage"]
1320
+ params[:multi_az] = @config['multi_az_on_create']
1321
+ end
1322
+
1323
+ noun = @config['create_cluster'] ? "cluster" : "instance"
1324
+
1325
+ if noun == "cluster" or !params[:db_cluster_identifier]
1326
+ params[:backup_retention_period] = @config["backup_retention_period"]
1327
+ params[:preferred_backup_window] = @config["preferred_backup_window"]
1328
+ params[:master_username] = @config['master_user']
1329
+ params[:port] = @config["port"] if @config["port"]
1330
+ params[:iops] = @config["iops"] if @config['storage_type'] == "io1"
1331
+ end
1332
+
1333
+ params
1334
+ end
1335
+
1336
+ # creation_style = new, existing, new_snapshot, existing_snapshot
1337
+ def create_basic
1338
+ params = basicParams
1339
+
1340
+ clean_parent_opts = Proc.new {
1341
+ [:storage_encrypted, :master_user_password, :engine_version, :allocated_storage, :backup_retention_period, :preferred_backup_window, :master_username, :db_name, :database_name].each { |p| params.delete(p) }
1342
+ }
1343
+
1344
+ noun = @config["create_cluster"] ? "cluster" : "instance"
1345
+
1346
+ MU.retrier([Aws::RDS::Errors::InvalidParameterValue, Aws::RDS::Errors::DBSubnetGroupNotFoundFault], max: 10, wait: 15) {
1347
+ if %w{existing_snapshot new_snapshot}.include?(@config["creation_style"])
1348
+ clean_parent_opts.call
1349
+ MU.log "Creating database #{noun} #{@cloud_id} from snapshot #{@config["snapshot_id"]}"
1350
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).send("restore_db_#{noun}_from_#{noun == "instance" ? "db_" : ""}snapshot".to_sym, params)
1351
+ else
1352
+ clean_parent_opts.call if noun == "instance" and params[:db_cluster_identifier]
1353
+ MU.log "Creating pristine database #{noun} #{@cloud_id} (#{@config['name']}) in #{@config['region']}", MU::NOTICE, details: params
1354
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).send("create_db_#{noun}".to_sym, params)
1355
+ end
1356
+ }
1357
+ end
1358
+
1359
+ # creation_style = point_in_time
1360
+ def create_point_in_time
1361
+ @config["source"].kitten(@deploy)
1362
+ if !@config["source"].id
1363
+ raise MuError.new "Database '#{@config['name']}' couldn't resolve cloud id for source database", details: @config["source"].to_h
1364
+ end
1365
+
1366
+ params = genericParams
1367
+ params.delete(:db_instance_identifier)
1368
+ if @config['create_cluster']
1369
+ params[:source_db_cluster_identifier] = @config["source"].id
1370
+ params[:restore_to_time] = @config["restore_time"] unless @config["restore_time"] == "latest"
1371
+ else
1372
+ params[:source_db_instance_identifier] = @config["source"].id
1373
+ params[:target_db_instance_identifier] = @cloud_id
1374
+ end
1375
+ params[:restore_time] = @config['restore_time'] unless @config["restore_time"] == "latest"
1376
+ params[:use_latest_restorable_time] = true if @config['restore_time'] == "latest"
1377
+
1378
+
1379
+ MU.retrier([Aws::RDS::Errors::InvalidParameterValue], max: 15, wait: 20) {
1380
+ MU.log "Creating database #{@config['create_cluster'] ? "cluster" : "instance" } #{@cloud_id} based on point in time backup '#{@config['restore_time']}' of #{@config['source'].id}"
1381
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).send("restore_db_#{@config['create_cluster'] ? "cluster" : "instance"}_to_point_in_time".to_sym, params)
1382
+ }
1383
+ end
1384
+
1385
+ # creation_style = new, existing and read_replica_of is not nil
1386
+ def create_read_replica
1387
+ @config["source"].kitten(@deploy)
1388
+ if !@config["source"].id
1389
+ raise MuError.new "Database '#{@config['name']}' couldn't resolve cloud id for source database", details: @config["source"].to_h
1390
+ end
1391
+
1392
+ params = {
1393
+ db_instance_identifier: @cloud_id,
1394
+ source_db_instance_identifier: @config["source"].id,
1395
+ db_instance_class: @config["size"],
1396
+ auto_minor_version_upgrade: @config["auto_minor_version_upgrade"],
1397
+ publicly_accessible: @config["publicly_accessible"],
1398
+ tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } },
1399
+ db_subnet_group_name: @config["subnet_group_name"],
1400
+ storage_type: @config["storage_type"]
1401
+ }
1402
+ if @config["source"].region and @config['region'] != @config["source"].region
1403
+ params[:source_db_instance_identifier] = MU::Cloud::AWS::Database.getARN(@config["source"].id, "db", "rds", region: @config["source"].region, credentials: @config['credentials'])
1404
+ end
1405
+
1406
+ params[:port] = @config["port"] if @config["port"]
1407
+ params[:iops] = @config["iops"] if @config['storage_type'] == "io1"
1408
+
1409
+ on_retry = Proc.new { |e|
1410
+ if e.class == Aws::RDS::Errors::DBSubnetGroupNotAllowedFault
1411
+ MU.log "Being forced to use source database's subnet group: #{e.message}", MU::WARN
1412
+ params.delete(:db_subnet_group_name)
1413
+ end
1414
+ }
1415
+
1416
+ MU.retrier([Aws::RDS::Errors::InvalidDBInstanceState, Aws::RDS::Errors::InvalidParameterValue, Aws::RDS::Errors::DBSubnetGroupNotAllowedFault], max: 10, wait: 30, on_retry: on_retry) {
1417
+ MU.log "Creating read replica database instance #{@cloud_id} for #{@config['source'].id}"
1418
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).create_db_instance_read_replica(params)
1419
+ }
1420
+ end
1421
+
1422
+ # Sit on our hands until we show as available
1423
+ def wait_until_available
1424
+ loop_if = if @config["create_cluster"]
1425
+ Proc.new { cloud_desc(use_cache: false).status != "available" }
1426
+ else
1427
+ Proc.new { cloud_desc(use_cache: false).db_instance_status != "available" }
1428
+ end
1429
+ MU.retrier(wait: 10, max: 360, loop_if: loop_if) { |retries, _wait|
1430
+ if retries > 0 and retries % 20 == 0
1431
+ MU.log "Waiting for RDS #{@config['create_cluster'] ? "cluster" : "database" } #{@cloud_id} to be ready...", MU::NOTICE
1432
+ end
1433
+ }
1434
+ end
1435
+
1436
+ def do_naming
1437
+ if @config["create_cluster"]
1438
+ MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(name: cloud_desc.db_cluster_identifier, target: "#{cloud_desc.endpoint}.", cloudclass: MU::Cloud::Database, sync_wait: @config['dns_sync_wait'])
1439
+ MU.log "Database cluster #{@config['name']} is at #{cloud_desc.endpoint}", MU::SUMMARY
1440
+ else
1441
+ MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(name: cloud_desc.db_instance_identifier, target: "#{cloud_desc.endpoint.address}.", cloudclass: MU::Cloud::Database, sync_wait: @config['dns_sync_wait'])
1442
+ MU.log "Database #{@config['name']} is at #{cloud_desc.endpoint.address}", MU::SUMMARY
1443
+ end
1444
+ if @config['auth_vault']
1445
+ MU.log "knife vault show #{@config['auth_vault']['vault']} #{@config['auth_vault']['item']} for Database #{@config['name']} credentials", MU::SUMMARY
1446
+ end
1447
+ end
1448
+
1449
+ # Create a plain database instance or read replica, as described in our
1450
+ # +@config+.
1451
+ # @return [String]: The cloud provider's identifier for this database instance.
1452
+ def createDb
1453
+
1454
+ if @config['creation_style'] == "point_in_time"
1455
+ create_point_in_time
1456
+ elsif @config['read_replica_of']
1457
+ create_read_replica
1458
+ else
1459
+ create_basic
1460
+ end
1461
+
1462
+ wait_until_available
1463
+ do_naming
1464
+
1465
+ # If referencing an existing DB, insert this deploy's DB security group so it can access the thing
1466
+ if @config["creation_style"] == 'existing'
1467
+ mod_config = {}
1468
+ mod_config[:db_instance_identifier] = @cloud_id
1469
+ mod_config[:vpc_security_group_ids] = cloud_desc.vpc_security_groups.map { |sg| sg.vpc_security_group_id }
1470
+
1471
+ localdeploy_rule = @deploy.findLitterMate(type: "firewall_rule", name: "database"+@config['name'])
1472
+ if localdeploy_rule.nil?
1473
+ raise MU::MuError, "Database #{@config['name']} failed to find its generic security group 'database#{@config['name']}'"
1474
+ end
1475
+ mod_config[:vpc_security_group_ids] << localdeploy_rule.cloud_id
1476
+
1477
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).modify_db_instance(mod_config)
1478
+ MU.log "Modified database #{@cloud_id} with new security groups: #{mod_config}", MU::NOTICE
1479
+ end
1480
+
1481
+ # When creating from a snapshot or replicating an existing database,
1482
+ # some of the create arguments that we'd want to carry over aren't
1483
+ # applicable- but we can apply them after the fact with a modify.
1484
+ if %w{existing_snapshot new_snapshot point_in_time}.include?(@config["creation_style"]) or @config["read_replica_of"]
1485
+ mod_config = {
1486
+ db_instance_identifier: @cloud_id,
1487
+ apply_immediately: true
1488
+ }
1489
+ if !@config["read_replica_of"] or @config['region'] == @config['source'].region
1490
+ mod_config[:vpc_security_group_ids] = @config["vpc_security_group_ids"]
1491
+ end
1492
+
1493
+ if !@config["read_replica_of"]
1494
+ mod_config[:preferred_backup_window] = @config["preferred_backup_window"]
1495
+ mod_config[:backup_retention_period] = @config["backup_retention_period"]
1496
+ mod_config[:engine_version] = @config["engine_version"]
1497
+ mod_config[:allow_major_version_upgrade] = @config["allow_major_version_upgrade"] if @config['allow_major_version_upgrade']
1498
+ mod_config[:db_parameter_group_name] = @config["parameter_group_name"] if @config["parameter_group_name"]
1499
+ mod_config[:master_user_password] = @config['password']
1500
+ mod_config[:allocated_storage] = @config["storage"] if @config["storage"]
1501
+ end
1502
+ if @config["preferred_maintenance_window"]
1503
+ mod_config[:preferred_maintenance_window] = @config["preferred_maintenance_window"]
1504
+ end
1505
+
1506
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).modify_db_instance(mod_config)
1507
+ wait_until_available
1508
+ end
1509
+
1510
+ # Maybe wait for DB instance to be in available state. DB should still be writeable at this state
1511
+ if @config['allow_major_version_upgrade'] && @config["creation_style"] == "new"
1512
+ MU.log "Setting major database version upgrade on #{@cloud_id}'"
1513
+
1514
+ MU::Cloud::AWS.rds(region: @config['region'], credentials: @config['credentials']).modify_db_instance(
1515
+ db_instance_identifier: @cloud_id,
1516
+ apply_immediately: true,
1517
+ allow_major_version_upgrade: true
1518
+ )
1519
+ end
1520
+
1521
+ MU.log "Database #{@config['name']} (#{@mu_name}) is ready to use"
1522
+ @cloud_id
1523
+ end
1524
+
1525
+ def run_sql_commands
1526
+ MU.log "Running initial SQL commands on #{@config['name']}", details: @config['run_sql_on_deploy']
1527
+
1528
+ port = address = nil
1529
+
1530
+ if !cloud_desc.publicly_accessible and @vpc
1531
+ if @config['vpc']['nat_host_name']
1532
+ keypairname, _ssh_private_key, _ssh_public_key = @deploy.SSHKey
1533
+ begin
1534
+ gateway = Net::SSH::Gateway.new(
1535
+ @config['vpc']['nat_host_name'],
1536
+ @config['vpc']['nat_ssh_user'],
1537
+ :keys => [Etc.getpwuid(Process.uid).dir+"/.ssh"+"/"+keypairname],
1538
+ :keys_only => true,
1539
+ :auth_methods => ['publickey']
1540
+ )
1541
+ port = gateway.open(cloud_desc.endpoint.address, cloud_desc.endpoint.port)
1542
+ address = "127.0.0.1"
1543
+ MU.log "Tunneling #{@config['engine']} connection through #{@config['vpc']['nat_host_name']} via local port #{port}", MU::DEBUG
1544
+ rescue IOError => e
1545
+ MU.log "Got #{e.inspect} while connecting to #{@mu_name} through NAT #{@config['vpc']['nat_host_name']}", MU::ERR
1546
+ return
1547
+ end
1548
+ else
1549
+ MU.log "Can't run initial SQL commands! Database #{@mu_name} is not publicly accessible, but we have no NAT host for connecting to it", MU::WARN, details: @config['run_sql_on_deploy']
1550
+ return
1551
+ end
1552
+ else
1553
+ port = database.endpoint.port
1554
+ address = database.endpoint.address
1555
+ end
1556
+
1557
+ # Running SQL on deploy
1558
+ if @config['engine'] =~ /postgres/
1559
+ MU::Cloud::AWS::Database.run_sql_postgres(address, port, @config['master_user'], @config['password'], cloud_desc.db_name, @config['run_sql_on_deploy'], @config['name'])
1560
+ elsif @config['engine'] =~ /mysql|maria/
1561
+ MU::Cloud::AWS::Database.run_sql_mysql(address, port, @config['master_user'], @config['password'], cloud_desc.db_name, @config['run_sql_on_deploy'], @config['name'])
1562
+ end
1563
+
1564
+ # close the SQL on deploy sessions
1565
+ if !cloud_desc.publicly_accessible
1566
+ begin
1567
+ gateway.close(port)
1568
+ rescue IOError => e
1569
+ MU.log "Failed to close ssh session to NAT after running sql_on_deploy", MU::ERR, details: e.inspect
1570
+ end
1571
+ end
1572
+ end
1573
+
1574
+ def self.run_sql_postgres(address, port, user, password, db, cmds = [], identifier = nil)
1575
+ identifier ||= address
1576
+ MU.log "Initiating postgres connection to #{address}:#{port} as #{user}"
1577
+ autoload :PG, 'pg'
1578
+ begin
1579
+ conn = PG::Connection.new(
1580
+ :host => address,
1581
+ :port => port,
1582
+ :user => user,
1583
+ :password => password,
1584
+ :dbname => db
1585
+ )
1586
+ cmds.each { |cmd|
1587
+ MU.log "Running #{cmd} on database #{identifier}"
1588
+ conn.exec(cmd)
1589
+ }
1590
+ conn.finish
1591
+ rescue PG::Error => e
1592
+ MU.log "Failed to run initial SQL commands on #{identifier} via #{address}:#{port}: #{e.inspect}", MU::WARN, details: conn
1593
+ end
1594
+ end
1595
+ private_class_method :run_sql_postgres
1596
+
1597
+ def self.run_sql_mysql(address, port, user, password, db, cmds = [], identifier = nil)
1598
+ identifier ||= address
1599
+ autoload :Mysql, 'mysql'
1600
+ MU.log "Initiating mysql connection to #{address}:#{port} as #{user}"
1601
+ conn = Mysql.new(address, user, password, db, port)
1602
+ cmds.each { |cmd|
1603
+ MU.log "Running #{cmd} on database #{identifier}"
1604
+ conn.query(cmd)
1605
+ }
1606
+ conn.close
1607
+ end
1608
+ private_class_method :run_sql_mysql
1609
+
1610
+ def self.should_delete?(tags, cloud_id, ignoremaster = false, deploy_id = MU.deploy_id, master_ip = MU.mu_public_ip, known = [])
1611
+
1612
+ found_muid = false
1613
+ found_master = false
1614
+ tags.each { |tag|
1615
+ found_muid = true if tag.key == "MU-ID" && tag.value == deploy_id
1616
+ found_master = true if tag.key == "MU-MASTER-IP" && tag.value == master_ip
1617
+ }
1618
+ delete =
1619
+ if ignoremaster && found_muid
1620
+ true
1621
+ elsif !ignoremaster && found_muid && found_master
1622
+ true
1623
+ elsif known and cloud_id and known.include?(cloud_id)
1624
+ true
1625
+ else
1626
+ false
1627
+ end
1628
+ delete
1629
+ end
1630
+ private_class_method :should_delete?
1631
+
1632
+ # Remove an RDS database and associated artifacts
1633
+ # @param db [OpenStruct]: The cloud provider's description of the database artifact
1634
+ # @return [void]
1635
+ def self.terminate_rds_instance(db, noop: false, skipsnapshots: false, region: MU.curRegion, deploy_id: MU.deploy_id, mu_name: nil, cloud_id: nil, credentials: nil, cluster: false, known: [])
1636
+ db ||= MU::Cloud::AWS::Database.find(cloud_id: cloud_id, region: region, credentials: credentials, cluster: cluster).values.first if cloud_id
1637
+ db_obj ||= MU::MommaCat.findStray(
1638
+ "AWS",
1639
+ "database",
1640
+ region: region,
1641
+ deploy_id: deploy_id,
1642
+ cloud_id: cloud_id,
1643
+ mu_name: mu_name,
1644
+ dummy_ok: true
1645
+ ).first
1646
+ if db_obj
1647
+ cloud_id ||= db_obj.cloud_id
1648
+ db ||= db_obj.cloud_desc
1649
+ ["parameter_group_name", "subnet_group_name"].each { |attr|
1650
+ if db_obj.config[attr]
1651
+ known ||= []
1652
+ known << db_obj.config[attr]
1653
+ end
1654
+ }
1655
+ end
1656
+
1657
+ raise MuError, "terminate_rds_instance requires a non-nil database descriptor (#{cloud_id})" if db.nil? or cloud_id.nil?
1658
+
1659
+ MU.retrier([], wait: 60, loop_if: Proc.new { %w{creating modifying backing-up}.include?(cluster ? db.status : db.db_instance_status) }, loop_msg: "Waiting for RDS #{cluster ? "cluster" : "instance"} #{cloud_id} to be in a valid state for deletion") {
1660
+ db = MU::Cloud::AWS::Database.find(cloud_id: cloud_id, region: region, credentials: credentials, cluster: cluster).values.first
1661
+ return if db.nil?
1662
+ }
1663
+
1664
+ MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(name: cloud_id, target: (cluster ? db.endpoint : db.endpoint.address), cloudclass: MU::Cloud::Database, delete: true) if !noop
1665
+
1666
+ if %w{deleting deleted}.include?(cluster ? db.status : db.db_instance_status)
1667
+ MU.log "#{cloud_id} has already been terminated", MU::WARN
1668
+ else
1669
+ params = cluster ? { :db_cluster_identifier => cloud_id } : { :db_instance_identifier => cloud_id }
1670
+
1671
+ if skipsnapshots or (!cluster and (db.db_cluster_identifier or db.read_replica_source_db_instance_identifier))
1672
+ MU.log "Terminating #{cluster ? "cluster" : "database" } #{cloud_id} (not saving final snapshot)"
1673
+ params[:skip_final_snapshot] = true
1674
+ else
1675
+ MU.log "Terminating #{cluster ? "cluster" : "database" } #{cloud_id} (final snapshot: #{cloud_id}-mufinal)"
1676
+ params[:skip_final_snapshot] = false
1677
+ params[:final_db_snapshot_identifier] = "#{cloud_id}-mufinal"
1678
+ end
1679
+ sleep 30
1680
+ if !noop
1681
+ on_retry = Proc.new { |e|
1682
+ if [Aws::RDS::Errors::DBSnapshotAlreadyExists, Aws::RDS::Errors::DBClusterSnapshotAlreadyExistsFault, Aws::RDS::Errors::DBClusterQuotaExceeded].include?(e.class)
1683
+ MU.log e.message, MU::WARN
1684
+ params[:skip_final_snapshot] = true
1685
+ params.delete(:final_db_snapshot_identifier)
1686
+ end
1687
+ }
1688
+ MU.retrier([Aws::RDS::Errors::InvalidDBInstanceState, Aws::RDS::Errors::DBSnapshotAlreadyExists, Aws::RDS::Errors::InvalidDBClusterStateFault], wait: 60, max: 20, on_retry: on_retry) {
1689
+ if !noop
1690
+ cluster ? MU::Cloud::AWS.rds(region: region, credentials: credentials).delete_db_cluster(params) : MU::Cloud::AWS.rds(region: region, credentials: credentials).delete_db_instance(params)
1691
+ end
1692
+ }
1693
+ del_db = nil
1694
+ MU.retrier([], wait: 10, ignoreme: [Aws::RDS::Errors::DBInstanceNotFound], loop_if: Proc.new { del_db and ((!cluster and del_db.db_instance_status != "deleted") or (cluster and del_db.status != "deleted")) }, loop_msg: "Waiting for RDS #{cluster ? "cluster" : "instance"} #{cloud_id} to delete") {
1695
+ del_db = MU::Cloud::AWS::Database.find(cloud_id: cloud_id, region: region, cluster: cluster).values.first
1696
+ }
1697
+ end
1698
+ end
1699
+
1700
+ purge_rds_sgs(cloud_id, region, credentials, noop)
1701
+
1702
+ purge_groomer_artifacts(db_obj, cloud_id, noop)
1703
+
1704
+ MU.log "#{cloud_id} has been terminated" if !noop
1705
+ end
1706
+ private_class_method :terminate_rds_instance
1707
+
1708
+ def self.purge_groomer_artifacts(db_obj, cloud_id, noop)
1709
+ return if !db_obj
1710
+ # Cleanup the database vault
1711
+ groomer =
1712
+ if db_obj and db_obj.respond_to?(:config) and db_obj.config
1713
+ db_obj.config.has_key?("groomer") ? db_obj.config["groomer"] : MU::Config.defaultGroomer
1714
+ else
1715
+ MU::Config.defaultGroomer
1716
+ end
1717
+
1718
+ groomclass = MU::Groomer.loadGroomer(groomer)
1719
+ groomclass.deleteSecret(vault: cloud_id.upcase) if !noop
1720
+ end
1721
+ private_class_method :purge_groomer_artifacts
1722
+
1723
+ def self.purge_rds_sgs(cloud_id, region, credentials, noop)
1724
+ rdssecgroups = []
1725
+ begin
1726
+ secgroup = MU::Cloud::AWS.rds(region: region, credentials: credentials).describe_db_security_groups(db_security_group_name: cloud_id)
1727
+ rdssecgroups << cloud_id if !secgroup.nil?
1728
+ rescue Aws::RDS::Errors::DBSecurityGroupNotFound
1729
+ MU.log "No such RDS security group #{cloud_id} to purge", MU::DEBUG
1730
+ end
1731
+
1732
+ # RDS security groups can depend on EC2 security groups, do these last
1733
+ rdssecgroups.each { |sg|
1734
+ MU.log "Removing RDS Security Group #{sg}"
1735
+ begin
1736
+ MU::Cloud::AWS.rds(region: region, credentials: credentials).delete_db_security_group(db_security_group_name: sg) if !noop
1737
+ rescue Aws::RDS::Errors::DBSecurityGroupNotFound
1738
+ MU.log "RDS Security Group #{sg} disappeared before I could remove it", MU::NOTICE
1739
+ end
1740
+ }
1741
+ end
1742
+ private_class_method :purge_rds_sgs
1743
+
1744
+ end #class
1745
+ end #class
1746
+ end
1747
+ end #module