diff --git a/moto/rds/models.py b/moto/rds/models.py index b55c97025..d394f933e 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -35,7 +35,13 @@ from .exceptions import ( SubscriptionNotFoundError, SubscriptionAlreadyExistError, ) -from .utils import FilterDef, apply_filter, merge_filters, validate_filters +from .utils import ( + FilterDef, + apply_filter, + merge_filters, + validate_filters, + valid_preferred_maintenance_window, +) class Cluster: @@ -446,9 +452,15 @@ class Database(CloudFormationModel): self.db_subnet_group = None self.security_groups = kwargs.get("security_groups", []) self.vpc_security_group_ids = kwargs.get("vpc_security_group_ids", []) - self.preferred_maintenance_window = kwargs.get( - "preferred_maintenance_window", "wed:06:38-wed:07:08" + self.preferred_maintenance_window = kwargs.get("preferred_maintenance_window") + self.preferred_backup_window = kwargs.get("preferred_backup_window") + msg = valid_preferred_maintenance_window( + self.preferred_maintenance_window, + self.preferred_backup_window, ) + if msg: + raise RDSClientError("InvalidParameterValue", msg) + self.db_parameter_group_name = kwargs.get("db_parameter_group_name") if ( self.db_parameter_group_name @@ -458,9 +470,6 @@ class Database(CloudFormationModel): ): raise DBParameterGroupNotFoundError(self.db_parameter_group_name) - self.preferred_backup_window = kwargs.get( - "preferred_backup_window", "13:14-13:44" - ) self.license_model = kwargs.get("license_model", "general-public-license") self.option_group_name = kwargs.get("option_group_name", None) self.option_group_supplied = self.option_group_name is not None @@ -554,8 +563,8 @@ class Database(CloudFormationModel): {{ database.db_instance_identifier }} {{ database.dbi_resource_id }} {{ database.instance_create_time }} - 03:50-04:20 - wed:06:38-wed:07:08 + {{ database.preferred_backup_window }} + {{ database.preferred_maintenance_window }} {% for replica_id in database.replicas %} {{ replica_id }} @@ -771,6 +780,12 @@ class Database(CloudFormationModel): "db_instance_class": properties.get("DBInstanceClass"), "db_instance_identifier": resource_name, "db_name": properties.get("DBName"), + "preferred_backup_window": properties.get( + "PreferredBackupWindow", "13:14-13:44" + ), + "preferred_maintenance_window": properties.get( + "PreferredMaintenanceWindow", "wed:06:38-wed:07:08" + ).lower(), "db_subnet_group_name": db_subnet_group_name, "engine": properties.get("Engine"), "engine_version": properties.get("EngineVersion"), @@ -1448,6 +1463,13 @@ class RDSBackend(BaseBackend): "db_instance_identifier" ] = db_kwargs.pop("new_db_instance_identifier") self.databases[db_instance_identifier] = database + preferred_backup_window = db_kwargs.get("preferred_backup_window") + preferred_maintenance_window = db_kwargs.get("preferred_maintenance_window") + msg = valid_preferred_maintenance_window( + preferred_maintenance_window, preferred_backup_window + ) + if msg: + raise RDSClientError("InvalidParameterValue", msg) database.update(db_kwargs) return database diff --git a/moto/rds/responses.py b/moto/rds/responses.py index 814e9d828..61cf35441 100644 --- a/moto/rds/responses.py +++ b/moto/rds/responses.py @@ -44,8 +44,12 @@ class RDSResponse(BaseResponse): "multi_az": self._get_bool_param("MultiAZ"), "option_group_name": self._get_param("OptionGroupName"), "port": self._get_param("Port"), - # PreferredBackupWindow - # PreferredMaintenanceWindow + "preferred_backup_window": self._get_param( + "PreferredBackupWindow", "13:14-13:44" + ), + "preferred_maintenance_window": self._get_param( + "PreferredMaintenanceWindow", "wed:06:38-wed:07:08" + ).lower(), "publicly_accessible": self._get_param("PubliclyAccessible"), "account_id": self.current_account, "region": self.region, diff --git a/moto/rds/utils.py b/moto/rds/utils.py index d5c3dca72..667aad638 100644 --- a/moto/rds/utils.py +++ b/moto/rds/utils.py @@ -3,7 +3,10 @@ from collections import namedtuple from botocore.utils import merge_dicts from collections import OrderedDict +import datetime +import re +SECONDS_IN_ONE_DAY = 24 * 60 * 60 FilterDef = namedtuple( "FilterDef", [ @@ -133,3 +136,155 @@ def apply_filter(resources, filters, filter_defs): if matches_filter: resources_filtered[identifier] = obj return resources_filtered + + +def get_start_date_end_date(base_date, window): + """Gets the start date and end date given DDD:HH24:MM-DDD:HH24:MM. + + :param base_date: + type datetime + :param window: + DDD:HH24:MM-DDD:HH24:MM + :returns: + Start and End Date in datetime format + :rtype: + tuple + """ + days = {"mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6, "sun": 7} + start = datetime.datetime.strptime( + base_date + " " + window[4:9], "%d/%m/%y %H:%M" + ) + datetime.timedelta(days=days[window[0:3]]) + end = datetime.datetime.strptime( + base_date + " " + window[14::], "%d/%m/%y %H:%M" + ) + datetime.timedelta(days=days[window[10:13]]) + return start, end + + +def get_start_date_end_date_from_time(base_date, window): + """Gets the start date and end date given HH24:MM-HH24:MM. + + :param base_date: + type datetime + :param window: + HH24:MM-HH24:MM + :returns: + Start and End Date in datetime format + along with flag for spills over a day + This is useful when determine time overlaps + :rtype: + tuple + """ + times = window.split("-") + spillover = False + start = datetime.datetime.strptime(base_date + " " + times[0], "%d/%m/%y %H:%M") + end = datetime.datetime.strptime(base_date + " " + times[1], "%d/%m/%y %H:%M") + if end < start: + end += datetime.timedelta(days=1) + spillover = True + return start, end, spillover + + +def get_overlap_between_two_date_ranges( + start_time_1, end_time_1, start_time_2, end_time_2 +): + """Determines overlap between 2 date ranges. + + :param start_time_1: + type datetime + :param start_time_2: + type datetime + :param end_time_1: + type datetime + :param end_time_2: + type datetime + :returns: + overlap in seconds + :rtype: + int + """ + latest_start = max(start_time_1, start_time_2) + earliest_end = min(end_time_1, end_time_2) + delta = earliest_end - latest_start + overlap = (delta.days * SECONDS_IN_ONE_DAY) + delta.seconds + return overlap + + +def valid_preferred_maintenance_window(maintenance_window, backup_window): + """Determines validity of preferred_maintenance_window + + :param maintenance_windown: + type DDD:HH24:MM-DDD:HH24:MM + :param backup_window: + type HH24:MM-HH24:MM + :returns: + message + :rtype: + str + """ + MINUTES_30 = 1800 + HOURS_24 = 86400 + base_date = datetime.datetime.now().strftime("%d/%m/%y") + try: + p = re.compile( + "([a-z]{3}):([0-9]{2}):([0-9]{2})-([a-z]{3}):([0-9]{2}):([0-9]{2})" + ) + if len(maintenance_window) != 19 or re.search(p, maintenance_window) is None: + return f"Invalid maintenance window format: {maintenance_window}. Should be specified as a range ddd:hh24:mi-ddd:hh24:mi (24H Clock UTC). Example: Sun:23:45-Mon:00:15" + if backup_window: + ( + backup_window_start, + backup_window_end, + backup_spill, + ) = get_start_date_end_date_from_time(base_date, backup_window) + ( + maintenance_window_start, + maintenance_window_end, + maintenance_spill, + ) = get_start_date_end_date_from_time( + base_date, maintenance_window[4:10] + maintenance_window[14::] + ) + if ( + get_overlap_between_two_date_ranges( + backup_window_start, + backup_window_end, + maintenance_window_start, + maintenance_window_end, + ) + >= 0 + ): + return "The backup window and maintenance window must not overlap." + + # Due to spill overs, adjust the windows + elif maintenance_spill: + backup_window_start += datetime.timedelta(days=1) + backup_window_end += datetime.timedelta(days=1) + elif backup_spill: + maintenance_window_start += datetime.timedelta(days=1) + maintenance_window_end += datetime.timedelta(days=1) + + # If spills, rerun overlap test with adjusted windows + if maintenance_spill or backup_spill: + if ( + get_overlap_between_two_date_ranges( + backup_window_start, + backup_window_end, + maintenance_window_start, + maintenance_window_end, + ) + >= 0 + ): + return "The backup window and maintenance window must not overlap." + + maintenance_window_start, maintenance_window_end = get_start_date_end_date( + base_date, maintenance_window + ) + delta = maintenance_window_end - maintenance_window_start + delta_seconds = delta.seconds + (delta.days * SECONDS_IN_ONE_DAY) + if delta_seconds >= MINUTES_30 and delta_seconds <= HOURS_24: + return + elif delta_seconds >= 0 and delta_seconds <= MINUTES_30: + return "The maintenance window must be at least 30 minutes." + else: + return "Maintenance window must be less than 24 hours." + except Exception: + return f"Invalid day:hour:minute value: {maintenance_window}" diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py index 006903788..cb452c06b 100644 --- a/tests/test_rds/test_rds.py +++ b/tests/test_rds/test_rds.py @@ -75,6 +75,179 @@ def test_create_database_no_allocated_storage(): db_instance["Engine"].should.equal("postgres") db_instance["StorageType"].should.equal("gp2") db_instance["AllocatedStorage"].should.equal(20) + db_instance["PreferredMaintenanceWindow"].should.equal("wed:06:38-wed:07:08") + + +@mock_rds +def test_create_database_invalid_preferred_maintenance_window_more_24_hours(): + conn = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="mon:16:00-tue:17:00", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal("Maintenance window must be less than 24 hours.") + + +@mock_rds +def test_create_database_invalid_preferred_maintenance_window_less_30_mins(): + conn = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="mon:16:00-mon:16:05", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal("The maintenance window must be at least 30 minutes.") + + +@mock_rds +def test_create_database_invalid_preferred_maintenance_window_value(): + conn = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="sim:16:00-mon:16:30", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.contain("Invalid day:hour:minute") + + +@mock_rds +def test_create_database_invalid_preferred_maintenance_window_format(): + conn = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="mon:16tue:17:00", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.contain( + "Should be specified as a range ddd:hh24:mi-ddd:hh24:mi (24H Clock UTC). Example: Sun:23:45-Mon:00:15" + ) + + +@mock_rds +def test_create_database_preferred_backup_window_overlap_no_spill(): + conn = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="wed:18:00-wed:22:00", + PreferredBackupWindow="20:00-20:30", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.contain( + "The backup window and maintenance window must not overlap." + ) + + +@mock_rds +def test_create_database_preferred_backup_window_overlap_maintenance_window_spill(): + conn = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="wed:18:00-thu:01:00", + PreferredBackupWindow="00:00-00:30", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.contain( + "The backup window and maintenance window must not overlap." + ) + + +@mock_rds +def test_create_database_preferred_backup_window_overlap_backup_window_spill(): + conn = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="thu:00:00-thu:14:00", + PreferredBackupWindow="23:50-00:20", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.contain( + "The backup window and maintenance window must not overlap." + ) + + +@mock_rds +def test_create_database_preferred_backup_window_overlap_both_spill(): + conn = boto3.client("rds", region_name="us-west-2") + with pytest.raises(ClientError) as ex: + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="wed:18:00-thu:01:00", + PreferredBackupWindow="23:50-00:20", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.contain( + "The backup window and maintenance window must not overlap." + ) + + +@mock_rds +def test_create_database_valid_preferred_maintenance_window_format(): + conn = boto3.client("rds", region_name="us-west-2") + database = conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="sun:16:00-sun:16:30", + ) + db_instance = database["DBInstance"] + db_instance["DBInstanceClass"].should.equal("db.m1.small") + db_instance["PreferredMaintenanceWindow"].should.equal("sun:16:00-sun:16:30") + + +@mock_rds +def test_create_database_valid_preferred_maintenance_window_uppercase_format(): + conn = boto3.client("rds", region_name="us-west-2") + database = conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PreferredMaintenanceWindow="MON:16:00-TUE:01:30", + ) + db_instance = database["DBInstance"] + db_instance["DBInstanceClass"].should.equal("db.m1.small") + db_instance["PreferredMaintenanceWindow"].should.equal("mon:16:00-tue:01:30") @mock_rds @@ -397,6 +570,9 @@ def test_modify_db_instance(): ) instances = conn.describe_db_instances(DBInstanceIdentifier="db-master-1") instances["DBInstances"][0]["AllocatedStorage"].should.equal(20) + instances["DBInstances"][0]["PreferredMaintenanceWindow"].should.equal( + "wed:06:38-wed:07:08" + ) instances["DBInstances"][0]["VpcSecurityGroups"][0][ "VpcSecurityGroupId" ].should.equal("sg-123456") @@ -423,6 +599,252 @@ def test_modify_db_instance_not_existent_db_parameter_group_name(): ).should.throw(ClientError) +@mock_rds +def test_modify_db_instance_valid_preferred_maintenance_window(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + instances = conn.describe_db_instances(DBInstanceIdentifier="db-master-1") + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="sun:16:00-sun:16:30", + ) + instances = conn.describe_db_instances(DBInstanceIdentifier="db-master-1") + instances["DBInstances"][0]["PreferredMaintenanceWindow"].should.equal( + "sun:16:00-sun:16:30" + ) + + +@mock_rds +def test_modify_db_instance_valid_preferred_maintenance_window_uppercase(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + instances = conn.describe_db_instances(DBInstanceIdentifier="db-master-1") + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="SUN:16:00-SUN:16:30", + ) + instances = conn.describe_db_instances(DBInstanceIdentifier="db-master-1") + instances["DBInstances"][0]["PreferredMaintenanceWindow"].should.equal( + "sun:16:00-sun:16:30" + ) + + +@mock_rds +def test_modify_db_instance_invalid_preferred_maintenance_window_more_than_24_hours(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + with pytest.raises(ClientError) as ex: + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="sun:16:00-sat:16:30", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal("Maintenance window must be less than 24 hours.") + + +@mock_rds +def test_modify_db_instance_invalid_preferred_maintenance_window_less_than_30_mins(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + with pytest.raises(ClientError) as ex: + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="sun:16:00-sun:16:10", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal("The maintenance window must be at least 30 minutes.") + + +@mock_rds +def test_modify_db_instance_invalid_preferred_maintenance_window_value(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + with pytest.raises(ClientError) as ex: + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="sin:16:00-sun:16:30", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.contain("Invalid day:hour:minute value") + + +@mock_rds +def test_modify_db_instance_invalid_preferred_maintenance_window_format(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + with pytest.raises(ClientError) as ex: + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="sun:16:00sun:16:30", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.contain( + "Should be specified as a range ddd:hh24:mi-ddd:hh24:mi (24H Clock UTC). Example: Sun:23:45-Mon:00:15" + ) + + +@mock_rds +def test_modify_db_instance_maintenance_backup_window_no_spill(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + with pytest.raises(ClientError) as ex: + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="sun:16:00-sun:16:30", + PreferredBackupWindow="15:50-16:20", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal( + "The backup window and maintenance window must not overlap." + ) + + +@mock_rds +def test_modify_db_instance_maintenance_backup_window_maintenance_spill(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + with pytest.raises(ClientError) as ex: + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="sun:16:00-mon:15:00", + PreferredBackupWindow="00:00-00:30", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal( + "The backup window and maintenance window must not overlap." + ) + + +@mock_rds +def test_modify_db_instance_maintenance_backup_window_backup_spill(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + with pytest.raises(ClientError) as ex: + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="mon:00:00-mon:15:00", + PreferredBackupWindow="23:50-00:20", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal( + "The backup window and maintenance window must not overlap." + ) + + +@mock_rds +def test_modify_db_instance_maintenance_backup_window_both_spill(): + conn = boto3.client("rds", region_name="us-west-2") + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + DBInstanceClass="postgres", + Engine="db.m1.small", + MasterUsername="root", + MasterUserPassword="hunter2", + Port=1234, + DBSecurityGroups=["my_sg"], + ) + with pytest.raises(ClientError) as ex: + conn.modify_db_instance( + DBInstanceIdentifier="db-master-1", + PreferredMaintenanceWindow="sun:16:00-mon:15:00", + PreferredBackupWindow="23:20-00:20", + ) + err = ex.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal( + "The backup window and maintenance window must not overlap." + ) + + @mock_rds def test_rename_db_instance(): conn = boto3.client("rds", region_name="us-west-2")