summaryrefslogtreecommitdiffstats
path: root/ansible_collections/amazon/aws/plugins/modules/rds_instance.py
blob: 0362df0ba2fbb811389ec332c09e2dde5475e391 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = r"""
---
module: rds_instance
version_added: 5.0.0
short_description: Manage RDS instances
description:
 - Create, modify, and delete RDS instances.
 - This module was originally added to C(community.aws) in release 1.0.0.
extends_documentation_fragment:
 - amazon.aws.common.modules
 - amazon.aws.region.modules
 - amazon.aws.tags
 - amazon.aws.boto3
author:
 - Sloane Hertel (@s-hertel)

options:
  # General module options
    state:
        description:
          - Desired state of the RDS Instance.
          - I(state=rebooted) is not idempotent and will leave the DB instance in a running state
            and start it prior to rebooting if it was stopped. I(present) will leave the DB instance in the current running/stopped state,
            (running if creating the DB instance).
          - I(state=running) and I(state=started) are synonyms, as are I(state=rebooted) and I(state=restarted).
        choices: ['present', 'absent', 'terminated', 'running', 'started', 'stopped', 'rebooted', 'restarted']
        default: 'present'
        type: str
    creation_source:
        description: Which source to use if restoring from a template (an existing instance, S3 bucket, or snapshot).
        choices: ['snapshot', 's3', 'instance']
        type: str
    force_update_password:
        description:
          - Set to C(true) to update your instance password with I(master_user_password). Since comparing passwords to determine
            if it needs to be updated is not possible this is set to c(false) by default to allow idempotence.
        type: bool
        default: false
    purge_cloudwatch_logs_exports:
        description:
          - Set to C(false) to retain any enabled cloudwatch logs that aren't specified in the task and are associated with the instance.
          - Set I(enable_cloudwatch_logs_exports) to an empty list to disable all.
        type: bool
        default: true
    read_replica:
        description:
          - Set to C(false) to promote a read replica instance or C(true) to create one. When creating a read replica C(creation_source) should
            be set to 'instance' or not provided. C(source_db_instance_identifier) must be provided with this option.
        type: bool
    wait:
        description:
          - Whether to wait for the instance to be available, stopped, or deleted. At a later time a I(wait_timeout) option may be added.
            Following each API call to create/modify/delete the instance a waiter is used with a 60 second delay 30 times until the instance reaches
            the expected state (available/stopped/deleted). The total task time may also be influenced by AWSRetry which helps stabilize if the
            instance is in an invalid state to operate on to begin with (such as if you try to stop it when it is in the process of rebooting).
            If setting this to C(false) task retries and delays may make your playbook execution better handle timeouts for major modifications.
        type: bool
        default: true

    # Options that have a corresponding boto3 parameter
    allocated_storage:
        description:
          - The amount of storage (in gibibytes) to allocate for the DB instance.
        type: int
    allow_major_version_upgrade:
        description:
          - Whether to allow major version upgrades.
        type: bool
    apply_immediately:
        description:
          - A value that specifies whether modifying an instance with I(new_db_instance_identifier) and I(master_user_password)
            should be applied as soon as possible, regardless of the I(preferred_maintenance_window) setting. If C(false), changes
            are applied during the next maintenance window.
        type: bool
        default: false
    auto_minor_version_upgrade:
        description:
          - Whether minor version upgrades are applied automatically to the DB instance during the maintenance window.
        type: bool
    availability_zone:
        description:
          - A list of EC2 Availability Zones that the DB instance can be created in.
            May be used when creating an instance or when restoring from S3 or a snapshot. Mutually exclusive with I(multi_az).
        aliases:
          - az
          - zone
        type: str
    backup_retention_period:
        description:
          - The number of days for which automated backups are retained.
          - When set to C(0), automated backups will be disabled. (Not applicable if the DB instance is a source to read replicas)
          - May be used when creating a new instance, when restoring from S3, or when modifying an instance.
        type: int
    ca_certificate_identifier:
        description:
          - The identifier of the CA certificate for the DB instance.
        type: str
    character_set_name:
        description:
          - The character set to associate with the DB instance.
        type: str
    copy_tags_to_snapshot:
        description:
          - Whether or not to copy all tags from the DB instance to snapshots of the instance. When initially creating
            a DB instance the RDS API defaults this to C(false) if unspecified.
        type: bool
    db_cluster_identifier:
        description:
          - The DB cluster (lowercase) identifier to add the aurora DB instance to. The identifier must contain from 1 to
            63 letters, numbers, or hyphens and the first character must be a letter and may not end in a hyphen or
            contain consecutive hyphens.
        aliases:
          - cluster_id
        type: str
    db_instance_class:
        description:
          - The compute and memory capacity of the DB instance, for example db.t2.micro.
        aliases:
          - class
          - instance_type
        type: str
    db_instance_identifier:
        description:
          - The DB instance (lowercase) identifier. The identifier must contain from 1 to 63 letters, numbers, or
            hyphens and the first character must be a letter and may not end in a hyphen or contain consecutive hyphens.
        aliases:
          - instance_id
          - id
        required: true
        type: str
    db_name:
        description:
          - The name for your database. If a name is not provided Amazon RDS will not create a database.
        type: str
    db_parameter_group_name:
        description:
          - The name of the DB parameter group to associate with this DB instance. When creating the DB instance if this
            argument is omitted the default DBParameterGroup for the specified engine is used.
        type: str
    db_security_groups:
        description:
          - (EC2-Classic platform) A list of DB security groups to associate with this DB instance.
        type: list
        elements: str
    db_snapshot_identifier:
        description:
          - The identifier or ARN of the DB snapshot to restore from when using I(creation_source=snapshot).
        type: str
        aliases:
          - snapshot_identifier
          - snapshot_id
    db_subnet_group_name:
        description:
          - The DB subnet group name to use for the DB instance.
        aliases:
          - subnet_group
        type: str
    deletion_protection:
        description:
          -  A value that indicates whether the DB instance has deletion protection enabled.
             The database can't be deleted when deletion protection is enabled.
             By default, deletion protection is disabled.
        type: bool
        version_added: 3.3.0
        version_added_collection: community.aws
    domain:
        description:
          - The Active Directory Domain to restore the instance in.
        type: str
    domain_iam_role_name:
        description:
          - The name of the IAM role to be used when making API calls to the Directory Service.
        type: str
    enable_cloudwatch_logs_exports:
        description:
          - A list of log types that need to be enabled for exporting to CloudWatch Logs.
        aliases:
          - cloudwatch_log_exports
        type: list
        elements: str
    enable_iam_database_authentication:
        description:
          - Enable mapping of AWS Identity and Access Management (IAM) accounts to database accounts.
            If this option is omitted when creating the instance, Amazon RDS sets this to C(false).
        type: bool
    enable_performance_insights:
        description:
          - Whether to enable Performance Insights for the DB instance.
        type: bool
    engine:
        description:
          - The name of the database engine to be used for this DB instance. This is required to create an instance.
        choices: ['aurora', 'aurora-mysql', 'aurora-postgresql', 'mariadb', 'mysql', 'oracle-ee', 'oracle-ee-cdb',
                  'oracle-se2', 'oracle-se2-cdb', 'postgres', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web']
        type: str
    engine_version:
        description:
          - The version number of the database engine to use. For Aurora MySQL that could be C(5.6.10a) , C(5.7.12).
            Aurora PostgreSQL example, C(9.6.3)
        type: str
    final_db_snapshot_identifier:
        description:
          - The DB instance snapshot identifier of the new DB instance snapshot created when I(skip_final_snapshot) is C(false).
        aliases:
          - final_snapshot_identifier
        type: str
    force_failover:
        description:
          - Set to C(true) to conduct the reboot through a MultiAZ failover.
        type: bool
    iam_roles:
        description:
          - List of Amazon Web Services Identity and Access Management (IAM) roles to associate with DB instance.
        type: list
        elements: dict
        suboptions:
          feature_name:
            description:
              - The name of the feature associated with the IAM role.
            type: str
            required: true
          role_arn:
            description:
              - The ARN of the IAM role to associate with the DB instance.
            type: str
            required: true
        version_added: 3.3.0
        version_added_collection: community.aws
    iops:
        description:
          - The Provisioned IOPS (I/O operations per second) value. Is only set when using I(storage_type) is set to io1.
        type: int
    kms_key_id:
        description:
          - The ARN of the AWS KMS key identifier for an encrypted DB instance. If you are creating a DB instance with the
            same AWS account that owns the KMS encryption key used to encrypt the new DB instance, then you can use the KMS key
            alias instead of the ARN for the KM encryption key.
          - If I(storage_encrypted) is C(true) and and this option is not provided, the default encryption key is used.
        type: str
    license_model:
        description:
          - The license model for the DB instance.
          - Several options are license-included, bring-your-own-license, and general-public-license.
          - This option can also be omitted to default to an accepted value.
        type: str
    master_user_password:
        description:
          - An 8-41 character password for the master database user. The password can contain any printable ASCII character
            except C(/), C("), or C(@). To modify the password use I(force_update_password). Use I(apply_immediately) to change
            the password immediately, otherwise it is updated during the next maintenance window.
        aliases:
          - password
        type: str
    master_username:
        description:
          - The name of the master user for the DB instance. Must be 1-16 letters or numbers and begin with a letter.
        aliases:
          - username
        type: str
    max_allocated_storage:
        description:
          - The upper limit to which Amazon RDS can automatically scale the storage of the DB instance.
        type: int
    monitoring_interval:
        description:
          - The interval, in seconds, when Enhanced Monitoring metrics are collected for the DB instance. To disable collecting
            metrics, specify C(0). Amazon RDS defaults this to 0 if omitted when initially creating a DB instance.
        type: int
    monitoring_role_arn:
        description:
          - The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs.
        type: str
    multi_az:
        description:
          - Specifies if the DB instance is a Multi-AZ deployment. Mutually exclusive with I(availability_zone).
        type: bool
    new_db_instance_identifier:
        description:
          - The new DB instance (lowercase) identifier for the DB instance when renaming a DB instance. The identifier must contain
            from 1 to 63 letters, numbers, or hyphens and the first character must be a letter and may not end in a hyphen or
            contain consecutive hyphens. Use I(apply_immediately) to rename immediately, otherwise it is updated during the
            next maintenance window.
        aliases:
          - new_instance_id
          - new_id
        type: str
    option_group_name:
        description:
          - The option group to associate with the DB instance.
        type: str
    performance_insights_kms_key_id:
        description:
          - The AWS KMS key identifier (ARN, name, or alias) for encryption of Performance Insights data.
        type: str
    performance_insights_retention_period:
        description:
          - The amount of time, in days, to retain Performance Insights data. Valid values are 7 or 731.
        type: int
    port:
        description:
          - The port number on which the instances accept connections.
        type: int
    preferred_backup_window:
        description:
          - The daily time range (in UTC) of at least 30 minutes, during which automated backups are created if automated backups are
            enabled using I(backup_retention_period). The option must be in the format of "hh24:mi-hh24:mi" and not conflict with
            I(preferred_maintenance_window).
        aliases:
          - backup_window
        type: str
    preferred_maintenance_window:
        description:
          - The weekly time range (in UTC) of at least 30 minutes, during which system maintenance can occur. The option must
            be in the format "ddd:hh24:mi-ddd:hh24:mi" where ddd is one of Mon, Tue, Wed, Thu, Fri, Sat, Sun.
        aliases:
          - maintenance_window
        type: str
    processor_features:
        description:
          - A dictionary of Name, Value pairs to indicate the number of CPU cores and the number of threads per core for the
            DB instance class of the DB instance. Names are threadsPerCore and coreCount.
            Set this option to an empty dictionary to use the default processor features.
        suboptions:
          threadsPerCore:
            description: The number of threads per core
          coreCount:
            description: The number of CPU cores
        type: dict
    promotion_tier:
        description:
          - An integer that specifies the order in which an Aurora Replica is promoted to the primary instance after a failure of
            the existing primary instance.
        type: int
    publicly_accessible:
        description:
          - Specifies the accessibility options for the DB instance. A value of C(true) specifies an Internet-facing instance with
            a publicly resolvable DNS name, which resolves to a public IP address. A value of C(false) specifies an internal
            instance with a DNS name that resolves to a private IP address.
        type: bool
    purge_iam_roles:
        description:
          - Set to C(true) to remove any IAM roles that aren't specified in the task and are associated with the instance.
        type: bool
        default: false
        version_added: 3.3.0
        version_added_collection: community.aws
    restore_time:
        description:
          - If using I(creation_source=instance) this indicates the UTC date and time to restore from the source instance.
            For example, "2009-09-07T23:45:00Z".
          - May alternatively set I(use_latest_restore_time=true).
          - Only one of I(use_latest_restorable_time) and I(restore_time) may be provided.
        type: str
    s3_bucket_name:
        description:
          - The name of the Amazon S3 bucket that contains the data used to create the Amazon DB instance.
        type: str
    s3_ingestion_role_arn:
        description:
          - The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that authorizes Amazon RDS to access
            the Amazon S3 bucket on your behalf.
        type: str
    s3_prefix:
        description:
          - The prefix for all of the file names that contain the data used to create the Amazon DB instance. If you do not
            specify a SourceS3Prefix value, then the Amazon DB instance is created by using all of the files in the Amazon S3 bucket.
        type: str
    skip_final_snapshot:
        description:
          - Whether a final DB instance snapshot is created before the DB instance is deleted. If this is C(false) I(final_db_snapshot_identifier)
            must be provided.
        type: bool
        default: false
    source_db_instance_identifier:
        description:
          - The identifier or ARN of the source DB instance from which to restore when creating a read replica or spinning up a point-in-time
            DB instance using I(creation_source=instance). If the source DB is not in the same region this should be an ARN.
        type: str
    source_engine:
        description:
          - The identifier for the database engine that was backed up to create the files stored in the Amazon S3 bucket.
        choices:
          - mysql
        type: str
    source_engine_version:
        description:
          - The version of the database that the backup files were created from.
        type: str
    source_region:
        description:
          - The region of the DB instance from which the replica is created.
        type: str
    storage_encrypted:
        description:
          - Whether the DB instance is encrypted.
        type: bool
    storage_type:
        description:
          - The storage type to be associated with the DB instance. I(storage_type) does not apply to Aurora DB instances.
        choices:
          - standard
          - gp2
          - gp3
          - io1
        type: str
    storage_throughput:
      description:
        - The storage throughput when the I(storage_type) is C(gp3).
        - When the allocated storage is below 400 GB, the storage throughput will always be 125 mb/s.
        - When the allocated storage is large than or equal 400 GB, the througput starts at 500 mb/s.
        - Requires botocore >= 1.29.0.
      type: int
      version_added: 5.2.0
    tde_credential_arn:
        description:
          - The ARN from the key store with which to associate the instance for Transparent Data Encryption. This is
            supported by Oracle or SQL Server DB instances and may be used in conjunction with C(storage_encrypted)
            though it might slightly affect the performance of your database.
        aliases:
          - transparent_data_encryption_arn
        type: str
    tde_credential_password:
        description:
          - The password for the given ARN from the key store in order to access the device.
        aliases:
          - transparent_data_encryption_password
        type: str
    timezone:
        description:
          - The time zone of the DB instance.
        type: str
    use_latest_restorable_time:
        description:
          - Whether to restore the DB instance to the latest restorable backup time.
          - Only one of I(use_latest_restorable_time) and I(restore_time) may be provided.
        type: bool
        aliases:
          - restore_from_latest
    vpc_security_group_ids:
        description:
          - A list of EC2 VPC security groups to associate with the DB instance.
        type: list
        elements: str
    purge_security_groups:
        description:
          - Set to C(false) to retain any enabled security groups that aren't specified in the task and are associated with the instance.
          - Can be applied to I(vpc_security_group_ids) and I(db_security_groups)
        type: bool
        default: true
        version_added: 1.5.0
        version_added_collection: community.aws
"""

EXAMPLES = r"""
# Note: These examples do not set authentication details, see the AWS Guide for details.
- name: create minimal aurora instance in default VPC and default subnet group
  amazon.aws.rds_instance:
    engine: aurora
    db_instance_identifier: ansible-test-aurora-db-instance
    instance_type: db.t2.small
    password: "{{ password }}"
    username: "{{ username }}"
    cluster_id: ansible-test-cluster  # This cluster must exist - see rds_cluster to manage it

- name: Create a DB instance using the default AWS KMS encryption key
  amazon.aws.rds_instance:
    id: test-encrypted-db
    state: present
    engine: mariadb
    storage_encrypted: true
    db_instance_class: db.t2.medium
    username: "{{ username }}"
    password: "{{ password }}"
    allocated_storage: "{{ allocated_storage }}"

- name: remove the DB instance without a final snapshot
  amazon.aws.rds_instance:
    id: "{{ instance_id }}"
    state: absent
    skip_final_snapshot: true

- name: remove the DB instance with a final snapshot
  amazon.aws.rds_instance:
    id: "{{ instance_id }}"
    state: absent
    final_snapshot_identifier: "{{ snapshot_id }}"

- name: Add a new security group without purge
  amazon.aws.rds_instance:
    id: "{{ instance_id }}"
    state: present
    vpc_security_group_ids:
      - sg-0be17ba10c9286b0b
    purge_security_groups: false
  register: result

# Add IAM role to db instance
- name: Create IAM policy
  amazon.aws.iam_managed_policy:
    policy_name: "my-policy"
    policy: "{{ lookup('file','files/policy.json') }}"
    state: present
  register: iam_policy

- name: Create IAM role
  community.aws.iam_role:
    assume_role_policy_document: "{{ lookup('file','files/assume_policy.json') }}"
    name: "my-role"
    state: present
    managed_policy: "{{ iam_policy.policy.arn }}"
  register: iam_role

- name: Create DB instance with added IAM role
  amazon.aws.rds_instance:
    id: "my-instance-id"
    state: present
    engine: postgres
    engine_version: 14.2
    username: "{{ username }}"
    password: "{{ password }}"
    db_instance_class: db.m6g.large
    allocated_storage: "{{ allocated_storage }}"
    iam_roles:
      - role_arn: "{{ iam_role.arn }}"
        feature_name: 's3Export'

- name: Remove IAM role from DB instance
  amazon.aws.rds_instance:
    id: "my-instance-id"
    state: present
    purge_iam_roles: true

# Restore DB instance from snapshot
- name: Create a snapshot and wait until completion
  amazon.aws.rds_instance_snapshot:
    instance_id: 'my-instance-id'
    snapshot_id: 'my-new-snapshot'
    state: present
    wait: true
  register: snapshot

- name: Restore DB from snapshot
  amazon.aws.rds_instance:
    id: 'my-restored-db'
    creation_source: snapshot
    snapshot_identifier: 'my-new-snapshot'
    engine: mariadb
    state: present
  register: restored_db
"""

RETURN = r"""
allocated_storage:
  description: The allocated storage size in gigabytes. This is always 1 for aurora database engines.
  returned: always
  type: int
  sample: 20
associated_roles:
  description: The list of currently associated roles.
  returned: always
  type: list
  sample: []
auto_minor_version_upgrade:
  description: Whether minor engine upgrades are applied automatically to the DB instance during the maintenance window.
  returned: always
  type: bool
  sample: true
availability_zone:
  description: The availability zone for the DB instance.
  returned: always
  type: str
  sample: us-east-1f
backup_retention_period:
  description: The number of days for which automated backups are retained.
  returned: always
  type: int
  sample: 1
ca_certificate_identifier:
  description:
    - The identifier of the CA certificate for the DB instance.
    - Requires minimum botocore version 1.29.44.
  returned: always
  type: str
  sample: rds-ca-2015
copy_tags_to_snapshot:
  description: Whether tags are copied from the DB instance to snapshots of the DB instance.
  returned: always
  type: bool
  sample: false
db_instance_arn:
  description: The Amazon Resource Name (ARN) for the DB instance.
  returned: always
  type: str
  sample: arn:aws:rds:us-east-1:123456789012:db:ansible-test
db_instance_class:
  description: The name of the compute and memory capacity class of the DB instance.
  returned: always
  type: str
  sample: db.m4.large
db_instance_identifier:
  description: The identifier of the DB instance
  returned: always
  type: str
  sample: ansible-test
db_instance_port:
  description: The port that the DB instance listens on.
  returned: always
  type: int
  sample: 0
db_instance_status:
  description: The current state of this database.
  returned: always
  type: str
  sample: stopped
db_parameter_groups:
  description: The list of DB parameter groups applied to this DB instance.
  returned: always
  type: complex
  contains:
    db_parameter_group_name:
      description: The name of the DP parameter group.
      returned: always
      type: str
      sample: default.mariadb10.0
    parameter_apply_status:
      description: The status of parameter updates.
      returned: always
      type: str
      sample: in-sync
db_security_groups:
  description: A list of DB security groups associated with this DB instance.
  returned: always
  type: list
  sample: []
db_subnet_group:
  description: The subnet group associated with the DB instance.
  returned: always
  type: complex
  contains:
    db_subnet_group_description:
      description: The description of the DB subnet group.
      returned: always
      type: str
      sample: default
    db_subnet_group_name:
      description: The name of the DB subnet group.
      returned: always
      type: str
      sample: default
    subnet_group_status:
      description: The status of the DB subnet group.
      returned: always
      type: str
      sample: Complete
    subnets:
      description: A list of Subnet elements.
      returned: always
      type: complex
      contains:
        subnet_availability_zone:
          description: The availability zone of the subnet.
          returned: always
          type: complex
          contains:
            name:
              description: The name of the Availability Zone.
              returned: always
              type: str
              sample: us-east-1c
        subnet_identifier:
          description: The ID of the subnet.
          returned: always
          type: str
          sample: subnet-12345678
        subnet_status:
          description: The status of the subnet.
          returned: always
          type: str
          sample: Active
    vpc_id:
      description: The VpcId of the DB subnet group.
      returned: always
      type: str
      sample: vpc-12345678
dbi_resource_id:
  description: The AWS Region-unique, immutable identifier for the DB instance.
  returned: always
  type: str
  sample: db-UHV3QRNWX4KB6GALCIGRML6QFA
deletion_protection:
  description: C(true) if the DB instance has deletion protection enabled, C(False) if not.
  returned: always
  type: bool
  sample: False
  version_added: 3.3.0
  version_added_collection: community.aws
domain_memberships:
  description: The Active Directory Domain membership records associated with the DB instance.
  returned: always
  type: list
  sample: []
endpoint:
  description: The connection endpoint.
  returned: always
  type: complex
  contains:
    address:
      description: The DNS address of the DB instance.
      returned: always
      type: str
      sample: ansible-test.cvlrtwiennww.us-east-1.rds.amazonaws.com
    hosted_zone_id:
      description: The ID that Amazon Route 53 assigns when you create a hosted zone.
      returned: always
      type: str
      sample: ZTR2ITUGPA61AM
    port:
      description: The port that the database engine is listening on.
      returned: always
      type: int
      sample: 3306
engine:
  description: The database engine version.
  returned: always
  type: str
  sample: mariadb
engine_version:
  description: The database engine version.
  returned: always
  type: str
  sample: 10.0.35
iam_database_authentication_enabled:
  description: Whether mapping of AWS Identity and Access Management (IAM) accounts to database accounts is enabled.
  returned: always
  type: bool
  sample: false
instance_create_time:
  description: The date and time the DB instance was created.
  returned: always
  type: str
  sample: '2018-07-04T16:48:35.332000+00:00'
kms_key_id:
  description: The AWS KMS key identifier for the encrypted DB instance when storage_encrypted is true.
  returned: When storage_encrypted is true
  type: str
  sample: arn:aws:kms:us-east-1:123456789012:key/70c45553-ad2e-4a85-9f14-cfeb47555c33
latest_restorable_time:
  description: The latest time to which a database can be restored with point-in-time restore.
  returned: always
  type: str
  sample: '2018-07-04T16:50:50.642000+00:00'
license_model:
  description: The License model information for this DB instance.
  returned: always
  type: str
  sample: general-public-license
master_username:
  description: The master username for the DB instance.
  returned: always
  type: str
  sample: test
max_allocated_storage:
  description: The upper limit to which Amazon RDS can automatically scale the storage of the DB instance.
  returned: When max allocated storage is present.
  type: int
  sample: 100
monitoring_interval:
  description:
    - The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance.
      0 means collecting Enhanced Monitoring metrics is disabled.
  returned: always
  type: int
  sample: 0
multi_az:
  description: Whether the DB instance is a Multi-AZ deployment.
  returned: always
  type: bool
  sample: false
option_group_memberships:
  description: The list of option group memberships for this DB instance.
  returned: always
  type: complex
  contains:
    option_group_name:
      description: The name of the option group that the instance belongs to.
      returned: always
      type: str
      sample: default:mariadb-10-0
    status:
      description: The status of the DB instance's option group membership.
      returned: always
      type: str
      sample: in-sync
pending_modified_values:
  description: The changes to the DB instance that are pending.
  returned: always
  type: complex
  contains: {}
performance_insights_enabled:
  description: true if Performance Insights is enabled for the DB instance, and otherwise false.
  returned: always
  type: bool
  sample: false
preferred_backup_window:
  description: The daily time range during which automated backups are created if automated backups are enabled.
  returned: always
  type: str
  sample: 07:01-07:31
preferred_maintenance_window:
  description: The weekly time range (in UTC) during which system maintenance can occur.
  returned: always
  type: str
  sample: sun:09:31-sun:10:01
publicly_accessible:
  description:
    - C(True) for an Internet-facing instance with a publicly resolvable DNS name, C(False) to indicate an
      internal instance with a DNS name that resolves to a private IP address.
  returned: always
  type: bool
  sample: true
read_replica_db_instance_identifiers:
  description: Identifiers of the Read Replicas associated with this DB instance.
  returned: always
  type: list
  sample: []
storage_encrypted:
  description: Whether the DB instance is encrypted.
  returned: always
  type: bool
  sample: false
storage_type:
  description: The storage type to be associated with the DB instance.
  returned: always
  type: str
  sample: standard
tags:
  description: A dictionary of tags associated with the DB instance.
  returned: always
  type: complex
  contains: {}
vpc_security_groups:
  description: A list of VPC security group elements that the DB instance belongs to.
  returned: always
  type: complex
  contains:
    status:
      description: The status of the VPC security group.
      returned: always
      type: str
      sample: active
    vpc_security_group_id:
      description: The name of the VPC security group.
      returned: always
      type: str
      sample: sg-12345678
"""

from time import sleep

try:
    import botocore
except ImportError:
    pass  # caught by AnsibleAWSModule


from ansible.module_utils._text import to_text
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
from ansible.module_utils.six import string_types

from ansible_collections.amazon.aws.plugins.module_utils.botocore import get_boto3_client_method_parameters
from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_message
from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.rds import arg_spec_to_rds_params
from ansible_collections.amazon.aws.plugins.module_utils.rds import call_method
from ansible_collections.amazon.aws.plugins.module_utils.rds import compare_iam_roles
from ansible_collections.amazon.aws.plugins.module_utils.rds import ensure_tags
from ansible_collections.amazon.aws.plugins.module_utils.rds import get_final_identifier
from ansible_collections.amazon.aws.plugins.module_utils.rds import get_rds_method_attribute
from ansible_collections.amazon.aws.plugins.module_utils.rds import get_tags
from ansible_collections.amazon.aws.plugins.module_utils.rds import update_iam_roles
from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict

valid_engines = [
    "aurora",
    "aurora-mysql",
    "aurora-postgresql",
    "mariadb",
    "mysql",
    "oracle-ee",
    "oracle-ee-cdb",
    "oracle-se2",
    "oracle-se2-cdb",
    "postgres",
    "sqlserver-ee",
    "sqlserver-se",
    "sqlserver-ex",
    "sqlserver-web",
]

valid_engines_iam_roles = [
    "aurora-postgresql",
    "oracle-ee",
    "oracle-ee-cdb",
    "oracle-se2",
    "oracle-se2-cdb",
    "postgres",
    "sqlserver-ee",
    "sqlserver-se",
    "sqlserver-ex",
    "sqlserver-web",
]


def get_rds_method_attribute_name(instance, state, creation_source, read_replica):
    method_name = None
    if state == "absent" or state == "terminated":
        if instance and instance["DBInstanceStatus"] not in ["deleting", "deleted"]:
            method_name = "delete_db_instance"
    else:
        if instance:
            method_name = "modify_db_instance"
        elif read_replica is True:
            method_name = "create_db_instance_read_replica"
        elif creation_source == "snapshot":
            method_name = "restore_db_instance_from_db_snapshot"
        elif creation_source == "s3":
            method_name = "restore_db_instance_from_s3"
        elif creation_source == "instance":
            method_name = "restore_db_instance_to_point_in_time"
        else:
            method_name = "create_db_instance"
    return method_name


def get_instance(client, module, db_instance_id):
    try:
        for _i in range(3):
            try:
                instance = client.describe_db_instances(DBInstanceIdentifier=db_instance_id)["DBInstances"][0]
                instance["Tags"] = get_tags(client, module, instance["DBInstanceArn"])
                if instance.get("ProcessorFeatures"):
                    instance["ProcessorFeatures"] = dict(
                        (feature["Name"], feature["Value"]) for feature in instance["ProcessorFeatures"]
                    )
                if instance.get("PendingModifiedValues", {}).get("ProcessorFeatures"):
                    instance["PendingModifiedValues"]["ProcessorFeatures"] = dict(
                        (feature["Name"], feature["Value"])
                        for feature in instance["PendingModifiedValues"]["ProcessorFeatures"]
                    )
                break
            except is_boto3_error_code("DBInstanceNotFound"):
                sleep(3)
        else:
            instance = {}
    except (
        botocore.exceptions.ClientError,
        botocore.exceptions.BotoCoreError,
    ) as e:  # pylint: disable=duplicate-except
        module.fail_json_aws(e, msg="Failed to describe DB instances")
    return instance


def get_final_snapshot(client, module, snapshot_identifier):
    try:
        snapshots = AWSRetry.jittered_backoff()(client.describe_db_snapshots)(DBSnapshotIdentifier=snapshot_identifier)
        if len(snapshots.get("DBSnapshots", [])) == 1:
            return snapshots["DBSnapshots"][0]
        return {}
    except is_boto3_error_code("DBSnapshotNotFound"):  # May not be using wait: True
        return {}
    except (
        botocore.exceptions.ClientError,
        botocore.exceptions.BotoCoreError,
    ) as e:  # pylint: disable=duplicate-except
        module.fail_json_aws(e, msg="Failed to retrieve information about the final snapshot")


def get_parameters(client, module, parameters, method_name):
    if method_name == "restore_db_instance_to_point_in_time":
        parameters["TargetDBInstanceIdentifier"] = module.params["db_instance_identifier"]

    required_options = get_boto3_client_method_parameters(client, method_name, required=True)
    if any(parameters.get(k) is None for k in required_options):
        description = get_rds_method_attribute(method_name, module).operation_description
        module.fail_json(msg=f"To {description} requires the parameters: {required_options}")
    options = get_boto3_client_method_parameters(client, method_name)
    parameters = dict((k, v) for k, v in parameters.items() if k in options and v is not None)

    if parameters.get("ProcessorFeatures") is not None:
        parameters["ProcessorFeatures"] = [
            {"Name": k, "Value": to_text(v)} for k, v in parameters["ProcessorFeatures"].items()
        ]

    # If this parameter is an empty list it can only be used with modify_db_instance (as the parameter UseDefaultProcessorFeatures)
    if parameters.get("ProcessorFeatures") == [] and not method_name == "modify_db_instance":
        parameters.pop("ProcessorFeatures")

    if method_name in ["create_db_instance", "create_db_instance_read_replica", "restore_db_instance_from_db_snapshot"]:
        if parameters.get("Tags"):
            parameters["Tags"] = ansible_dict_to_boto3_tag_list(parameters["Tags"])

    if method_name == "modify_db_instance":
        parameters = get_options_with_changing_values(client, module, parameters)

    return parameters


def get_options_with_changing_values(client, module, parameters):
    instance_id = module.params["db_instance_identifier"]
    purge_cloudwatch_logs = module.params["purge_cloudwatch_logs_exports"]
    force_update_password = module.params["force_update_password"]
    port = module.params["port"]
    apply_immediately = parameters.pop("ApplyImmediately", None)
    cloudwatch_logs_enabled = module.params["enable_cloudwatch_logs_exports"]
    purge_security_groups = module.params["purge_security_groups"]
    ca_certificate_identifier = module.params["ca_certificate_identifier"]

    if ca_certificate_identifier:
        parameters["CACertificateIdentifier"] = ca_certificate_identifier
    if port:
        parameters["DBPortNumber"] = port
    if not force_update_password:
        parameters.pop("MasterUserPassword", None)
    if cloudwatch_logs_enabled is not None:
        parameters["CloudwatchLogsExportConfiguration"] = cloudwatch_logs_enabled
    if not module.params["storage_type"]:
        parameters.pop("Iops", None)

    instance = get_instance(client, module, instance_id)
    updated_parameters = get_changing_options_with_inconsistent_keys(
        parameters, instance, purge_cloudwatch_logs, purge_security_groups
    )
    updated_parameters.update(get_changing_options_with_consistent_keys(parameters, instance))
    parameters = updated_parameters

    if instance.get("StorageType") == "io1":
        # Bundle Iops and AllocatedStorage while updating io1 RDS Instance
        current_iops = instance.get("PendingModifiedValues", {}).get("Iops", instance["Iops"])
        current_allocated_storage = instance.get("PendingModifiedValues", {}).get(
            "AllocatedStorage", instance["AllocatedStorage"]
        )
        new_iops = module.params.get("iops")
        new_allocated_storage = module.params.get("allocated_storage")

        if current_iops != new_iops or current_allocated_storage != new_allocated_storage:
            parameters["AllocatedStorage"] = new_allocated_storage
            parameters["Iops"] = new_iops

    if instance.get("StorageType") == "gp3":
        GP3_THROUGHPUT = True
        current_storage_throughput = instance.get("PendingModifiedValues", {}).get(
            "StorageThroughput", instance["StorageThroughput"]
        )
        new_storage_throughput = module.params.get("storage_throughput") or current_storage_throughput
        if new_storage_throughput != current_storage_throughput:
            parameters["StorageThroughput"] = new_storage_throughput

        current_iops = instance.get("PendingModifiedValues", {}).get("Iops", instance["Iops"])
        # when you just change from gp2 to gp3, you may not add the iops parameter
        new_iops = module.params.get("iops") or current_iops

        new_allocated_storage = module.params.get("allocated_storage")
        current_allocated_storage = instance.get("PendingModifiedValues", {}).get(
            "AllocatedStorage", instance["AllocatedStorage"]
        )

        if new_allocated_storage:
            if current_allocated_storage != new_allocated_storage:
                parameters["AllocatedStorage"] = new_allocated_storage

            if new_allocated_storage >= 400:
                if new_iops < 12000:
                    module.fail_json(
                        msg="IOPS must be at least 12000 when the allocated storage is larger than or equal to 400 GB."
                    )

                if new_storage_throughput < 500 and GP3_THROUGHPUT:
                    module.fail_json(
                        msg=(
                            "Storage Throughput must be at least 500 when the allocated storage is larger than or equal"
                            " to 400 GB."
                        )
                    )

                if current_iops != new_iops:
                    parameters["Iops"] = new_iops
                    # must be always specified when changing iops
                    parameters["AllocatedStorage"] = new_allocated_storage

    if parameters.get("NewDBInstanceIdentifier") and instance.get("PendingModifiedValues", {}).get(
        "DBInstanceIdentifier"
    ):
        if (
            parameters["NewDBInstanceIdentifier"] == instance["PendingModifiedValues"]["DBInstanceIdentifier"]
            and not apply_immediately
        ):
            parameters.pop("NewDBInstanceIdentifier")

    if parameters:
        parameters["DBInstanceIdentifier"] = instance_id
        if apply_immediately is not None:
            parameters["ApplyImmediately"] = apply_immediately

    return parameters


def get_current_attributes_with_inconsistent_keys(instance):
    options = {}
    if instance.get("PendingModifiedValues", {}).get("PendingCloudwatchLogsExports", {}).get("LogTypesToEnable", []):
        current_enabled = instance["PendingModifiedValues"]["PendingCloudwatchLogsExports"]["LogTypesToEnable"]
        current_disabled = instance["PendingModifiedValues"]["PendingCloudwatchLogsExports"]["LogTypesToDisable"]
        options["CloudwatchLogsExportConfiguration"] = {
            "LogTypesToEnable": current_enabled,
            "LogTypesToDisable": current_disabled,
        }
    else:
        options["CloudwatchLogsExportConfiguration"] = {
            "LogTypesToEnable": instance.get("EnabledCloudwatchLogsExports", []),
            "LogTypesToDisable": [],
        }
    if instance.get("PendingModifiedValues", {}).get("Port"):
        options["DBPortNumber"] = instance["PendingModifiedValues"]["Port"]
    else:
        options["DBPortNumber"] = instance["Endpoint"]["Port"]
    if instance.get("PendingModifiedValues", {}).get("DBSubnetGroupName"):
        options["DBSubnetGroupName"] = instance["PendingModifiedValues"]["DBSubnetGroupName"]
    else:
        options["DBSubnetGroupName"] = instance["DBSubnetGroup"]["DBSubnetGroupName"]
    if instance.get("PendingModifiedValues", {}).get("ProcessorFeatures"):
        options["ProcessorFeatures"] = instance["PendingModifiedValues"]["ProcessorFeatures"]
    else:
        options["ProcessorFeatures"] = instance.get("ProcessorFeatures", {})
    options["OptionGroupName"] = [g["OptionGroupName"] for g in instance["OptionGroupMemberships"]]
    options["DBSecurityGroups"] = [
        sg["DBSecurityGroupName"] for sg in instance["DBSecurityGroups"] if sg["Status"] in ["adding", "active"]
    ]
    options["VpcSecurityGroupIds"] = [
        sg["VpcSecurityGroupId"] for sg in instance["VpcSecurityGroups"] if sg["Status"] in ["adding", "active"]
    ]
    options["DBParameterGroupName"] = [
        parameter_group["DBParameterGroupName"] for parameter_group in instance["DBParameterGroups"]
    ]
    options["EnableIAMDatabaseAuthentication"] = instance["IAMDatabaseAuthenticationEnabled"]
    # PerformanceInsightsEnabled is not returned on older RDS instances it seems
    options["EnablePerformanceInsights"] = instance.get("PerformanceInsightsEnabled", False)
    options["NewDBInstanceIdentifier"] = instance["DBInstanceIdentifier"]

    # Neither of these are returned via describe_db_instances, so if either is specified during a check_mode run, changed=True
    options["AllowMajorVersionUpgrade"] = None
    options["MasterUserPassword"] = None

    return options


def get_changing_options_with_inconsistent_keys(modify_params, instance, purge_cloudwatch_logs, purge_security_groups):
    changing_params = {}
    current_options = get_current_attributes_with_inconsistent_keys(instance)
    for option, current_option in current_options.items():
        desired_option = modify_params.pop(option, None)
        if desired_option is None:
            continue

        # TODO: allow other purge_option module parameters rather than just checking for things to add
        if isinstance(current_option, list):
            if isinstance(desired_option, list):
                if (
                    set(desired_option) < set(current_option)
                    and option in ["DBSecurityGroups", "VpcSecurityGroupIds"]
                    and purge_security_groups
                ):
                    changing_params[option] = desired_option
                elif set(desired_option) <= set(current_option):
                    continue
            elif isinstance(desired_option, string_types):
                if desired_option in current_option:
                    continue

        # Current option and desired option are the same - continue loop
        if option != "ProcessorFeatures" and current_option == desired_option:
            continue

        if option == "ProcessorFeatures" and current_option == boto3_tag_list_to_ansible_dict(
            desired_option, "Name", "Value"
        ):
            continue

        # Current option and desired option are different - add to changing_params list
        if option == "ProcessorFeatures" and desired_option == []:
            changing_params["UseDefaultProcessorFeatures"] = True
        elif option == "CloudwatchLogsExportConfiguration":
            current_option = set(current_option.get("LogTypesToEnable", []))
            desired_option = set(desired_option)
            format_option = {"EnableLogTypes": [], "DisableLogTypes": []}
            format_option["EnableLogTypes"] = list(desired_option.difference(current_option))
            if purge_cloudwatch_logs:
                format_option["DisableLogTypes"] = list(current_option.difference(desired_option))
            if format_option["EnableLogTypes"] or format_option["DisableLogTypes"]:
                changing_params[option] = format_option
        elif option in ["DBSecurityGroups", "VpcSecurityGroupIds"]:
            if purge_security_groups:
                changing_params[option] = desired_option
            else:
                changing_params[option] = list(set(current_option) | set(desired_option))
        else:
            changing_params[option] = desired_option

    return changing_params


def get_changing_options_with_consistent_keys(modify_params, instance):
    changing_params = {}

    for param in modify_params:
        current_option = instance.get("PendingModifiedValues", {}).get(param, None)
        if current_option is None:
            current_option = instance.get(param, None)
        if modify_params[param] != current_option:
            changing_params[param] = modify_params[param]

    return changing_params


def validate_options(client, module, instance):
    state = module.params["state"]
    skip_final_snapshot = module.params["skip_final_snapshot"]
    snapshot_id = module.params["final_db_snapshot_identifier"]
    modified_id = module.params["new_db_instance_identifier"]
    engine = module.params["engine"]
    tde_options = bool(module.params["tde_credential_password"] or module.params["tde_credential_arn"])
    read_replica = module.params["read_replica"]
    creation_source = module.params["creation_source"]
    source_instance = module.params["source_db_instance_identifier"]

    if modified_id:
        modified_instance = get_instance(client, module, modified_id)
    else:
        modified_instance = {}

    if modified_id and instance and modified_instance:
        module.fail_json(msg=f"A new instance ID {modified_id} was provided but it already exists")
    if modified_id and not instance and modified_instance:
        module.fail_json(
            msg=f"A new instance ID {modified_id} was provided but the instance to be renamed does not exist"
        )
    if state in ("absent", "terminated") and instance and not skip_final_snapshot and snapshot_id is None:
        module.fail_json(
            msg="skip_final_snapshot is false but all of the following are missing: final_db_snapshot_identifier"
        )
    if engine is not None and not (engine.startswith("mysql") or engine.startswith("oracle")) and tde_options:
        module.fail_json(msg="TDE is available for MySQL and Oracle DB instances")
    if read_replica is True and not instance and creation_source not in [None, "instance"]:
        module.fail_json(msg=f"Cannot create a read replica from {creation_source}. You must use a source DB instance")
    if read_replica is True and not instance and not source_instance:
        module.fail_json(
            msg=(
                "read_replica is true and the instance does not exist yet but all of the following are missing:"
                " source_db_instance_identifier"
            )
        )


def update_instance(client, module, instance, instance_id):
    changed = False

    # Get newly created DB instance
    if not instance:
        instance = get_instance(client, module, instance_id)

    # Check tagging/promoting/rebooting/starting/stopping instance
    changed |= ensure_tags(
        client, module, instance["DBInstanceArn"], instance["Tags"], module.params["tags"], module.params["purge_tags"]
    )
    changed |= promote_replication_instance(client, module, instance, module.params["read_replica"])
    changed |= update_instance_state(client, module, instance, module.params["state"])

    return changed


def promote_replication_instance(client, module, instance, read_replica):
    changed = False
    if read_replica is False:
        # 'StatusInfos' only exists when the instance is a read replica
        # See https://awscli.amazonaws.com/v2/documentation/api/latest/reference/rds/describe-db-instances.html
        if bool(instance.get("StatusInfos")):
            try:
                _result, changed = call_method(
                    client,
                    module,
                    method_name="promote_read_replica",
                    parameters={"DBInstanceIdentifier": instance["DBInstanceIdentifier"]},
                )
            except is_boto3_error_message("DB Instance is not a read replica"):
                pass
    return changed


def ensure_iam_roles(client, module, instance_id):
    """
    Ensure specified IAM roles are associated with DB instance

        Parameters:
            client: RDS client
            module: AWSModule
            instance_id: DB's instance ID

        Returns:
            changed (bool): True if changes were successfully made to DB instance's IAM roles; False if not
    """
    instance = camel_dict_to_snake_dict(
        get_instance(client, module, instance_id), ignore_list=["Tags", "ProcessorFeatures"]
    )

    # Ensure engine type supports associating IAM roles
    engine = instance.get("engine")
    if engine not in valid_engines_iam_roles:
        module.fail_json(
            msg=f"DB engine {engine} is not valid for adding IAM roles. Valid engines are {valid_engines_iam_roles}"
        )

    changed = False
    purge_iam_roles = module.params.get("purge_iam_roles")
    target_roles = module.params.get("iam_roles") if module.params.get("iam_roles") else []
    existing_roles = instance.get("associated_roles", [])
    roles_to_add, roles_to_remove = compare_iam_roles(existing_roles, target_roles, purge_iam_roles)
    if bool(roles_to_add or roles_to_remove):
        changed = True
        # Don't update on check_mode
        if module.check_mode:
            module.exit_json(changed=changed, **instance)
        else:
            update_iam_roles(client, module, instance_id, roles_to_add, roles_to_remove)
    return changed


def update_instance_state(client, module, instance, state):
    changed = False
    if state in ["rebooted", "restarted"]:
        changed |= reboot_running_db_instance(client, module, instance)
    if state in ["started", "running", "stopped"]:
        changed |= start_or_stop_instance(client, module, instance, state)
    return changed


def reboot_running_db_instance(client, module, instance):
    parameters = {"DBInstanceIdentifier": instance["DBInstanceIdentifier"]}
    if instance["DBInstanceStatus"] in ["stopped", "stopping"]:
        call_method(client, module, "start_db_instance", parameters)
    if module.params.get("force_failover") is not None:
        parameters["ForceFailover"] = module.params["force_failover"]
    _results, changed = call_method(client, module, "reboot_db_instance", parameters)
    return changed


def start_or_stop_instance(client, module, instance, state):
    changed = False
    parameters = {"DBInstanceIdentifier": instance["DBInstanceIdentifier"]}
    if state == "stopped" and instance["DBInstanceStatus"] not in ["stopping", "stopped"]:
        if module.params["db_snapshot_identifier"]:
            parameters["DBSnapshotIdentifier"] = module.params["db_snapshot_identifier"]
        _result, changed = call_method(client, module, "stop_db_instance", parameters)
    elif state == "started" and instance["DBInstanceStatus"] not in ["available", "starting", "restarting"]:
        _result, changed = call_method(client, module, "start_db_instance", parameters)
    return changed


def main():
    arg_spec = dict(
        state=dict(
            choices=["present", "absent", "terminated", "running", "started", "stopped", "rebooted", "restarted"],
            default="present",
        ),
        creation_source=dict(choices=["snapshot", "s3", "instance"]),
        force_update_password=dict(type="bool", default=False, no_log=False),
        purge_cloudwatch_logs_exports=dict(type="bool", default=True),
        purge_iam_roles=dict(type="bool", default=False),
        purge_tags=dict(type="bool", default=True),
        read_replica=dict(type="bool"),
        wait=dict(type="bool", default=True),
        purge_security_groups=dict(type="bool", default=True),
    )

    parameter_options = dict(
        allocated_storage=dict(type="int"),
        allow_major_version_upgrade=dict(type="bool"),
        apply_immediately=dict(type="bool", default=False),
        auto_minor_version_upgrade=dict(type="bool"),
        availability_zone=dict(aliases=["az", "zone"]),
        backup_retention_period=dict(type="int"),
        ca_certificate_identifier=dict(type="str"),
        character_set_name=dict(),
        copy_tags_to_snapshot=dict(type="bool"),
        db_cluster_identifier=dict(aliases=["cluster_id"]),
        db_instance_class=dict(aliases=["class", "instance_type"]),
        db_instance_identifier=dict(required=True, aliases=["instance_id", "id"]),
        db_name=dict(),
        db_parameter_group_name=dict(),
        db_security_groups=dict(type="list", elements="str"),
        db_snapshot_identifier=dict(type="str", aliases=["snapshot_identifier", "snapshot_id"]),
        db_subnet_group_name=dict(aliases=["subnet_group"]),
        deletion_protection=dict(type="bool"),
        domain=dict(),
        domain_iam_role_name=dict(),
        enable_cloudwatch_logs_exports=dict(type="list", aliases=["cloudwatch_log_exports"], elements="str"),
        enable_iam_database_authentication=dict(type="bool"),
        enable_performance_insights=dict(type="bool"),
        engine=dict(type="str", choices=valid_engines),
        engine_version=dict(),
        final_db_snapshot_identifier=dict(aliases=["final_snapshot_identifier"]),
        force_failover=dict(type="bool"),
        iam_roles=dict(type="list", elements="dict"),
        iops=dict(type="int"),
        kms_key_id=dict(),
        license_model=dict(),
        master_user_password=dict(aliases=["password"], no_log=True),
        master_username=dict(aliases=["username"]),
        max_allocated_storage=dict(type="int"),
        monitoring_interval=dict(type="int"),
        monitoring_role_arn=dict(),
        multi_az=dict(type="bool"),
        new_db_instance_identifier=dict(aliases=["new_instance_id", "new_id"]),
        option_group_name=dict(),
        performance_insights_kms_key_id=dict(),
        performance_insights_retention_period=dict(type="int"),
        port=dict(type="int"),
        preferred_backup_window=dict(aliases=["backup_window"]),
        preferred_maintenance_window=dict(aliases=["maintenance_window"]),
        processor_features=dict(type="dict"),
        promotion_tier=dict(type="int"),
        publicly_accessible=dict(type="bool"),
        restore_time=dict(),
        s3_bucket_name=dict(),
        s3_ingestion_role_arn=dict(),
        s3_prefix=dict(),
        skip_final_snapshot=dict(type="bool", default=False),
        source_db_instance_identifier=dict(),
        source_engine=dict(choices=["mysql"]),
        source_engine_version=dict(),
        source_region=dict(),
        storage_encrypted=dict(type="bool"),
        storage_type=dict(choices=["standard", "gp2", "gp3", "io1"]),
        storage_throughput=dict(type="int"),
        tags=dict(type="dict", aliases=["resource_tags"]),
        tde_credential_arn=dict(aliases=["transparent_data_encryption_arn"]),
        tde_credential_password=dict(no_log=True, aliases=["transparent_data_encryption_password"]),
        timezone=dict(),
        use_latest_restorable_time=dict(type="bool", aliases=["restore_from_latest"]),
        vpc_security_group_ids=dict(type="list", elements="str"),
    )
    arg_spec.update(parameter_options)

    required_if_s3_creation_source = [
        "s3_bucket_name",
        "engine",
        "master_username",
        "master_user_password",
        "source_engine",
        "source_engine_version",
        "s3_ingestion_role_arn",
    ]

    required_if = [
        ["engine", "aurora", ["db_cluster_identifier"]],
        ["engine", "aurora-mysql", ["db_cluster_identifier"]],
        ["engine", "aurora-postresql", ["db_cluster_identifier"]],
        ["storage_type", "io1", ["iops", "allocated_storage"]],
        ["creation_source", "snapshot", ["db_snapshot_identifier", "engine"]],
        ["creation_source", "s3", required_if_s3_creation_source],
    ]
    mutually_exclusive = [
        ["s3_bucket_name", "source_db_instance_identifier", "db_snapshot_identifier"],
        ["use_latest_restorable_time", "restore_time"],
        ["availability_zone", "multi_az"],
    ]

    module = AnsibleAWSModule(
        argument_spec=arg_spec,
        required_if=required_if,
        mutually_exclusive=mutually_exclusive,
        supports_check_mode=True,
    )

    if module.params["ca_certificate_identifier"]:
        module.require_botocore_at_least(
            "1.29.44", reason="to use 'ca_certificate_identifier' while creating/updating rds instance"
        )

    # Sanitize instance identifiers
    module.params["db_instance_identifier"] = module.params["db_instance_identifier"].lower()
    if module.params["new_db_instance_identifier"]:
        module.params["new_db_instance_identifier"] = module.params["new_db_instance_identifier"].lower()

    # Sanitize processor features
    if module.params["processor_features"] is not None:
        module.params["processor_features"] = dict(
            (k, to_text(v)) for k, v in module.params["processor_features"].items()
        )

    # Ensure dates are in lowercase
    if module.params["preferred_maintenance_window"]:
        module.params["preferred_maintenance_window"] = module.params["preferred_maintenance_window"].lower()

    # Throw warning regarding case when allow_major_version_upgrade is specified in check_mode
    # describe_rds_instance never returns this value, so on check_mode, it will always return changed=True
    # In non-check mode runs, changed will return the correct value, so no need to warn there.
    # see: amazon.aws.module_util.rds.handle_errors.
    if module.params.get("allow_major_version_upgrade") and module.check_mode:
        module.warn(
            "allow_major_version_upgrade is not returned when describing db instances, so changed will always be `True`"
            " on check mode runs."
        )

    client = module.client("rds")
    changed = False
    state = module.params["state"]
    instance_id = module.params["db_instance_identifier"]
    instance = get_instance(client, module, instance_id)
    validate_options(client, module, instance)
    method_name = get_rds_method_attribute_name(
        instance, state, module.params["creation_source"], module.params["read_replica"]
    )

    if method_name:
        # Exit on create/delete if check_mode
        if module.check_mode and method_name in ["create_db_instance", "delete_db_instance"]:
            module.exit_json(
                changed=True, **camel_dict_to_snake_dict(instance, ignore_list=["Tags", "ProcessorFeatures"])
            )

        raw_parameters = arg_spec_to_rds_params(
            dict((k, module.params[k]) for k in module.params if k in parameter_options)
        )
        parameters_to_modify = get_parameters(client, module, raw_parameters, method_name)

        if parameters_to_modify:
            # Exit on check_mode when parameters to modify
            if module.check_mode:
                module.exit_json(
                    changed=True, **camel_dict_to_snake_dict(instance, ignore_list=["Tags", "ProcessorFeatures"])
                )
            _result, changed = call_method(client, module, method_name, parameters_to_modify)

        instance_id = get_final_identifier(method_name, module)

        if state != "absent":
            # Check tagging/promoting/rebooting/starting/stopping instance
            if not module.check_mode or instance:
                changed |= update_instance(client, module, instance, instance_id)

            # Check IAM roles
            if module.params.get("iam_roles") or module.params.get("purge_iam_roles"):
                changed |= ensure_iam_roles(client, module, instance_id)

        if changed:
            instance = get_instance(client, module, instance_id)
            if state != "absent" and (instance or not module.check_mode):
                for _wait_attempt in range(0, 10):
                    instance = get_instance(client, module, instance_id)
                    if instance:
                        break
                    sleep(5)

        if state == "absent" and changed and not module.params["skip_final_snapshot"]:
            instance.update(
                FinalSnapshot=get_final_snapshot(client, module, module.params["final_db_snapshot_identifier"])
            )

    pending_processor_features = None
    if instance.get("PendingModifiedValues", {}).get("ProcessorFeatures"):
        pending_processor_features = instance["PendingModifiedValues"].pop("ProcessorFeatures")
    instance = camel_dict_to_snake_dict(instance, ignore_list=["Tags", "ProcessorFeatures"])
    if pending_processor_features is not None:
        instance["pending_modified_values"]["processor_features"] = pending_processor_features

    module.exit_json(changed=changed, **instance)


if __name__ == "__main__":
    main()