diff --git a/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy b/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy index f82352cd..3d84ef66 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 @@ -449,25 +451,12 @@ class AutoScalingController { UserContext userContext = UserContext.of(request) String name = params.name AutoScalingGroup group = awsAutoScalingService.getAutoScalingGroup(userContext, name) - Boolean showGroupNext = 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." - } 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 - } + GroupDeleteOperation operation = pushService.startGroupDelete(userContext, group) + redirect(controller: 'task', action: 'show', params: [id: operation.taskId]) } - showGroupNext ? redirect(action: 'show', params: [id: name]) : redirect(action: 'list') } def postpone = { diff --git a/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy b/grails-app/controllers/com/netflix/asgard/RdsInstanceController.groovy index 5d40974c..96da46a7 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,23 @@ 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(), + 'vpcId': purposeToVpcId[params.subnetPurpose], 'zoneList':awsEc2Service.getAvailabilityZones(userContext) ] } @@ -73,17 +89,26 @@ class RdsInstanceController { } else { try { boolean multiAZ = "on".equals(params.multiAZ) - def selectedDBSecurityGroups = (params.selectedDBSecurityGroups instanceof String) ? [params.selectedDBSecurityGroups] : params.selectedDBSecurityGroups as List - if (!selectedDBSecurityGroups) selectedDBSecurityGroups = ["default"] - //awsRdsService.createDBSecurityGroup(params.name, params.description) + Subnets subnets = awsEc2Service.getSubnets(userContext) + List selectedSecurityGroups = Requests.ensureList(params.selectedSecurityGroups) + List selectedDBSecurityGroups = Requests.ensureList(params.selectedDBSecurityGroups) + 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) - .withAvailabilityZone(params.availabilityZone) .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) @@ -91,8 +116,13 @@ class RdsInstanceController { .withPreferredBackupWindow(params.preferredBackupWindow) .withPreferredMaintenanceWindow(params.preferredMaintenanceWindow) .withLicenseModel(params.licenseModel) + .withVpcSecurityGroups(selectedSecurityGroups) - awsRdsService.createDBInstance(userContext, dbInstance, params.masterUserPassword, params.port as Integer) + if (!multiAZ) { + dbInstance.availabilityZone = params.availabilityZone + } + + 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) { @@ -147,7 +177,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) } @@ -180,10 +218,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. @@ -196,12 +237,34 @@ 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 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 ece1a386..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 @@ -92,6 +93,9 @@ 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=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 @@ -102,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..b97bcddb 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,8 @@ 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.DescribeDBSubnetGroupsResult import com.amazonaws.services.rds.model.ModifyDBInstanceRequest import com.amazonaws.services.rds.model.RestoreDBInstanceFromDBSnapshotRequest import com.amazonaws.services.rds.model.RevokeDBSecurityGroupIngressRequest @@ -115,13 +118,34 @@ 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 -> - final DBInstance createdInstance = awsClient.by(userContext.region).createDBInstance(request) + if (subnetPurpose) { + DescribeDBSubnetGroupsResult 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) { + 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 + } + 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/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/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 @@ - + 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 @@
${dbi.dBInstanceStatus} ${dbi.dBName} ${dbi.dBInstanceClass}${dbi.availabilityZone} + All + ${dbi.availabilityZone} +
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/DbCreateCommandSpec.groovy b/test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy new file mode 100644 index 00000000..176271fc --- /dev/null +++ b/test/unit/com/netflix/asgard/DbCreateCommandSpec.groovy @@ -0,0 +1,126 @@ +/* + * 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 { + + 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 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: + 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' + } +} diff --git a/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy b/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy index dee07076..228df034 100644 --- a/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy +++ b/test/unit/com/netflix/asgard/RdsInstanceControllerSpec.groovy @@ -35,6 +35,38 @@ 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", + ] + + 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) @@ -55,6 +87,26 @@ 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: + controller.save(cmd) + + then: + '/rdsInstance/create' == response.redirectUrl + } + + def 'save should return exception when VPC and DB security groups specified'() { + def cmd = new DbCreateCommand(showVPCParams) + + 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 +131,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' 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;