From 65ee903944804633fac3f937c52321dd652e326d Mon Sep 17 00:00:00 2001 From: Amadeo Casas Date: Tue, 11 Jun 2013 16:07:34 -0700 Subject: [PATCH 1/7] Force Delete support in ASGs --- .../com/netflix/asgard/AutoScalingController.groovy | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy b/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy index f82352cd..c9e00be0 100644 --- a/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy +++ b/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy @@ -37,6 +37,7 @@ import com.netflix.asgard.model.GroupedInstance import com.netflix.asgard.model.InstancePriceType import com.netflix.asgard.model.SubnetTarget import com.netflix.asgard.model.Subnets +import com.netflix.asgard.push.GroupDeleteOperation import com.netflix.grails.contextParam.ContextParam import grails.converters.JSON import grails.converters.XML @@ -57,6 +58,7 @@ class AutoScalingController { def configService def instanceTypeService def mergedInstanceService + def pushService def spotInstanceRequestService def stackService @@ -450,13 +452,15 @@ class AutoScalingController { String name = params.name AutoScalingGroup group = awsAutoScalingService.getAutoScalingGroup(userContext, name) Boolean showGroupNext = false + Boolean showTask = false if (!group) { flash.message = "Auto Scaling Group '${name}' not found." } else { if (group?.instances?.size() <= 0) { try { - awsAutoScalingService.deleteAutoScalingGroup(userContext, name) - flash.message = "AutoScaling Group '${name}' has been deleted." + GroupDeleteOperation operation = pushService.startGroupDelete(userContext, group) + showTask = true + redirect(controller: 'task', action: 'show', params: [id: operation.taskId]) } catch (Exception e) { flash.message = "Could not delete Auto Scaling Group: ${e}" showGroupNext = true @@ -467,7 +471,9 @@ class AutoScalingController { showGroupNext = true } } - showGroupNext ? redirect(action: 'show', params: [id: name]) : redirect(action: 'list') + if (!showTask) { + showGroupNext ? redirect(action: 'show', params: [id: name]) : redirect(action: 'list') + } } def postpone = { From 3704f0deead2905a347f1304df7cd2eabb5f6105 Mon Sep 17 00:00:00 2001 From: Amadeo Casas Date: Tue, 11 Jun 2013 17:51:47 -0700 Subject: [PATCH 2/7] Cleaning up code based on code review feedback --- .../asgard/AutoScalingController.groovy | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy b/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy index c9e00be0..3d84ef66 100644 --- a/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy +++ b/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy @@ -451,28 +451,11 @@ class AutoScalingController { UserContext userContext = UserContext.of(request) String name = params.name AutoScalingGroup group = awsAutoScalingService.getAutoScalingGroup(userContext, name) - Boolean showGroupNext = false - Boolean showTask = false if (!group) { flash.message = "Auto Scaling Group '${name}' not found." } else { - if (group?.instances?.size() <= 0) { - try { - GroupDeleteOperation operation = pushService.startGroupDelete(userContext, group) - showTask = true - redirect(controller: 'task', action: 'show', params: [id: operation.taskId]) - } catch (Exception e) { - flash.message = "Could not delete Auto Scaling Group: ${e}" - showGroupNext = true - } - } else { - flash.message = "You cannot delete an auto scaling group that still has instances. " + - "Set the min and max to 0, wait for the instances to disappear, then try deleting again." - showGroupNext = true - } - } - if (!showTask) { - showGroupNext ? redirect(action: 'show', params: [id: name]) : redirect(action: 'list') + GroupDeleteOperation operation = pushService.startGroupDelete(userContext, group) + redirect(controller: 'task', action: 'show', params: [id: operation.taskId]) } } From 1ebd5531e722a5735961c841569470445d4bfb06 Mon Sep 17 00:00:00 2001 From: Amadeo Casas Date: Tue, 11 Jun 2013 20:23:15 -0700 Subject: [PATCH 3/7] ASGARD-4 : Allow creation of multi-AZ RDS DB instances --- .../com/netflix/asgard/RdsInstanceController.groovy | 9 +++++++-- grails-app/i18n/messages.properties | 1 + grails-app/views/rdsInstance/list.gsp | 5 ++++- web-app/js/custom.js | 13 +++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy b/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy index 5d40974c..234a46f5 100644 --- a/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy +++ b/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy @@ -79,7 +79,6 @@ class RdsInstanceController { final DBInstance dbInstance = new DBInstance() .withAllocatedStorage(params.allocatedStorage as Integer) - .withAvailabilityZone(params.availabilityZone) .withBackupRetentionPeriod(params.backupRetentionPeriod as Integer) .withDBInstanceClass(params.dBInstanceClass,) .withDBInstanceIdentifier(params.dBInstanceIdentifier) @@ -92,6 +91,10 @@ class RdsInstanceController { .withPreferredMaintenanceWindow(params.preferredMaintenanceWindow) .withLicenseModel(params.licenseModel) + if (!multiAZ) { + dbInstance.availabilityZone = params.availabilityZone + } + awsRdsService.createDBInstance(userContext, dbInstance, params.masterUserPassword, params.port as Integer) flash.message = "DB Instance '${params.dBInstanceIdentifier}' has been created." redirect(action: 'show', params: [id: params.dBInstanceIdentifier]) @@ -184,6 +187,7 @@ class DbCreateCommand { String masterUsername // Must be an alphanumeric string containing from 1 to 16 characters. String masterUserPassword // Must contain 4 to 16 alphanumeric characters. Integer port + String multiAZ String preferredBackupWindow // Constraints: Must be in the format hh24:mi-hh24:mi. // Times should be 24-hour Universal Time Coordinated (UTC). // Must not conflict with the --preferred-maintenance-window. @@ -196,7 +200,7 @@ class DbCreateCommand { static constraints = { allocatedStorage(nullable: false, range: 5..1024) - availabilityZone(nullable: false, blank: false) // Need more -- custom validator? + availabilityZone(nullable: false, validator: { value, object -> "on".equals(object.multiAZ) != value.length() > 0 ?: 'dbCreateCommand.multiaz.availabilityzones.error' }) backupRetentionPeriod(blank: false, nullable: false, range: 0..8) dBInstanceClass(nullable: false, blank: false) dBInstanceIdentifier(nullable: false, blank: false, size: 1..63, matches: '[a-zA-Z]{1}[a-zA-Z0-9-]*[^-]$') // This match does not check the double-hyphen @@ -205,6 +209,7 @@ class DbCreateCommand { masterUsername(nullable: false, blank: false, size: 1..16, matches: '[a-zA-Z0-9]{1,16}') masterUserPassword(nullable: false, blank: false, size: 4..16, matches: '[a-zA-Z0-9]{4,16}') port(nullable: false) + multiAZ(nullable: false) preferredBackupWindow(blank: true, matches: '(20|21|22|23|[01]\\d|\\d)(([:][0-5]\\d){1,2})-(20|21|22|23|[01]\\d|\\d)(([:][0-5]\\d){1,2})') // Did not check for 2 hour min, clash with maintenance, or that backup period specified diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index ece1a386..34db18de 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -92,6 +92,7 @@ dbCreateCommand.masterUserPassword.matches.error=Master user password must be 4 dbCreateCommand.port.blank.error=Port must not be blank dbCreateCommand.port.nullable.error=Port must not be blank dbCreateCommand.preferredBackupWindow.matches.error=If specified, preferred backup window must be in the format hh24:mi-hh24:mi, 24-hour Universal Time Coordinated (UTC), must not conflict with the preferred maintenance window, must be at least 2 hours, and cannot be set if the backup retention period has not been specified. +dbCreateCommand.multiaz.availabilityzones.error=If multiple AZ specified, no availability zone can be selected dbUpdateCommand.allocatedStorage.nullable.error=Allocated storage must be a number between 5 and 1024 dbUpdateCommand.allocatedStorage.blank.error=Allocated storage must be a number between 5 and 1024 diff --git a/grails-app/views/rdsInstance/list.gsp b/grails-app/views/rdsInstance/list.gsp index e740e6bb..807e7f7a 100644 --- a/grails-app/views/rdsInstance/list.gsp +++ b/grails-app/views/rdsInstance/list.gsp @@ -57,7 +57,10 @@ ${dbi.dBInstanceStatus} ${dbi.dBName} ${dbi.dBInstanceClass} - ${dbi.availabilityZone} + + All + ${dbi.availabilityZone} + diff --git a/web-app/js/custom.js b/web-app/js/custom.js index d338d199..5b0ed2be 100644 --- a/web-app/js/custom.js +++ b/web-app/js/custom.js @@ -850,6 +850,19 @@ jQuery(document).ready(function() { }; setUpFastPropertyCreatePage(); + // RDS create page + var setUpRDSCreatePage = function() { + jQuery('#multiAZ').change(function() { + if (this.checked) { + jQuery('#availabilityZone').select2('disable', false); + jQuery('#availabilityZone').select2('val', "-Zone-"); + } else { + jQuery('#availabilityZone').select2('enable'); + } + }); + }; + setUpRDSCreatePage(); + // Task page var setUpTaskPage = function() { var autoScroller; From 6ed9f72b8d006a54f6d23ac54ae52f20a6da90c5 Mon Sep 17 00:00:00 2001 From: Amadeo Casas Date: Wed, 12 Jun 2013 16:02:37 -0700 Subject: [PATCH 4/7] Test added to verify custom validator over availabilityZone --- .../asgard/RdsInstanceController.groovy | 1 - grails-app/i18n/messages.properties | 2 +- .../asgard/RdsInstanceControllerSpec.groovy | 36 ++++++++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy b/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy index 234a46f5..16c1b57b 100644 --- a/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy +++ b/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy @@ -209,7 +209,6 @@ class DbCreateCommand { masterUsername(nullable: false, blank: false, size: 1..16, matches: '[a-zA-Z0-9]{1,16}') masterUserPassword(nullable: false, blank: false, size: 4..16, matches: '[a-zA-Z0-9]{4,16}') port(nullable: false) - multiAZ(nullable: false) preferredBackupWindow(blank: true, matches: '(20|21|22|23|[01]\\d|\\d)(([:][0-5]\\d){1,2})-(20|21|22|23|[01]\\d|\\d)(([:][0-5]\\d){1,2})') // Did not check for 2 hour min, clash with maintenance, or that backup period specified diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 34db18de..b3e53a32 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -92,7 +92,7 @@ dbCreateCommand.masterUserPassword.matches.error=Master user password must be 4 dbCreateCommand.port.blank.error=Port must not be blank dbCreateCommand.port.nullable.error=Port must not be blank dbCreateCommand.preferredBackupWindow.matches.error=If specified, preferred backup window must be in the format hh24:mi-hh24:mi, 24-hour Universal Time Coordinated (UTC), must not conflict with the preferred maintenance window, must be at least 2 hours, and cannot be set if the backup retention period has not been specified. -dbCreateCommand.multiaz.availabilityzones.error=If multiple AZ specified, no availability zone can be selected +dbCreateCommand.multiaz.availabilityzones.error=Either specify multiple AZ, or select an availability zone dbUpdateCommand.allocatedStorage.nullable.error=Allocated storage must be a number between 5 and 1024 dbUpdateCommand.allocatedStorage.blank.error=Allocated storage must be a number between 5 and 1024 diff --git a/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy b/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy index dee07076..9af56f89 100644 --- a/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy +++ b/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy @@ -35,6 +35,22 @@ class RdsInstanceControllerSpec extends Specification { preferredMaintenanceWindow: "", ] + final showMultiAZParams = [ + allocatedStorage: 5, + availabilityZone: "us-east-1a", + backupRetentionPeriod: 0, + dBInstanceClass: "dbClass", + dBInstanceIdentifier: "testDB", + dBName: "DBname", + masterUsername: "testname", + masterUserPassword: "testpassword", + multiAZ: "on", + port: 3306, + preferredBackupWindow: "", + preferredMaintenanceWindow: "", + selectedDBSecurityGroups: "testsecgroup", + ] + void setup() { TestUtils.setUpMockRequest() MockUtils.prepareForConstraintsTests(DbCreateCommand) @@ -55,6 +71,24 @@ class RdsInstanceControllerSpec extends Specification { response.redirectUrl == '/rdsInstance/show/testDB' } + def 'save should return exception when multiaz on and availability zone specified'() { + def cmd = new DbCreateCommand(showMultiAZParams) + + when: + cmd.validate() + + then: + cmd.hasErrors() + cmd.errors.errorCount == 1 + cmd.errors.fieldError.field == "availabilityZone" + + when: + controller.save(cmd) + + then: + '/rdsInstance/create' == response.redirectUrl + } + def 'save should return exception message when necessary'() { controller.params.putAll(showParams) final cmd = new DbCreateCommand(showParams) @@ -79,7 +113,7 @@ class RdsInstanceControllerSpec extends Specification { then: response.redirectUrl == '/rdsInstance/create?' + showParams.collect { k,v -> "$k=$v" }.join('&') flash.chainModel.cmd.errors.allocatedStorage == 'range' - } + } def 'create should return possible RDS engines and license model selections'() { controller.params.dBInstanceIdentifier = '0' From 55aa01807c0548d44d8338786736a514761bbb0f Mon Sep 17 00:00:00 2001 From: Amadeo Casas Date: Mon, 17 Jun 2013 12:55:40 -0700 Subject: [PATCH 5/7] ASGARD-1011 - Support RDS in VPC --- .../asgard/RdsInstanceController.groovy | 72 +++++++++++++++++-- grails-app/i18n/messages.properties | 4 ++ .../com/netflix/asgard/AwsRdsService.groovy | 25 ++++++- grails-app/views/rdsInstance/create.gsp | 4 +- grails-app/views/rdsInstance/show.gsp | 12 ++++ .../asgard/RdsInstanceControllerSpec.groovy | 34 +++++++++ 6 files changed, 143 insertions(+), 8 deletions(-) diff --git a/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy b/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy index 16c1b57b..d159bc8d 100644 --- a/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy +++ b/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy @@ -15,9 +15,13 @@ */ package com.netflix.asgard +import com.amazonaws.services.ec2.model.SecurityGroup import com.amazonaws.services.rds.model.DBInstance import com.amazonaws.services.rds.model.DBSnapshot +import com.amazonaws.services.rds.model.DBSubnetGroup import com.netflix.asgard.AwsRdsService.Engine +import com.netflix.asgard.model.SubnetTarget +import com.netflix.asgard.model.Subnets import com.netflix.grails.contextParam.ContextParam import grails.converters.JSON import grails.converters.XML @@ -56,11 +60,24 @@ class RdsInstanceController { def create = { UserContext userContext = UserContext.of(request) + List effectiveGroups = awsEc2Service.getEffectiveSecurityGroups(userContext).sort { + it.groupName?.toLowerCase() + } + Map> securityGroupsGroupedByVpcId = effectiveGroups.groupBy { it.vpcId } + Subnets subnets = awsEc2Service.getSubnets(userContext) + Map purposeToVpcId = subnets.mapPurposeToVpcId() [ 'allDBSecurityGroups' : awsRdsService.getDBSecurityGroups(userContext), 'allDBInstanceClasses' : AwsRdsService.getDBInstanceClasses(), 'allDbInstanceEngines' : Engine.values()*.awsValue, 'allLicenseModels' : AwsRdsService.getLicenseModels(), + 'purposeToVpcId': purposeToVpcId, + 'securityGroupsGroupedByVpcId': securityGroupsGroupedByVpcId, + 'selectedSecurityGroups': Requests.ensureList(params.selectedSecurityGroups), + 'subnetPurpose': params.subnetPurpose ?: null, + 'subnetPurposes': subnets.getPurposesForZones(awsEc2Service.getAvailabilityZones(userContext)*.zoneName, SubnetTarget.ELB).sort(), + 'subnets': subnets, + 'vpcId': purposeToVpcId[params.subnetPurpose], 'zoneList':awsEc2Service.getAvailabilityZones(userContext) ] } @@ -73,16 +90,26 @@ class RdsInstanceController { } else { try { boolean multiAZ = "on".equals(params.multiAZ) + def subnets = awsEc2Service.getSubnets(userContext) + def selectedSecurityGroups = (params.selectedSecurityGroups instanceof String) ? [params.selectedSecurityGroups] : params.selectedSecurityGroups as List def selectedDBSecurityGroups = (params.selectedDBSecurityGroups instanceof String) ? [params.selectedDBSecurityGroups] : params.selectedDBSecurityGroups as List - if (!selectedDBSecurityGroups) selectedDBSecurityGroups = ["default"] - //awsRdsService.createDBSecurityGroup(params.name, params.description) + if (!selectedDBSecurityGroups && !params.subnetPurpose) { + selectedDBSecurityGroups = ["default"] + } + + final DBSubnetGroup dbSubnetGroup = new DBSubnetGroup() + .withDBSubnetGroupName(params.dBName) + .withDBSubnetGroupDescription(params.dBName) + .withSubnets(subnets.allSubnets) + .withVpcId(subnets.mapPurposeToVpcId()[params.subnetPurpose]) final DBInstance dbInstance = new DBInstance() .withAllocatedStorage(params.allocatedStorage as Integer) .withBackupRetentionPeriod(params.backupRetentionPeriod as Integer) - .withDBInstanceClass(params.dBInstanceClass,) + .withDBInstanceClass(params.dBInstanceClass) .withDBInstanceIdentifier(params.dBInstanceIdentifier) .withDBName(params.dBName) + .withDBSubnetGroup(dbSubnetGroup) .withEngine(params.engine) .withDBSecurityGroups(selectedDBSecurityGroups) .withMasterUsername(params.masterUsername) @@ -90,12 +117,13 @@ class RdsInstanceController { .withPreferredBackupWindow(params.preferredBackupWindow) .withPreferredMaintenanceWindow(params.preferredMaintenanceWindow) .withLicenseModel(params.licenseModel) + .withVpcSecurityGroups(selectedSecurityGroups) if (!multiAZ) { dbInstance.availabilityZone = params.availabilityZone } - awsRdsService.createDBInstance(userContext, dbInstance, params.masterUserPassword, params.port as Integer) + awsRdsService.createDBInstance(userContext, dbInstance, params.masterUserPassword, params.port as Integer, params.subnetPurpose ?: null) flash.message = "DB Instance '${params.dBInstanceIdentifier}' has been created." redirect(action: 'show', params: [id: params.dBInstanceIdentifier]) } catch (Exception e) { @@ -150,7 +178,15 @@ class RdsInstanceController { if (!dbInstance) { Requests.renderNotFound('DB Instance', dbInstanceId, this) } else { - def details = ['dbInstance':dbInstance, 'snapshots' : snapshots] + List subnetIds = dbInstance.DBSubnetGroup?.subnets?.collect { it.subnetIdentifier } ?: [] + + def details = [ + 'dbInstance': dbInstance, + 'snapshots': snapshots, + 'vpcId': awsEc2Service.getSubnets(userContext).coerceLoneOrNoneFromIds(subnetIds)?.vpcId ?: '', + 'subnets': subnetIds, + 'vpcSecurityGroupsIds': dbInstance.vpcSecurityGroups.collect { it.vpcSecurityGroupId } + ] withFormat { html { return details } xml { new XML(details).render(response) } @@ -183,11 +219,13 @@ class DbCreateCommand { String dBInstanceClass // Valid values: db.m1.small | db.m1.large | db.m1.xlarge | db.m2.2xlarge | db.m2.4xlarge String dBInstanceIdentifier // Constraints: Must contain from 1 to 63 alphanumeric characters or hyphens. First character must be a letter. Cannot end with a hyphen or contain two consecutive hyphens. String dBName // Cannot be empty. Must contain 1 to 64 alphanumeric characters. Cannot be a word reserved by the specified database engine. + Collection selectedSecurityGroups // At least one Collection selectedDBSecurityGroups // At least one String masterUsername // Must be an alphanumeric string containing from 1 to 16 characters. String masterUserPassword // Must contain 4 to 16 alphanumeric characters. Integer port String multiAZ + String subnetPurpose String preferredBackupWindow // Constraints: Must be in the format hh24:mi-hh24:mi. // Times should be 24-hour Universal Time Coordinated (UTC). // Must not conflict with the --preferred-maintenance-window. @@ -205,7 +243,29 @@ class DbCreateCommand { dBInstanceClass(nullable: false, blank: false) dBInstanceIdentifier(nullable: false, blank: false, size: 1..63, matches: '[a-zA-Z]{1}[a-zA-Z0-9-]*[^-]$') // This match does not check the double-hyphen dBName(nullable: false, blank: false, size: 1..64, matches: '[a-zA-Z0-9]{1,64}') - selectedDBSecurityGroups(nullable: false, minSize: 1) + selectedSecurityGroups(validator: { + value, object -> + if (object.subnetPurpose) { + if (!value && !object.selectedDBSecurityGroups) { + 'dbCreateCommand.selectedSecurityGroups.minSize.error' + } + } else { + if (value) { + 'dbCreateCommand.selectedSecurityGroups.vpc.error' + } + } + }) + selectedDBSecurityGroups(validator: { + value, object -> + if (object.subnetPurpose) { + if (!value && !object.selectedSecurityGroups) { + 'dbCreateCommand.selectedDBSecurityGroups.minSize.error' + } + if (value && object) { + 'dbCreateCommand.selectedDBSecurityGroups.vpc.error' + } + } + }) masterUsername(nullable: false, blank: false, size: 1..16, matches: '[a-zA-Z0-9]{1,16}') masterUserPassword(nullable: false, blank: false, size: 4..16, matches: '[a-zA-Z0-9]{4,16}') port(nullable: false) diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index b3e53a32..4e4aea05 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -81,6 +81,7 @@ dbCreateCommand.dBName.matches.error=DB Name must be 1 to 64 alphanumeric charac dbCreateCommand.selectedDBSecurityGroups.nullable.error=At least one DB Security Group must be selected dbCreateCommand.selectedDBSecurityGroups.blank.error=At least one DB Security Group must be selected dbCreateCommand.selectedDBSecurityGroups.minSize.error=At least one DB Security Group must be selected +dbCreateCommand.selectedDBSecurityGroups.vpc.error=DB Security Group cannot be selected for RDS in VPC dbCreateCommand.masterUsername.nullable.error=Master username must be 1 to 16 alphanumeric characters dbCreateCommand.masterUsername.blank.error=Master username must be 1 to 16 alphanumeric characters dbCreateCommand.masterUsername.size.error=Master username must be 1 to 16 alphanumeric characters @@ -93,6 +94,8 @@ dbCreateCommand.port.blank.error=Port must not be blank dbCreateCommand.port.nullable.error=Port must not be blank dbCreateCommand.preferredBackupWindow.matches.error=If specified, preferred backup window must be in the format hh24:mi-hh24:mi, 24-hour Universal Time Coordinated (UTC), must not conflict with the preferred maintenance window, must be at least 2 hours, and cannot be set if the backup retention period has not been specified. dbCreateCommand.multiaz.availabilityzones.error=Either specify multiple AZ, or select an availability zone +dbCreateCommand.selectedSecurityGroups.vpc.error=Select only DB Security Groups for non-VPC RDS +dbCreateCommand.selectedSecurityGroups.minSize.error=At least one Security Group must be selected dbUpdateCommand.allocatedStorage.nullable.error=Allocated storage must be a number between 5 and 1024 dbUpdateCommand.allocatedStorage.blank.error=Allocated storage must be a number between 5 and 1024 @@ -103,6 +106,7 @@ dbUpdateCommand.dBInstanceClass.nullable.error=DB Instance class must not be bla dbUpdateCommand.selectedDBSecurityGroups.nullable.error=At least one DB Security Group must be selected dbUpdateCommand.selectedDBSecurityGroups.blank.error=At least one DB Security Group must be selected dbUpdateCommand.selectedDBSecurityGroups.minSize.error=At least one DB Security Group must be selected +dbUpdateCommand.selectedSecurityGroups.minSize.error=At least one Security Group must be selected dbUpdateCommand.masterUserPassword.size.error=If specified, master user password must be 4 to 16 alphanumeric characters dbUpdateCommand.masterUserPassword.matches.error=If specified, master user password must be 4 to 16 alphanumeric characters dbUpdateCommand.preferredBackupWindow.matches.error=If specified, preferred backup window must be in the format hh24:mi-hh24:mi, 24-hour Universal Time Coordinated (UTC), must not conflict with the preferred maintenance window, must be at least 2 hours, and cannot be set if the backup retention period has not been specified. diff --git a/grails-app/services/com/netflix/asgard/AwsRdsService.groovy b/grails-app/services/com/netflix/asgard/AwsRdsService.groovy index b7063e65..8f6b5577 100644 --- a/grails-app/services/com/netflix/asgard/AwsRdsService.groovy +++ b/grails-app/services/com/netflix/asgard/AwsRdsService.groovy @@ -21,6 +21,7 @@ import com.amazonaws.services.rds.model.AuthorizeDBSecurityGroupIngressRequest import com.amazonaws.services.rds.model.CreateDBInstanceRequest import com.amazonaws.services.rds.model.CreateDBSecurityGroupRequest import com.amazonaws.services.rds.model.CreateDBSnapshotRequest +import com.amazonaws.services.rds.model.CreateDBSubnetGroupRequest import com.amazonaws.services.rds.model.DBInstance import com.amazonaws.services.rds.model.DBSecurityGroup import com.amazonaws.services.rds.model.DBSnapshot @@ -31,6 +32,7 @@ import com.amazonaws.services.rds.model.DescribeDBInstancesRequest import com.amazonaws.services.rds.model.DescribeDBSecurityGroupsRequest import com.amazonaws.services.rds.model.DescribeDBSnapshotsRequest import com.amazonaws.services.rds.model.DescribeDBSnapshotsResult +import com.amazonaws.services.rds.model.DescribeDBSubnetGroupsRequest import com.amazonaws.services.rds.model.ModifyDBInstanceRequest import com.amazonaws.services.rds.model.RestoreDBInstanceFromDBSnapshotRequest import com.amazonaws.services.rds.model.RevokeDBSecurityGroupIngressRequest @@ -115,12 +117,33 @@ class AwsRdsService implements CacheInitializer, InitializingBean { }, Link.to(EntityType.rdsInstance, dbInstanceId)) } - DBInstance createDBInstance(UserContext userContext, DBInstance templateDbInstance, String masterUserPassword, Integer port) { + DBInstance createDBInstance(UserContext userContext, DBInstance templateDbInstance, String masterUserPassword, Integer port, String subnetPurpose) { final BeanState templateDbInstanceState = BeanState.ofSourceBean(templateDbInstance) final CreateDBInstanceRequest request = templateDbInstanceState.injectState(new CreateDBInstanceRequest()) request.masterUserPassword = masterUserPassword if (port) {request.setPort(port)} taskService.runTask(userContext, "Creating DB instance '${templateDbInstance.DBInstanceIdentifier}'", { task -> + if (subnetPurpose) { + def dbSubnetGroups + try { + DescribeDBSubnetGroupsRequest subnetExistsRequest = new DescribeDBSubnetGroupsRequest() + .withDBSubnetGroupName(templateDbInstance.DBSubnetGroup.DBSubnetGroupName) + dbSubnetGroups = awsClient.by(userContext.region).describeDBSubnetGroups(subnetExistsRequest)?.DBSubnetGroups + } catch (AmazonServiceException ignored) { + dbSubnetGroups = null + } + + if (!dbSubnetGroups) { + final CreateDBSubnetGroupRequest subnetRequest = new CreateDBSubnetGroupRequest() + .withDBSubnetGroupName(templateDbInstance.DBSubnetGroup.DBSubnetGroupName) + .withDBSubnetGroupDescription(templateDbInstance.DBSubnetGroup.DBSubnetGroupDescription) + .withSubnetIds(templateDbInstance.DBSubnetGroup.subnets.collect { it.subnetId }) + awsClient.by(userContext.region).createDBSubnetGroup(subnetRequest) + } + // If this is a VPC RDS then we must find the proper DBSubnetGroupName. + request.DBSubnetGroupName = templateDbInstance.DBSubnetGroup.DBSubnetGroupName + request.vpcSecurityGroupIds = templateDbInstance.vpcSecurityGroups + } final DBInstance createdInstance = awsClient.by(userContext.region).createDBInstance(request) caches.allDBInstances.by(userContext.region).put(createdInstance.getDBInstanceIdentifier(), createdInstance) }, Link.to(EntityType.rdsInstance, templateDbInstance.DBInstanceIdentifier)) diff --git a/grails-app/views/rdsInstance/create.gsp b/grails-app/views/rdsInstance/create.gsp index 3fda1f6c..44d71d23 100644 --- a/grails-app/views/rdsInstance/create.gsp +++ b/grails-app/views/rdsInstance/create.gsp @@ -93,7 +93,9 @@ - + + + DB Security Groups: diff --git a/grails-app/views/rdsInstance/show.gsp b/grails-app/views/rdsInstance/show.gsp index e5b64035..492caf84 100644 --- a/grails-app/views/rdsInstance/show.gsp +++ b/grails-app/views/rdsInstance/show.gsp @@ -97,6 +97,14 @@ + + + + + + + + @@ -133,6 +141,10 @@
DB Engine Version: ${dbInstance.engineVersion}
VPC:${vpcId}
Subnets:${subnets}
DB Create Date:
+ + VPC Security Groups: + ${vpcSecurityGroupsIds} + DB Snapshots: diff --git a/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy b/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy index 9af56f89..7ab01e86 100644 --- a/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy +++ b/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy @@ -51,6 +51,22 @@ class RdsInstanceControllerSpec extends Specification { selectedDBSecurityGroups: "testsecgroup", ] + final showVPCParams = [ + allocatedStorage: 5, + availabilityZone: "us-east-1a", + backupRetentionPeriod: 0, + dBInstanceClass: "dbClass", + dBInstanceIdentifier: "testDB", + dBName: "DBname", + masterUsername: "testname", + masterUserPassword: "testpassword", + port: 3306, + preferredBackupWindow: "", + preferredMaintenanceWindow: "", + selectedDBSecurityGroups: "testsecgroup", + subnetPurpose: "internal", + ] + void setup() { TestUtils.setUpMockRequest() MockUtils.prepareForConstraintsTests(DbCreateCommand) @@ -89,6 +105,24 @@ class RdsInstanceControllerSpec extends Specification { '/rdsInstance/create' == response.redirectUrl } + def 'save should return exception when VPC and DB security groups specified'() { + def cmd = new DbCreateCommand(showVPCParams) + + when: + cmd.validate() + + then: + cmd.hasErrors() + cmd.errors.errorCount == 1 + cmd.errors.fieldError.field == "selectedDBSecurityGroups" + + when: + controller.save(cmd) + + then: + '/rdsInstance/create' == response.redirectUrl + } + def 'save should return exception message when necessary'() { controller.params.putAll(showParams) final cmd = new DbCreateCommand(showParams) From 359d4c400a150097c8a5e811c9d29389f70082e3 Mon Sep 17 00:00:00 2001 From: Amadeo Casas Date: Mon, 17 Jun 2013 16:19:25 -0700 Subject: [PATCH 6/7] Code improvements and new DbCreateCommandSpec suite for checking validation on creation of RDS instances --- .../asgard/RdsInstanceController.groovy | 7 +- .../com/netflix/asgard/AwsRdsService.groovy | 7 +- .../netflix/asgard/DbCreateCommandSpec.groovy | 71 +++++++++++++++++++ .../asgard/RdsInstanceControllerSpec.groovy | 16 ----- 4 files changed, 78 insertions(+), 23 deletions(-) create mode 100644 test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy diff --git a/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy b/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy index d159bc8d..96da46a7 100644 --- a/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy +++ b/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy @@ -76,7 +76,6 @@ class RdsInstanceController { 'selectedSecurityGroups': Requests.ensureList(params.selectedSecurityGroups), 'subnetPurpose': params.subnetPurpose ?: null, 'subnetPurposes': subnets.getPurposesForZones(awsEc2Service.getAvailabilityZones(userContext)*.zoneName, SubnetTarget.ELB).sort(), - 'subnets': subnets, 'vpcId': purposeToVpcId[params.subnetPurpose], 'zoneList':awsEc2Service.getAvailabilityZones(userContext) ] @@ -90,9 +89,9 @@ class RdsInstanceController { } else { try { boolean multiAZ = "on".equals(params.multiAZ) - def subnets = awsEc2Service.getSubnets(userContext) - def selectedSecurityGroups = (params.selectedSecurityGroups instanceof String) ? [params.selectedSecurityGroups] : params.selectedSecurityGroups as List - def selectedDBSecurityGroups = (params.selectedDBSecurityGroups instanceof String) ? [params.selectedDBSecurityGroups] : params.selectedDBSecurityGroups as List + Subnets subnets = awsEc2Service.getSubnets(userContext) + List selectedSecurityGroups = Requests.ensureList(params.selectedSecurityGroups) + List selectedDBSecurityGroups = Requests.ensureList(params.selectedDBSecurityGroups) if (!selectedDBSecurityGroups && !params.subnetPurpose) { selectedDBSecurityGroups = ["default"] } diff --git a/grails-app/services/com/netflix/asgard/AwsRdsService.groovy b/grails-app/services/com/netflix/asgard/AwsRdsService.groovy index 8f6b5577..b97bcddb 100644 --- a/grails-app/services/com/netflix/asgard/AwsRdsService.groovy +++ b/grails-app/services/com/netflix/asgard/AwsRdsService.groovy @@ -33,6 +33,7 @@ import com.amazonaws.services.rds.model.DescribeDBSecurityGroupsRequest import com.amazonaws.services.rds.model.DescribeDBSnapshotsRequest import com.amazonaws.services.rds.model.DescribeDBSnapshotsResult import com.amazonaws.services.rds.model.DescribeDBSubnetGroupsRequest +import com.amazonaws.services.rds.model.DescribeDBSubnetGroupsResult import com.amazonaws.services.rds.model.ModifyDBInstanceRequest import com.amazonaws.services.rds.model.RestoreDBInstanceFromDBSnapshotRequest import com.amazonaws.services.rds.model.RevokeDBSecurityGroupIngressRequest @@ -124,7 +125,7 @@ class AwsRdsService implements CacheInitializer, InitializingBean { if (port) {request.setPort(port)} taskService.runTask(userContext, "Creating DB instance '${templateDbInstance.DBInstanceIdentifier}'", { task -> if (subnetPurpose) { - def dbSubnetGroups + DescribeDBSubnetGroupsResult dbSubnetGroups try { DescribeDBSubnetGroupsRequest subnetExistsRequest = new DescribeDBSubnetGroupsRequest() .withDBSubnetGroupName(templateDbInstance.DBSubnetGroup.DBSubnetGroupName) @@ -134,7 +135,7 @@ class AwsRdsService implements CacheInitializer, InitializingBean { } if (!dbSubnetGroups) { - final CreateDBSubnetGroupRequest subnetRequest = new CreateDBSubnetGroupRequest() + CreateDBSubnetGroupRequest subnetRequest = new CreateDBSubnetGroupRequest() .withDBSubnetGroupName(templateDbInstance.DBSubnetGroup.DBSubnetGroupName) .withDBSubnetGroupDescription(templateDbInstance.DBSubnetGroup.DBSubnetGroupDescription) .withSubnetIds(templateDbInstance.DBSubnetGroup.subnets.collect { it.subnetId }) @@ -144,7 +145,7 @@ class AwsRdsService implements CacheInitializer, InitializingBean { request.DBSubnetGroupName = templateDbInstance.DBSubnetGroup.DBSubnetGroupName request.vpcSecurityGroupIds = templateDbInstance.vpcSecurityGroups } - final DBInstance createdInstance = awsClient.by(userContext.region).createDBInstance(request) + DBInstance createdInstance = awsClient.by(userContext.region).createDBInstance(request) caches.allDBInstances.by(userContext.region).put(createdInstance.getDBInstanceIdentifier(), createdInstance) }, Link.to(EntityType.rdsInstance, templateDbInstance.DBInstanceIdentifier)) getDBInstance(userContext, templateDbInstance.DBInstanceIdentifier) diff --git a/test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy b/test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy new file mode 100644 index 00000000..e947be86 --- /dev/null +++ b/test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy @@ -0,0 +1,71 @@ +/* + * Copyright 2012 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.asgard + +import grails.test.mixin.TestMixin +import grails.test.mixin.web.ControllerUnitTestMixin +import spock.lang.Specification +import spock.lang.Unroll + +@TestMixin(ControllerUnitTestMixin) +class DbCreateCommandSpec extends Specification { + + DbCreateCommand cmd + + void setup() { + mockForConstraintsTests(DbCreateCommand) + cmd = new DbCreateCommand() + } + + @Unroll("""should validate input to create RDS databases""") + def 'RDS database constraints'() { + cmd.allocatedStorage = 5 + cmd.backupRetentionPeriod = 0 + cmd.dBInstanceClass = "dbClass" + cmd.dBInstanceIdentifier = "testDB" + cmd.dBName = "DBname" + cmd.masterUsername = "testname" + cmd.masterUserPassword = "testpassword" + cmd.port = 3306 + cmd.preferredBackupWindow = "" + cmd.preferredMaintenanceWindow = "" + cmd.availabilityZone = availabilityZone + cmd.multiAZ = multiAZ + cmd.selectedSecurityGroups = selectedSecurityGroups + cmd.selectedDBSecurityGroups = selectedDBSecurityGroups + cmd.subnetPurpose = subnetPurpose + + when: + cmd.validate() + + then: + cmd.errors.availabilityZone == errorAvailabilityZone + cmd.errors.selectedSecurityGroups == errorSelectedSecurityGroups + cmd.errors.selectedDBSecurityGroups == errorSelectedDBSecurityGroups + + where: + availabilityZone | multiAZ | subnetPurpose | selectedSecurityGroups | selectedDBSecurityGroups | errorAvailabilityZone | errorSelectedSecurityGroups | errorSelectedDBSecurityGroups + "us-east-1a" | "" | "" | null | null | null | null | null + "" | "on" | "" | null | null | null | null | null + "us-east-1a" | "on" | "" | null | null | 'dbCreateCommand.multiaz.availabilityzones.error' | null | null + "" | "" | "" | null | null | 'dbCreateCommand.multiaz.availabilityzones.error' | null | null + "us-east-1a" | "" | "internal" | null | null | null | 'dbCreateCommand.selectedSecurityGroups.minSize.error' | null + "us-east-1a" | "" | "" | ["secgroup"] | null | null | 'dbCreateCommand.selectedSecurityGroups.vpc.error' | null + "us-east-1a" | "on" | "" | ["secgroup"] | null | 'dbCreateCommand.multiaz.availabilityzones.error' | 'dbCreateCommand.selectedSecurityGroups.vpc.error' | null + "us-east-1a" | "" | "internal" | ["secgroup"] | ["dbsecgroup"] | null | null | 'dbCreateCommand.selectedDBSecurityGroups.vpc.error' + "us-east-1a" | "on" | "internal" | ["secgroup"] | ["dbsecgroup"] | 'dbCreateCommand.multiaz.availabilityzones.error' | null | 'dbCreateCommand.selectedDBSecurityGroups.vpc.error' + } +} diff --git a/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy b/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy index 7ab01e86..228df034 100644 --- a/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy +++ b/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy @@ -90,14 +90,6 @@ class RdsInstanceControllerSpec extends Specification { def 'save should return exception when multiaz on and availability zone specified'() { def cmd = new DbCreateCommand(showMultiAZParams) - when: - cmd.validate() - - then: - cmd.hasErrors() - cmd.errors.errorCount == 1 - cmd.errors.fieldError.field == "availabilityZone" - when: controller.save(cmd) @@ -108,14 +100,6 @@ class RdsInstanceControllerSpec extends Specification { def 'save should return exception when VPC and DB security groups specified'() { def cmd = new DbCreateCommand(showVPCParams) - when: - cmd.validate() - - then: - cmd.hasErrors() - cmd.errors.errorCount == 1 - cmd.errors.fieldError.field == "selectedDBSecurityGroups" - when: controller.save(cmd) From afc4b1a48a14b94f5f9c78408b347e39a0991029 Mon Sep 17 00:00:00 2001 From: Amadeo Casas Date: Tue, 18 Jun 2013 08:23:07 -0700 Subject: [PATCH 7/7] Improvements in DB create command tests - use of high level constructs in Groovy and a clearer organization. --- .../netflix/asgard/DbCreateCommandSpec.groovy | 111 +++++++++++++----- 1 file changed, 83 insertions(+), 28 deletions(-) diff --git a/test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy b/test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy index e947be86..176271fc 100644 --- a/test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy +++ b/test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy @@ -23,49 +23,104 @@ import spock.lang.Unroll @TestMixin(ControllerUnitTestMixin) class DbCreateCommandSpec extends Specification { - DbCreateCommand cmd + final createCommandParams = [ + allocatedStorage: 5, + backupRetentionPeriod: 0, + dBInstanceClass: 'dbClass', + dBInstanceIdentifier: 'testDB', + dBName: 'DBname', + masterUsername: 'testname', + masterUserPassword: 'testpassword', + port: 3306, + preferredBackupWindow: '', + preferredMaintenanceWindow: '' + ] void setup() { mockForConstraintsTests(DbCreateCommand) cmd = new DbCreateCommand() } - @Unroll("""should validate input to create RDS databases""") - def 'RDS database constraints'() { - cmd.allocatedStorage = 5 - cmd.backupRetentionPeriod = 0 - cmd.dBInstanceClass = "dbClass" - cmd.dBInstanceIdentifier = "testDB" - cmd.dBName = "DBname" - cmd.masterUsername = "testname" - cmd.masterUserPassword = "testpassword" - cmd.port = 3306 - cmd.preferredBackupWindow = "" - cmd.preferredMaintenanceWindow = "" - cmd.availabilityZone = availabilityZone - cmd.multiAZ = multiAZ - cmd.selectedSecurityGroups = selectedSecurityGroups - cmd.selectedDBSecurityGroups = selectedDBSecurityGroups - cmd.subnetPurpose = subnetPurpose + @Unroll("""should validate input to create an RDS database + with error code #errorAvailabilityZone + when availability zone is #availabilityZone and multi availability zone is #multiAZ""") + def 'RDS database constraints for availability zones'() { + DbCreateCommand cmd = new DbCreateCommand(createCommandParams).with { + it.availabilityZone = availabilityZone + it.multiAZ = multiAZ + } when: cmd.validate() then: cmd.errors.availabilityZone == errorAvailabilityZone + + where: + availabilityZone | multiAZ | errorAvailabilityZone + '' | '' | 'dbCreateCommand.multiaz.availabilityzones.error' + '' | 'on' | null + 'us-east-1a' | '' | null + 'us-east-1a' | 'on' | 'dbCreateCommand.multiaz.availabilityzones.error' + } + + @Unroll("""should validate input to create RDS databases + with error code #errorSelectedSecurityGroups + when the selected security groups are #selectedSecurityGroups and + the selected DB security groups #selectedDBSecurityGroups and + the subnet purpose is #subnetPurpose""") + def 'RDS database constraints for selected security groups'() { + DbCreateCommand cmd = new DbCreateCommand(createCommandParams).with { + it.subnetPurpose = subnetPurpose + it.selectedSecurityGroups = selectedSecurityGroups + it.selectedDBSecurityGroups = selectedDBSecurityGroups + } + + when: + cmd.validate() + + then: cmd.errors.selectedSecurityGroups == errorSelectedSecurityGroups + + where: + subnetPurpose | selectedSecurityGroups | selectedDBSecurityGroups | errorSelectedSecurityGroups + '' | null | null | null + '' | null | ['dbsecgroup'] | null + '' | ['secgroup'] | null | 'dbCreateCommand.selectedSecurityGroups.vpc.error' + '' | ['secgroup'] | ['dbsecgroup'] | 'dbCreateCommand.selectedSecurityGroups.vpc.error' + 'internal' | null | null | 'dbCreateCommand.selectedSecurityGroups.minSize.error' + 'internal' | null | ['dbsecgroup'] | null + 'internal' | ['secgroup'] | null | null + 'internal' | ['secgroup'] | ['dbsecgroup'] | null + } + + @Unroll("""should validate input to create RDS databases + with error code #errorSelectedDBSecurityGroups + when the selected security groups are #selectedSecurityGroups and + the selected DB security groups #selectedDBSecurityGroups and + the subnet purpose is #subnetPurpose""") + def 'RDS database constraints for selected DB security groups'() { + DbCreateCommand cmd = new DbCreateCommand(createCommandParams).with { + it.subnetPurpose = subnetPurpose + it.selectedSecurityGroups = selectedSecurityGroups + it.selectedDBSecurityGroups = selectedDBSecurityGroups + } + + when: + cmd.validate() + + then: cmd.errors.selectedDBSecurityGroups == errorSelectedDBSecurityGroups where: - availabilityZone | multiAZ | subnetPurpose | selectedSecurityGroups | selectedDBSecurityGroups | errorAvailabilityZone | errorSelectedSecurityGroups | errorSelectedDBSecurityGroups - "us-east-1a" | "" | "" | null | null | null | null | null - "" | "on" | "" | null | null | null | null | null - "us-east-1a" | "on" | "" | null | null | 'dbCreateCommand.multiaz.availabilityzones.error' | null | null - "" | "" | "" | null | null | 'dbCreateCommand.multiaz.availabilityzones.error' | null | null - "us-east-1a" | "" | "internal" | null | null | null | 'dbCreateCommand.selectedSecurityGroups.minSize.error' | null - "us-east-1a" | "" | "" | ["secgroup"] | null | null | 'dbCreateCommand.selectedSecurityGroups.vpc.error' | null - "us-east-1a" | "on" | "" | ["secgroup"] | null | 'dbCreateCommand.multiaz.availabilityzones.error' | 'dbCreateCommand.selectedSecurityGroups.vpc.error' | null - "us-east-1a" | "" | "internal" | ["secgroup"] | ["dbsecgroup"] | null | null | 'dbCreateCommand.selectedDBSecurityGroups.vpc.error' - "us-east-1a" | "on" | "internal" | ["secgroup"] | ["dbsecgroup"] | 'dbCreateCommand.multiaz.availabilityzones.error' | null | 'dbCreateCommand.selectedDBSecurityGroups.vpc.error' + subnetPurpose | selectedSecurityGroups | selectedDBSecurityGroups | errorSelectedDBSecurityGroups + '' | null | null | null + '' | null | ['dbsecgroup'] | null + '' | ['secgroup'] | null | null + '' | ['secgroup'] | ['dbsecgroup'] | null + 'internal' | null | null | 'dbCreateCommand.selectedSecurityGroups.minSize.error' + 'internal' | null | ['dbsecgroup'] | null + 'internal' | ['secgroup'] | null | null + 'internal' | ['secgroup'] | ['dbsecgroup'] | 'dbCreateCommand.selectedSecurityGroups.vpc.error' } }