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.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/bin/mu-adopt +15 -12
- data/bin/mu-azure-tests +57 -0
- data/bin/mu-cleanup +2 -4
- data/bin/mu-configure +37 -1
- data/bin/mu-deploy +3 -3
- data/bin/mu-findstray-tests +25 -0
- data/bin/mu-gen-docs +2 -4
- data/bin/mu-load-config.rb +2 -1
- data/bin/mu-run-tests +37 -12
- data/cloud-mu.gemspec +4 -4
- data/cookbooks/mu-tools/attributes/default.rb +7 -0
- data/cookbooks/mu-tools/libraries/helper.rb +87 -3
- data/cookbooks/mu-tools/recipes/apply_security.rb +39 -23
- data/cookbooks/mu-tools/recipes/aws_api.rb +13 -0
- data/cookbooks/mu-tools/recipes/google_api.rb +4 -0
- data/cookbooks/mu-tools/recipes/rsyslog.rb +8 -1
- data/cookbooks/mu-tools/resources/disk.rb +33 -12
- data/cookbooks/mu-tools/resources/mommacat_request.rb +1 -2
- data/cookbooks/mu-tools/templates/centos-8/sshd_config.erb +215 -0
- data/extras/clean-stock-amis +10 -2
- data/extras/generate-stock-images +7 -3
- data/extras/image-generators/AWS/centos7.yaml +19 -16
- data/extras/image-generators/AWS/{rhel7.yaml → rhel71.yaml} +0 -0
- data/extras/image-generators/AWS/{win2k12.yaml → win2k12r2.yaml} +0 -0
- data/modules/mommacat.ru +2 -2
- data/modules/mu.rb +84 -97
- data/modules/mu/adoption.rb +359 -59
- data/modules/mu/cleanup.rb +67 -44
- data/modules/mu/cloud.rb +108 -1754
- data/modules/mu/cloud/database.rb +49 -0
- data/modules/mu/cloud/dnszone.rb +44 -0
- data/modules/mu/cloud/machine_images.rb +212 -0
- data/modules/mu/cloud/providers.rb +81 -0
- data/modules/mu/cloud/resource_base.rb +929 -0
- data/modules/mu/cloud/server.rb +40 -0
- data/modules/mu/cloud/server_pool.rb +1 -0
- data/modules/mu/cloud/ssh_sessions.rb +228 -0
- data/modules/mu/cloud/winrm_sessions.rb +237 -0
- data/modules/mu/cloud/wrappers.rb +178 -0
- data/modules/mu/config.rb +122 -80
- data/modules/mu/config/alarm.rb +2 -6
- data/modules/mu/config/bucket.rb +32 -3
- data/modules/mu/config/cache_cluster.rb +2 -2
- data/modules/mu/config/cdn.rb +100 -0
- data/modules/mu/config/collection.rb +1 -1
- data/modules/mu/config/container_cluster.rb +2 -2
- data/modules/mu/config/database.rb +84 -105
- data/modules/mu/config/database.yml +1 -2
- data/modules/mu/config/dnszone.rb +5 -4
- data/modules/mu/config/doc_helpers.rb +4 -5
- data/modules/mu/config/endpoint.rb +2 -1
- data/modules/mu/config/firewall_rule.rb +3 -19
- data/modules/mu/config/folder.rb +1 -1
- data/modules/mu/config/function.rb +17 -8
- data/modules/mu/config/group.rb +1 -1
- data/modules/mu/config/habitat.rb +1 -1
- data/modules/mu/config/job.rb +89 -0
- data/modules/mu/config/loadbalancer.rb +57 -11
- data/modules/mu/config/log.rb +1 -1
- data/modules/mu/config/msg_queue.rb +1 -1
- data/modules/mu/config/nosqldb.rb +1 -1
- data/modules/mu/config/notifier.rb +8 -19
- data/modules/mu/config/ref.rb +81 -9
- data/modules/mu/config/role.rb +1 -1
- data/modules/mu/config/schema_helpers.rb +30 -34
- data/modules/mu/config/search_domain.rb +1 -1
- data/modules/mu/config/server.rb +5 -13
- data/modules/mu/config/server_pool.rb +3 -7
- data/modules/mu/config/storage_pool.rb +1 -1
- data/modules/mu/config/tail.rb +10 -0
- data/modules/mu/config/user.rb +1 -1
- data/modules/mu/config/vpc.rb +13 -17
- data/modules/mu/defaults/AWS.yaml +106 -106
- data/modules/mu/defaults/Azure.yaml +1 -0
- data/modules/mu/defaults/Google.yaml +1 -0
- data/modules/mu/deploy.rb +33 -19
- data/modules/mu/groomer.rb +15 -0
- data/modules/mu/groomers/chef.rb +3 -0
- data/modules/mu/logger.rb +120 -144
- data/modules/mu/master.rb +22 -1
- data/modules/mu/mommacat.rb +71 -26
- data/modules/mu/mommacat/daemon.rb +23 -14
- data/modules/mu/mommacat/naming.rb +82 -3
- data/modules/mu/mommacat/search.rb +59 -16
- data/modules/mu/mommacat/storage.rb +119 -48
- data/modules/mu/{clouds → providers}/README.md +1 -1
- data/modules/mu/{clouds → providers}/aws.rb +248 -62
- data/modules/mu/{clouds → providers}/aws/alarm.rb +3 -3
- data/modules/mu/{clouds → providers}/aws/bucket.rb +275 -41
- data/modules/mu/{clouds → providers}/aws/cache_cluster.rb +14 -50
- data/modules/mu/providers/aws/cdn.rb +782 -0
- data/modules/mu/{clouds → providers}/aws/collection.rb +5 -5
- data/modules/mu/{clouds → providers}/aws/container_cluster.rb +65 -63
- data/modules/mu/providers/aws/database.rb +1747 -0
- data/modules/mu/{clouds → providers}/aws/dnszone.rb +26 -12
- data/modules/mu/providers/aws/endpoint.rb +1072 -0
- data/modules/mu/{clouds → providers}/aws/firewall_rule.rb +39 -32
- data/modules/mu/{clouds → providers}/aws/folder.rb +1 -1
- data/modules/mu/{clouds → providers}/aws/function.rb +291 -133
- data/modules/mu/{clouds → providers}/aws/group.rb +18 -20
- data/modules/mu/{clouds → providers}/aws/habitat.rb +3 -3
- data/modules/mu/providers/aws/job.rb +469 -0
- data/modules/mu/{clouds → providers}/aws/loadbalancer.rb +77 -47
- data/modules/mu/{clouds → providers}/aws/log.rb +5 -5
- data/modules/mu/{clouds → providers}/aws/msg_queue.rb +14 -11
- data/modules/mu/{clouds → providers}/aws/nosqldb.rb +96 -5
- data/modules/mu/{clouds → providers}/aws/notifier.rb +135 -63
- data/modules/mu/{clouds → providers}/aws/role.rb +112 -78
- data/modules/mu/{clouds → providers}/aws/search_domain.rb +172 -41
- data/modules/mu/{clouds → providers}/aws/server.rb +120 -145
- data/modules/mu/{clouds → providers}/aws/server_pool.rb +42 -60
- data/modules/mu/{clouds → providers}/aws/storage_pool.rb +21 -38
- data/modules/mu/{clouds → providers}/aws/user.rb +12 -16
- data/modules/mu/{clouds → providers}/aws/userdata/README.md +0 -0
- data/modules/mu/{clouds → providers}/aws/userdata/linux.erb +5 -4
- data/modules/mu/{clouds → providers}/aws/userdata/windows.erb +0 -0
- data/modules/mu/{clouds → providers}/aws/vpc.rb +141 -73
- data/modules/mu/{clouds → providers}/aws/vpc_subnet.rb +0 -0
- data/modules/mu/{clouds → providers}/azure.rb +4 -1
- data/modules/mu/{clouds → providers}/azure/container_cluster.rb +1 -5
- data/modules/mu/{clouds → providers}/azure/firewall_rule.rb +8 -1
- data/modules/mu/{clouds → providers}/azure/habitat.rb +0 -0
- data/modules/mu/{clouds → providers}/azure/loadbalancer.rb +0 -0
- data/modules/mu/{clouds → providers}/azure/role.rb +0 -0
- data/modules/mu/{clouds → providers}/azure/server.rb +32 -24
- data/modules/mu/{clouds → providers}/azure/user.rb +1 -1
- data/modules/mu/{clouds → providers}/azure/userdata/README.md +0 -0
- data/modules/mu/{clouds → providers}/azure/userdata/linux.erb +0 -0
- data/modules/mu/{clouds → providers}/azure/userdata/windows.erb +0 -0
- data/modules/mu/{clouds → providers}/azure/vpc.rb +4 -6
- data/modules/mu/{clouds → providers}/cloudformation.rb +1 -1
- data/modules/mu/{clouds → providers}/cloudformation/alarm.rb +3 -3
- data/modules/mu/{clouds → providers}/cloudformation/cache_cluster.rb +3 -3
- data/modules/mu/{clouds → providers}/cloudformation/collection.rb +3 -3
- data/modules/mu/{clouds → providers}/cloudformation/database.rb +6 -17
- data/modules/mu/{clouds → providers}/cloudformation/dnszone.rb +3 -3
- data/modules/mu/{clouds → providers}/cloudformation/firewall_rule.rb +3 -3
- data/modules/mu/{clouds → providers}/cloudformation/loadbalancer.rb +3 -3
- data/modules/mu/{clouds → providers}/cloudformation/log.rb +3 -3
- data/modules/mu/{clouds → providers}/cloudformation/server.rb +7 -7
- data/modules/mu/{clouds → providers}/cloudformation/server_pool.rb +5 -5
- data/modules/mu/{clouds → providers}/cloudformation/vpc.rb +3 -3
- data/modules/mu/{clouds → providers}/docker.rb +0 -0
- data/modules/mu/{clouds → providers}/google.rb +15 -6
- data/modules/mu/{clouds → providers}/google/bucket.rb +2 -2
- data/modules/mu/{clouds → providers}/google/container_cluster.rb +29 -14
- data/modules/mu/{clouds → providers}/google/database.rb +2 -9
- data/modules/mu/{clouds → providers}/google/firewall_rule.rb +3 -3
- data/modules/mu/{clouds → providers}/google/folder.rb +5 -9
- data/modules/mu/{clouds → providers}/google/function.rb +4 -4
- data/modules/mu/{clouds → providers}/google/group.rb +9 -17
- data/modules/mu/{clouds → providers}/google/habitat.rb +4 -8
- data/modules/mu/{clouds → providers}/google/loadbalancer.rb +2 -2
- data/modules/mu/{clouds → providers}/google/role.rb +46 -35
- data/modules/mu/{clouds → providers}/google/server.rb +26 -11
- data/modules/mu/{clouds → providers}/google/server_pool.rb +11 -11
- data/modules/mu/{clouds → providers}/google/user.rb +32 -22
- data/modules/mu/{clouds → providers}/google/userdata/README.md +0 -0
- data/modules/mu/{clouds → providers}/google/userdata/linux.erb +0 -0
- data/modules/mu/{clouds → providers}/google/userdata/windows.erb +0 -0
- data/modules/mu/{clouds → providers}/google/vpc.rb +38 -3
- data/modules/tests/aws-jobs-functions.yaml +46 -0
- data/modules/tests/centos6.yaml +15 -0
- data/modules/tests/centos7.yaml +15 -0
- data/modules/tests/centos8.yaml +12 -0
- data/modules/tests/ecs.yaml +2 -2
- data/modules/tests/eks.yaml +1 -1
- data/modules/tests/functions/node-function/lambda_function.js +10 -0
- data/modules/tests/functions/python-function/lambda_function.py +12 -0
- data/modules/tests/microservice_app.yaml +288 -0
- data/modules/tests/rds.yaml +108 -0
- data/modules/tests/regrooms/rds.yaml +123 -0
- data/modules/tests/server-with-scrub-muisms.yaml +1 -1
- data/modules/tests/super_complex_bok.yml +2 -2
- data/modules/tests/super_simple_bok.yml +2 -2
- data/spec/mu/clouds/azure_spec.rb +2 -2
- metadata +126 -98
- data/modules/mu/clouds/aws/database.rb +0 -1974
- 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
|
|
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
|
|
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
|
|
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 ==
|
|
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
|
-
@@
|
|
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
|
-
@@
|
|
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 =
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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(/^#{
|
|
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
|
|
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\/(#{
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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[
|
|
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["
|
|
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["
|
|
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[
|
|
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[
|
|
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
|