AWSTemplateFormatVersion: 2010-09-09 Description: >- This template creates a two-node Cisco ISE deployment in a two-AZ, two-subnet VPC infrastructure with AWS NLB, health monitoring, and optionally automated PAN failover. (qs-1tfq7mg9i) Metadata: LintSpellExclude: - openapi - pxGrid - Cisco - compute-1 - com - pem - amazonaws - myhostname - mykeypair - hostname - failover - console - plaintext - Management - Cloud cfn-lint: config: ignore_checks: - W9001 # Changing parameter name requires a code change. - W9002 - W9003 - W1020 Parameters: KeyPairName: Description: >- To access the Cisco ISE instance via SSH, choose the key pair that you created/imported in AWS. Create/import a key pair in AWS now if you have not configured one already. Usage example: ssh -i mykeypair.pem admin@myhostname.compute-1.amazonaws.com. NOTE: The username for ISE 3.1 is "admin" and for ISE 3.2+ is "iseadmin". Type: 'AWS::EC2::KeyPair::KeyName' AllowedPattern: .+ ConstraintDescription: Instance Key Pair cannot be empty ISEInstanceType: Type: String Description: Choose the required Cisco ISE instance type. Default: c5.4xlarge AllowedValues: - c5.4xlarge - m5.4xlarge - c5.9xlarge - t3.xlarge ConstraintDescription: Instance type should be one of the allowed values ISEVersion: Type: String Description: The ISE software version to be used for the ISE instances. AllowedValues: - '3.1' - '3.2' Default: '3.1' EBSEncrypt: Description: Choose true to enable EBS encryption. Type: String Default: 'false' AllowedValues: - 'false' - 'true' ConstraintDescription: It can either be true or false ERSapi: Description: Do you wish to enable ERS? Type: String Default: 'yes' AllowedValues: - 'yes' - 'no' ConstraintDescription: It can either be yes or no OpenAPI: Description: Do you wish to enable OpenAPI? Type: String Default: 'yes' AllowedValues: - 'yes' - 'no' ConstraintDescription: It can either be yes or no PXGrid: Description: Do you wish to enable pxGrid? Type: String Default: 'no' AllowedValues: - 'yes' - 'no' ConstraintDescription: It can either be yes or no PXGridCloud: Description: Do you wish to enable pxGrid Cloud? Type: String Default: 'no' AllowedValues: - 'yes' - 'no' ConstraintDescription: It can either be yes or no AutoFailover: Description: Do you wish to enable auto-failover mechanism? Type: String Default: 'DISABLED' AllowedValues: - 'ENABLED' - 'DISABLED' ConstraintDescription: It can either be ENABLED or DISABLED PrivateSubnet1A: Type: 'AWS::EC2::Subnet::Id' Description: >- ID of the subnet to be used for the ISE deployment in an Availability Zone A. PrivateSubnet1B: Type: 'AWS::EC2::Subnet::Id' Description: >- ID of the subnet to be used for the ISE deployment in an Availability Zone B. FailoverRate: Description: The rate (frequency) that determines when CloudWatch Events runs the rule that triggers the Lambda function which triggers Failover State Machine. Default: rate(60 minutes) AllowedValues: - rate(1 minute) - rate(10 minutes) - rate(60 minutes) Type: String HealthcheckRate: Description: The rate (frequency) that determines when CloudWatch Events runs the rule that triggers the Lambda function which checks health status of ISE deployment. Default: rate(10 minutes) AllowedValues: - rate(1 minute) - rate(10 minutes) - rate(60 minutes) Type: String Node1Hostname: Description: >- Enter the hostname. This field only supports alphanumeric characters and hyphen (-). The length of the hostname should not exceed 19 characters. Type: String Default: iseserver1 AllowedPattern: '^[a-zA-Z0-9-]{1,19}$' ConstraintDescription: >- This field only supports alphanumeric characters and hyphen (-). Hostname should not be more than 19 characters. Node2Hostname: Description: >- Enter the hostname. This field only supports alphanumeric characters and hyphen (-). The length of the hostname should not exceed 19 characters. Type: String Default: iseserver2 AllowedPattern: '^[a-zA-Z0-9-]{1,19}$' ConstraintDescription: >- This field only supports alphanumeric characters and hyphen (-). Hostname should not be more than 19 characters. #User defined R53 Private Hosted zone domain name DNSDomain: Description: >- Enter a domain name in correct syntax (for example, cisco.com). The valid characters for this field are ASCII characters, numerals, hyphen (-), and period (.). If you use the wrong syntax, Cisco ISE services might not come up on launch. Type: String Default: example.com AllowedPattern: '^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$' ConstraintDescription: >- Cannot be an IP address. Valid characters include ASCII characters, any numerals, the hyphen (-), and the period (.) for DNS domain. TimeZone: Description: Choose a system time zone. Type: String Default: Etc/UTC AllowedValues: - Africa/Abidjan - Africa/Accra - Africa/Algiers - Africa/Bissau - Africa/Cairo - Africa/Casablanca - Africa/Ceuta - Africa/El_Aaiun - Africa/Johannesburg - Africa/Juba - Africa/Khartoum - Africa/Lagos - Africa/Maputo - Africa/Monrovia - Africa/Nairobi - Africa/Ndjamena - Africa/Sao_Tome - Africa/Tripoli - Africa/Tunis - Africa/Windhoek - America/Adak - America/Anchorage - America/Araguaina - America/Argentina/Buenos_Aires - America/Argentina/Catamarca - America/Argentina/Cordoba - America/Argentina/Jujuy - America/Argentina/La_Rioja - America/Argentina/Mendoza - America/Argentina/Rio_Gallegos - America/Argentina/Salta - America/Argentina/San_Juan - America/Argentina/San_Luis - America/Argentina/Tucuman - America/Argentina/Ushuaia - America/Asuncion - America/Atikokan - America/Bahia - America/Bahia_Banderas - America/Barbados - America/Belem - America/Belize - America/Blanc-Sablon - America/Boa_Vista - America/Bogota - America/Boise - America/Cambridge_Bay - America/Campo_Grande - America/Cancun - America/Caracas - America/Cayenne - America/Chicago - America/Chihuahua - America/Costa_Rica - America/Creston - America/Cuiaba - America/Curacao - America/Danmarkshavn - America/Dawson - America/Dawson_Creek - America/Denver - America/Detroit - America/Edmonton - America/Eirunepe - America/El_Salvador - America/Fort_Nelson - America/Fortaleza - America/Glace_Bay - America/Goose_Bay - America/Grand_Turk - America/Guatemala - America/Guayaquil - America/Guyana - America/Halifax - America/Havana - America/Hermosillo - America/Indiana/Indianapolis - America/Indiana/Knox - America/Indiana/Marengo - America/Indiana/Petersburg - America/Indiana/Tell_City - America/Indiana/Vevay - America/Indiana/Vincennes - America/Indiana/Winamac - America/Inuvik - America/Iqaluit - America/Jamaica - America/Juneau - America/Kentucky/Louisville - America/Kentucky/Monticello - America/La_Paz - America/Lima - America/Los_Angeles - America/Maceio - America/Managua - America/Manaus - America/Martinique - America/Matamoros - America/Mazatlan - America/Menominee - America/Merida - America/Metlakatla - America/Mexico_City - America/Miquelon - America/Moncton - America/Monterrey - America/Montevideo - America/Nassau - America/New_York - America/Nipigon - America/Nome - America/Noronha - America/North_Dakota/Beulah - America/North_Dakota/Center - America/North_Dakota/New_Salem - America/Nuuk - America/Ojinaga - America/Panama - America/Pangnirtung - America/Paramaribo - America/Phoenix - America/Port-au-Prince - America/Port_of_Spain - America/Porto_Velho - America/Puerto_Rico - America/Punta_Arenas - America/Rainy_River - America/Rankin_Inlet - America/Recife - America/Regina - America/Resolute - America/Rio_Branco - America/Santarem - America/Santiago - America/Santo_Domingo - America/Sao_Paulo - America/Scoresbysund - America/Sitka - America/St_Johns - America/Swift_Current - America/Tegucigalpa - America/Thule - America/Thunder_Bay - America/Tijuana - America/Toronto - America/Vancouver - America/Whitehorse - America/Winnipeg - America/Yakutat - America/Yellowknife - Antarctica/Casey - Antarctica/Davis - Antarctica/DumontDUrville - Antarctica/Macquarie - Antarctica/Mawson - Antarctica/Palmer - Antarctica/Rothera - Antarctica/Syowa - Antarctica/Troll - Antarctica/Vostok - Asia/Almaty - Asia/Amman - Asia/Anadyr - Asia/Aqtau - Asia/Aqtobe - Asia/Ashgabat - Asia/Atyrau - Asia/Baghdad - Asia/Baku - Asia/Bangkok - Asia/Barnaul - Asia/Beirut - Asia/Bishkek - Asia/Brunei - Asia/Chita - Asia/Choibalsan - Asia/Colombo - Asia/Damascus - Asia/Dhaka - Asia/Dili - Asia/Dubai - Asia/Dushanbe - Asia/Famagusta - Asia/Gaza - Asia/Hebron - Asia/Ho_Chi_Minh - Asia/Hong_Kong - Asia/Hovd - Asia/Irkutsk - Asia/Jakarta - Asia/Jayapura - Asia/Jerusalem - Asia/Kabul - Asia/Kamchatka - Asia/Karachi - Asia/Kathmandu - Asia/Khandyga - Asia/Kolkata - Asia/Krasnoyarsk - Asia/Kuala_Lumpur - Asia/Kuching - Asia/Macau - Asia/Magadan - Asia/Makassar - Asia/Manila - Asia/Nicosia - Asia/Novokuznetsk - Asia/Novosibirsk - Asia/Omsk - Asia/Oral - Asia/Pontianak - Asia/Pyongyang - Asia/Qatar - Asia/Qostanay - Asia/Qyzylorda - Asia/Riyadh - Asia/Sakhalin - Asia/Samarkand - Asia/Seoul - Asia/Shanghai - Asia/Singapore - Asia/Srednekolymsk - Asia/Taipei - Asia/Tashkent - Asia/Tbilisi - Asia/Tehran - Asia/Thimphu - Asia/Tokyo - Asia/Tomsk - Asia/Ulaanbaatar - Asia/Urumqi - Asia/Ust-Nera - Asia/Vladivostok - Asia/Yakutsk - Asia/Yangon - Asia/Yekaterinburg - Asia/Yerevan - Atlantic/Azores - Atlantic/Bermuda - Atlantic/Canary - Atlantic/Cape_Verde - Atlantic/Faroe - Atlantic/Madeira - Atlantic/Reykjavik - Atlantic/South_Georgia - Atlantic/Stanley - Australia/Adelaide - Australia/Brisbane - Australia/Broken_Hill - Australia/Darwin - Australia/Eucla - Australia/Hobart - Australia/Lindeman - Australia/Lord_Howe - Australia/Melbourne - Australia/Perth - Australia/Sydney - CET - CST6CDT - EET - EST - EST5EDT - Etc/GMT - Etc/GMT+1 - Etc/GMT+10 - Etc/GMT+11 - Etc/GMT+12 - Etc/GMT+2 - Etc/GMT+3 - Etc/GMT+4 - Etc/GMT+5 - Etc/GMT+6 - Etc/GMT+7 - Etc/GMT+8 - Etc/GMT+9 - Etc/GMT-1 - Etc/GMT-10 - Etc/GMT-11 - Etc/GMT-12 - Etc/GMT-13 - Etc/GMT-14 - Etc/GMT-2 - Etc/GMT-3 - Etc/GMT-4 - Etc/GMT-5 - Etc/GMT-6 - Etc/GMT-7 - Etc/GMT-8 - Etc/GMT-9 - Etc/UTC - Europe/Amsterdam - Europe/Andorra - Europe/Astrakhan - Europe/Athens - Europe/Belgrade - Europe/Berlin - Europe/Brussels - Europe/Bucharest - Europe/Budapest - Europe/Chisinau - Europe/Copenhagen - Europe/Dublin - Europe/Gibraltar - Europe/Helsinki - Europe/Istanbul - Europe/Kaliningrad - Europe/Kiev - Europe/Kirov - Europe/Lisbon - Europe/London - Europe/Luxembourg - Europe/Madrid - Europe/Malta - Europe/Minsk - Europe/Monaco - Europe/Moscow - Europe/Oslo - Europe/Paris - Europe/Prague - Europe/Riga - Europe/Rome - Europe/Samara - Europe/Saratov - Europe/Simferopol - Europe/Sofia - Europe/Stockholm - Europe/Tallinn - Europe/Tirane - Europe/Ulyanovsk - Europe/Uzhgorod - Europe/Vienna - Europe/Vilnius - Europe/Volgograd - Europe/Warsaw - Europe/Zaporozhye - Europe/Zurich - HST - Indian/Chagos - Indian/Christmas - Indian/Cocos - Indian/Kerguelen - Indian/Mahe - Indian/Maldives - Indian/Mauritius - Indian/Reunion - MET - MST - MST7MDT - PST8PDT - Pacific/Apia - Pacific/Auckland - Pacific/Bougainville - Pacific/Chatham - Pacific/Chuuk - Pacific/Easter - Pacific/Efate - Pacific/Enderbury - Pacific/Fakaofo - Pacific/Fiji - Pacific/Funafuti - Pacific/Galapagos - Pacific/Gambier - Pacific/Guadalcanal - Pacific/Guam - Pacific/Honolulu - Pacific/Kiritimati - Pacific/Kosrae - Pacific/Kwajalein - Pacific/Majuro - Pacific/Marquesas - Pacific/Nauru - Pacific/Niue - Pacific/Norfolk - Pacific/Noumea - Pacific/Pago_Pago - Pacific/Palau - Pacific/Pitcairn - Pacific/Pohnpei - Pacific/Port_Moresby - Pacific/Rarotonga - Pacific/Tahiti - Pacific/Tarawa - Pacific/Tongatapu - Pacific/Wake - Pacific/Wallis - WET password: Description: >- Enter a password for the default admin user. The password must be aligned with the Cisco ISE password policy (must contain one or more lowercase, uppercase, numeric, and non-alphanumeric characters). The configured password is used for Cisco ISE GUI access. Warning: the password is displayed in plaintext in the user data section of the instance settings window in the AWS Management Console. NOTE: The username for ISE 3.1 is "admin" and for ISE 3.2+ is "iseadmin". NoEcho: 'True' Type: String AllowedPattern: >- ^((?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@~*!,+=_-])(?!.*?[iI]{1}[sS]{1}[eE]{1}[aA]{1}[dD]{1}[mM]{1}[iI]{1}[nN]{1})(?!.*?[nN]{1}[iI]{1}[mM]{1}[dD]{1}[aA]{1}[eE]{1}[sS]{1}[iI]{1})(?!.*?[cC]{1}[iI]{1}[sS]{1}[cC]{1}[oO]{1})(?!.*?[oO]{1}[cC]{1}[sS]{1}[iI]{1}[cC]{1})|(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?!.*?[iI]{1}[sS]{1}[eE]{1}[aA]{1}[dD]{1}[mM]{1}[iI]{1}[nN]{1})(?!.*?[nN]{1}[iI]{1}[mM]{1}[dD]{1}[aA]{1}[eE]{1}[sS]{1}[iI]{1})(?!.*?[cC]{1}[iI]{1}[sS]{1}[cC]{1}[oO]{1})(?!.*?[oO]{1}[cC]{1}[sS]{1}[iI]{1}[cC]{1})).{6,25}$ ConstraintDescription: >- The password for username (admin) to log in to the Cisco ISE GUI. The password must contain a minimum of 6 and maximum of 25 characters, and must include at least one numeral, one uppercase letter, and one lowercase letter. Password should not be the same as username or its reverse(admin or nimdaesi) or (cisco or ocsic). Allowed Special Characters @~*!,+=_- StorageSize: Description: >- Specify the storage in GB (Minimum 300GB and Maximum 2400GB). 600GB is recommended for production use, storage lesser than 600GB can be used for evaluation purpose only. On terminating the instance, volume will be deleted as well. Type: Number Default: '600' MinValue: '300' MaxValue: '2400' ConstraintDescription: >- Specify the storage in GB (Minimum 300GB and Maximum 2400GB). 600GB is recommended for production use, storage lesser than 600GB can be used for evaluation purpose only. On terminating the instance, volume will be deleted as well. VPCID: Description: 'ID of the VPC (e.g., vpc-0343606e)' Type: 'AWS::EC2::VPC::Id' VPCCIDR: AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$' ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 Default: 10.0.0.0/16 Description: CIDR block for the VPC. Type: String LBPrivateAddressSubnet1: AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])' ConstraintDescription: IP Address parameter must be in the form x.x.x.x #Default: 10.0.0.4 Description: Private IP Address of Load Balancer for Private Subnet-1 Type: String LBPrivateAddressSubnet2: AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])' ConstraintDescription: IP Address parameter must be in the form x.x.x.x #Default: 10.0.32.4 Description: Private IP Address of Load Balancer for Private Subnet-2 Type: String EmailSubscription: Description: 'Email list which will be notified about the Deployment health and Failover status' Type: String Mappings: CiscoISEAMI: us-east-1: BYOL31: ami-0bb0a9d243824a077 BYOL32: ami-08c545c5ef3cacced us-east-2: BYOL31: ami-0262130ee7b27f122 BYOL32: ami-068b6e34ad1266819 us-west-1: BYOL31: ami-0965fef2e601ad4d0 BYOL32: ami-0768dd8e82836d887 us-west-2: BYOL31: ami-0ffd69a117dbcbb9e BYOL32: ami-0531d829a9e4d6b83 ca-central-1: BYOL31: ami-0715d661908b3c937 BYOL32: ami-0d440428a5401cd3e eu-central-1: BYOL31: ami-0526fe132f57b4dd5 BYOL32: ami-0959760bb044c3247 eu-west-1: BYOL31: ami-0c0078c6bc939b794 BYOL32: ami-0c3b9a181c1c91a3a eu-west-2: BYOL31: ami-0a0e17dd5fa1643e9 BYOL32: ami-00e0b109d715904ad eu-west-3: BYOL31: ami-0f766d122c0b5c7b1 BYOL32: ami-04dee19d63c2edb18 eu-north-1: BYOL31: ami-06d5092c5d2de909d BYOL32: ami-00e9fa9b6e9bcec20 eu-south-1: BYOL31: ami-0941a499217ec268e BYOL32: ami-060ed864daf36bcac eu-south-2: BYOL31: ami-006a07d274fcaffac BYOL32: ami-0b433a7587fea7e41 ap-southeast-1: BYOL31: ami-0214a475ff692424f BYOL32: ami-02bb8125b423c29dc ap-southeast-2: BYOL31: ami-0f1846c9d911d1727 BYOL32: ami-0f238188265b7f80b ap-southeast-3: BYOL31: ami-0a824feea34fe65fb BYOL32: ami-0da7b907b79029925 ap-southeast-4: BYOL31: ami-06a6a3b37cf23c6e7 BYOL32: ami-0ea8a008eea59002f ap-south-1: BYOL31: ami-0add11be4e3a2b72e BYOL32: ami-05ef3254c75ce4053 ap-south-2: BYOL31: ami-09896c9d9eeed3138 BYOL32: ami-0448864ec746d003a ap-northeast-1: BYOL31: ami-0da69493a00c3ebb1 BYOL32: ami-07a8db1bcd9d807a7 ap-northeast-2: BYOL31: ami-0a56667a39f884c9e BYOL32: ami-032bcdac0d576df35 ap-east-1: BYOL31: ami-0118aa54aed56f415 BYOL32: ami-0e401f651fbb61c1d me-south-1: BYOL31: ami-0a2f4b9a138b52221 BYOL32: ami-0c8012fd684bdfbb5 ap-northeast-3: BYOL31: ami-05d2412cb877a373f BYOL32: ami-0d6ab22fd8ac904a3 sa-east-1: BYOL31: ami-0feeceb6d1a0dd691 BYOL32: ami-0c1b6e1fb53940d10 us-gov-west-1: BYOL31: ami-0692c2574536577a7 BYOL32: ami-0245b2414c12ac588 us-gov-east-1: BYOL31: ami-0cf29ff16a189964b BYOL32: ami-03594105967b59456 af-south-1: BYOL31: ami-003d33a44238d468e BYOL32: ami-08164edf9ea98e66e me-central-1: BYOL31: ami-023f6853b95edc0d7 BYOL32: ami-0889e9152037cb637 ISEVersionMap: '3.1': Code: BYOL31 '3.2': Code: BYOL32 Conditions: IsISE32Condition: !Equals [!Ref ISEVersion, '3.2' ] Resources: ISESecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: Enable HTTP, HTTPS and SSH to the ISE Server SecurityGroupIngress: - IpProtocol: -1 Description: "Allow Internal traffic" CidrIp: !Ref VPCCIDR VpcId: !Ref VPCID IseLaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateName: !Sub ${AWS::StackName}-00_ISE_cluster_launch_template LaunchTemplateData: InstanceType: !Ref ISEInstanceType KeyName: !Ref KeyPairName ImageId: !FindInMap [CiscoISEAMI, !Ref 'AWS::Region', !FindInMap [ISEVersionMap, !Ref ISEVersion, Code]] BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: VolumeType: gp2 DeleteOnTermination: 'true' Encrypted: !Ref EBSEncrypt VolumeSize: !Ref StorageSize SecurityGroupIds: - !Ref ISESecurityGroup ISENode1: Type: 'AWS::EC2::Instance' Properties: LaunchTemplate: LaunchTemplateId: !Ref IseLaunchTemplate Version: !GetAtt 'IseLaunchTemplate.LatestVersionNumber' SubnetId: !Ref PrivateSubnet1A Tags: - Key: "Name" Value: !Ref Node1Hostname UserData: !If - IsISE32Condition - !Base64 'Fn::Join': - '' - - hostname= - !Ref Node1Hostname - |+ - dnsdomain= - !Ref DNSDomain - |+ - primarynameserver=169.254.169.253 - |+ - ntpserver=169.254.169.123 - |+ - username=iseadmin - |+ - password= - !Ref password - |+ - timezone= - !Ref TimeZone - |+ - ersapi= - !Ref ERSapi - |+ - openapi= - !Ref OpenAPI - |+ - pxGrid= - !Ref PXGrid - |+ - pxgrid_cloud= - !Ref PXGridCloud - |+ - !Base64 'Fn::Join': - '' - - hostname= - !Ref Node1Hostname - |+ - dnsdomain= - !Ref DNSDomain - |+ - primarynameserver=169.254.169.253 - |+ - ntpserver=169.254.169.123 - |+ - username=admin - |+ - password= - !Ref password - |+ - timezone= - !Ref TimeZone - |+ - ersapi= - !Ref ERSapi - |+ - openapi= - !Ref OpenAPI - |+ - pxGrid= - !Ref PXGrid - |+ - pxgrid_cloud= - !Ref PXGridCloud - |+ ISENode2: Type: 'AWS::EC2::Instance' Properties: LaunchTemplate: LaunchTemplateId: !Ref IseLaunchTemplate Version: !GetAtt 'IseLaunchTemplate.LatestVersionNumber' SubnetId: !Ref PrivateSubnet1B Tags: - Key: "Name" Value: !Ref Node2Hostname UserData: !If - IsISE32Condition - !Base64 'Fn::Join': - '' - - hostname= - !Ref Node2Hostname - |+ - dnsdomain= - !Ref DNSDomain - |+ - primarynameserver=169.254.169.253 - |+ - ntpserver=169.254.169.123 - |+ - username=iseadmin - |+ - password= - !Ref password - |+ - timezone= - !Ref TimeZone - |+ - ersapi= - !Ref ERSapi - |+ - openapi= - !Ref OpenAPI - |+ - pxGrid= - !Ref PXGrid - |+ - pxgrid_cloud= - !Ref PXGridCloud - |+ - !Base64 'Fn::Join': - '' - - hostname= - !Ref Node2Hostname - |+ - dnsdomain= - !Ref DNSDomain - |+ - primarynameserver=169.254.169.253 - |+ - ntpserver=169.254.169.123 - |+ - username=admin - |+ - password= - !Ref password - |+ - timezone= - !Ref TimeZone - |+ - ersapi= - !Ref ERSapi - |+ - openapi= - !Ref OpenAPI - |+ - pxGrid= - !Ref PXGrid - |+ - pxgrid_cloud= - !Ref PXGridCloud - |+ #Route53 configuration ForwardDNS: Type: 'AWS::Route53::HostedZone' Properties: HostedZoneConfig: Comment: 'Private hosted forward zone for ISE' Name: !Ref DNSDomain VPCs: - VPCId: !Ref VPCID VPCRegion: !Sub ${AWS::Region} HostedZoneTags: - Key: 'Name' Value: !Sub - forwardzone-${DNSDomain} - DNSDomain: !Ref DNSDomain ReverseDNS: Type: 'AWS::Route53::HostedZone' Properties: HostedZoneConfig: Comment: 'Private hosted reverse zone for ISE' Name: !Join - "." - - !Select [1, !Split ['.', !Ref VPCCIDR]] - !Select [0, !Split ['.', !Ref VPCCIDR]] - 'in-addr.arpa' VPCs: - VPCId: !Ref VPCID VPCRegion: !Sub ${AWS::Region} HostedZoneTags: - Key: 'Name' Value: !Sub - reversezone-${DNSDomain} - DNSDomain: !Ref DNSDomain ISEForwardDNSRecords: Type: AWS::Route53::RecordSetGroup Properties: Comment: Create records for Route53 hosted forward zone HostedZoneId: !Ref ForwardDNS RecordSets: - Name: !Join - "." - - !Ref Node1Hostname - !Ref DNSDomain ResourceRecords: - !GetAtt ISENode1.PrivateIp Type: A TTL: "300" - Name: !Join - "." - - !Ref Node2Hostname - !Ref DNSDomain ResourceRecords: - !GetAtt ISENode2.PrivateIp Type: A TTL: "300" ISEReverseDNSRecordNode1: Type: AWS::Route53::RecordSet Properties: Comment: Create records for Route53 hosted reverse zone for ise node 1 HostedZoneId: !Ref ReverseDNS Name: !Join - "." - - !Select [3, !Split ['.', !GetAtt ISENode1.PrivateIp]] - !Select [2, !Split ['.', !GetAtt ISENode1.PrivateIp]] - !Select [1, !Split ['.', !GetAtt ISENode1.PrivateIp]] - !Select [0, !Split ['.', !GetAtt ISENode1.PrivateIp]] - 'in-addr.arpa' ResourceRecords: - !Join [ ".", [!Ref Node1Hostname, !Ref DNSDomain] ] Type: PTR TTL: "300" ISEReverseDNSRecordNode2: Type: AWS::Route53::RecordSet Properties: Comment: Create records for Route53 hosted reverse zone for ise node 2 HostedZoneId: !Ref ReverseDNS Name: !Join - "." - - !Select [3, !Split ['.', !GetAtt ISENode2.PrivateIp]] - !Select [2, !Split ['.', !GetAtt ISENode2.PrivateIp]] - !Select [1, !Split ['.', !GetAtt ISENode2.PrivateIp]] - !Select [0, !Split ['.', !GetAtt ISENode2.PrivateIp]] - 'in-addr.arpa' ResourceRecords: - !Join [ ".", [!Ref Node2Hostname, !Ref DNSDomain] ] Type: PTR TTL: "300" LBDNSRecord: Type: AWS::Route53::RecordSet Properties: Type: A Name: !Join - "." - - "lb" - !Ref DNSDomain AliasTarget: HostedZoneId: !GetAtt PSNNLB.CanonicalHostedZoneID DNSName: !GetAtt PSNNLB.DNSName HostedZoneId: !Ref ForwardDNS #load balancer resources PSNNLB: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Type: network Scheme: internal IpAddressType: ipv4 SubnetMappings: - SubnetId: !Ref PrivateSubnet1A PrivateIPv4Address: !Ref LBPrivateAddressSubnet1 - SubnetId: !Ref PrivateSubnet1B PrivateIPv4Address: !Ref LBPrivateAddressSubnet2 LoadBalancerAttributes: - Key: load_balancing.cross_zone.enabled Value: "true" Tags: - Key: Name Value: !Sub ${VPCID}-Nlb PSNTargetGroupforRADIUS1812: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckPort: '443' HealthCheckProtocol: TCP Port: 1812 Protocol: UDP Targets: - Id: !Ref ISENode1 - Id: !Ref ISENode2 VpcId: !Ref VPCID TargetGroupAttributes: - Key: stickiness.enabled Value: "true" Tags: - Key: "Name" Value: Radius1812 PSNTargetGroupforRADIUS1813: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckPort: '443' HealthCheckProtocol: TCP Port: 1813 Protocol: UDP Targets: - Id: !Ref ISENode1 - Id: !Ref ISENode2 VpcId: !Ref VPCID TargetGroupAttributes: - Key: stickiness.enabled Value: "true" Tags: - Key: "Name" Value: Radius1813 PSNTargetGroupforRADIUS1645: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckPort: '443' HealthCheckProtocol: TCP Port: 1645 Protocol: UDP Targets: - Id: !Ref ISENode1 - Id: !Ref ISENode2 VpcId: !Ref VPCID TargetGroupAttributes: - Key: stickiness.enabled Value: "true" Tags: - Key: "Name" Value: Radius1645 PSNTargetGroupforRADIUS1646: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckPort: '443' HealthCheckProtocol: TCP Port: 1646 Protocol: UDP Targets: - Id: !Ref ISENode1 - Id: !Ref ISENode2 VpcId: !Ref VPCID Tags: - Key: "Name" Value: Radius1646 PSNTargetGroupforTACACS49: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckPort: '443' HealthCheckProtocol: TCP Port: 49 Protocol: TCP Targets: - Id: !Ref ISENode1 - Id: !Ref ISENode2 VpcId: !Ref VPCID TargetGroupAttributes: - Key: stickiness.enabled Value: "true" Tags: - Key: "Name" Value: Tacacs49 PSNListener1: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref PSNTargetGroupforRADIUS1812 LoadBalancerArn: !Ref PSNNLB Port: 1812 Protocol: UDP PSNListener2: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref PSNTargetGroupforRADIUS1813 LoadBalancerArn: !Ref PSNNLB Port: 1813 Protocol: UDP PSNListener3: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref PSNTargetGroupforTACACS49 LoadBalancerArn: !Ref PSNNLB Port: 49 Protocol: TCP PSNListener4: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref PSNTargetGroupforRADIUS1645 LoadBalancerArn: !Ref PSNNLB Port: 1645 Protocol: UDP PSNListener5: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref PSNTargetGroupforRADIUS1646 LoadBalancerArn: !Ref PSNNLB Port: 1646 Protocol: UDP NotifyAdminTopic: Type: AWS::SNS::Topic Properties: DisplayName: NotifyAdminTopic #Subscription: # - Endpoint: !Ref EmailSubscription # Protocol: "email" TopicName: NotifyAdminTopic SNSSubscription: Type: AWS::SNS::Subscription Properties: Endpoint: !Ref EmailSubscription Protocol: "email" TopicArn: !Ref NotifyAdminTopic PipLayerLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - lambda.amazonaws.com Policies: - PolicyDocument: Version: 2012-10-17 Statement: - Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Effect: Allow Resource: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/PipLayer-ISE-Lambda:* - Action: - lambda:PublishLayerVersion - lambda:DeleteLayerVersion Effect: Allow Resource: - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:layer:* PolicyName: lambda PipLayerLambda: Type: AWS::Lambda::Function Properties: Description: Create layers based on pip FunctionName: !Sub "PipLayer-ISE-Lambda" Handler: index.handler MemorySize: 1024 Role: !GetAtt PipLayerLambdaRole.Arn Runtime: python3.9 Timeout: 300 Code: ZipFile: | import json import logging import pathlib import re import subprocess import sys import tempfile import typing as t import shutil import cfnresponse import boto3 logger = logging.getLogger() logger.setLevel(logging.INFO) class PipLayerException(Exception): pass def _create(properties) -> t.Tuple[str, t.Mapping[str, str]]: try: layername = properties["LayerName"] description = properties.get("Description", "PipLayer") packages = properties["Packages"] except KeyError as e: raise PipLayerException("Missing parameter: %s" % e.args[0]) description += " ({})".format(", ".join(packages)) if not isinstance(layername, str): raise PipLayerException("LayerName must be a string") if not isinstance(description, str): raise PipLayerException("Description must be a string") if not isinstance(packages, list) or not all(isinstance(p, str) for p in packages): raise PipLayerException("Packages must be a list of strings") tempdir = pathlib.Path(tempfile.TemporaryDirectory().name) / "python" try: subprocess.check_call([ sys.executable, "-m", "pip", "install", *packages, "-t", tempdir]) except subprocess.CalledProcessError: raise PipLayerException("Error while installing %s" % str(packages)) zipfilename = pathlib.Path(tempfile.NamedTemporaryFile(suffix=".zip").name) shutil.make_archive( zipfilename.with_suffix(""), format="zip", root_dir=tempdir.parent) client = boto3.client("lambda") layer = client.publish_layer_version( LayerName=layername, Description=description, Content={"ZipFile": zipfilename.read_bytes()}, CompatibleRuntimes=["python%d.%d" % sys.version_info[:2]], ) logger.info("Created layer %s", layer["LayerVersionArn"]) return (layer["LayerVersionArn"], {}) def _delete(physical_id): match = re.fullmatch( r"arn:aws:lambda:(?P[^:]+):(?P\d+):layer:" r"(?P[^:]+):(?P\d+)", physical_id) if not match: logger.warning("Cannot parse physical id %s, not deleting", physical_id) return layername = match.group("layername") version_number = int(match.group("version_number")) logger.info("Now deleting layer %s:%d", layername, version_number) client = boto3.client("lambda") deletion = client.delete_layer_version( LayerName=layername, VersionNumber=version_number) logger.info("Done") def handler(event, context): logger.info('{"event": %s}', json.dumps(event)) try: if event["RequestType"].upper() in ("CREATE", "UPDATE"): # Note: treat UPDATE as CREATE; it will create a new physical ID, # signalling CloudFormation that it's a replace and the old should be # deleted physicalId, attributes = _create(event["ResourceProperties"]) cfnresponse.send( event=event, context=context, responseData=attributes, responseStatus=cfnresponse.SUCCESS, physicalResourceId=physicalId, ) else: assert event["RequestType"].upper() == "DELETE" _delete(event["PhysicalResourceId"]) cfnresponse.send( event=event, context=context, responseData={}, responseStatus=cfnresponse.SUCCESS, physicalResourceId=event["PhysicalResourceId"], ) except Exception as e: logger.exception("Internal Error") cfnresponse.send( event=event, context=context, responseData=None, responseStatus=cfnresponse.FAILED, reason=str(e)) CiscoISELayer: Type: Custom::PipLayer Properties: ServiceToken: !GetAtt PipLayerLambda.Arn Region: !Ref AWS::Region LayerName: CiscoISElayer Packages: - boto3 - requests # Set up ISE Deployment resources ISEDeployment: Type: Custom::ISEDeployment DependsOn: - PipLayerLambda - DeploymentStateMachine Properties: ServiceToken: !GetAtt DeploymentStateMachineLambda.Arn WaitHandle: !Ref DeploymentWaitHandle DeploymentStateMachineLambda: Type: AWS::Lambda::Function Properties: Description: Invokes Lambda that kicks off deployment state machine Handler: index.handler Runtime: python3.9 Role: !GetAtt DeploymentStateMachineLambdaRole.Arn Timeout: 300 Code: ZipFile: !Sub | import boto3 import json import logging import threading import cfnresponse sfn = boto3.client('stepfunctions') def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') cfnresponse.send(event, context, cfnresponse.FAILED, {}, None) def handler(event, context): timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) timer.start() logging.info('Received event: %s' % json.dumps(event)) status = cfnresponse.SUCCESS responseData = {} try: if event['RequestType'] == 'Create': response = sfn.start_execution( stateMachineArn='${DeploymentStateMachine}', input=json.dumps(event) ) responseData = {'executionArn': response['executionArn']} else: logging.info('Nothing to do') except Exception as e: logging.error('Exception: %s' % e, exc_info=True) status = cfnresponse.FAILED finally: timer.cancel() cfnresponse.send(event, context, status, responseData) ISENode1IPParameter: Type: AWS::SSM::Parameter Properties: Description: IP Address of Primary PAN Name: Primary_IP Type: String Value: !GetAtt - ISENode1 - PrivateIp ISENode2IPParameter: Type: AWS::SSM::Parameter Properties: Description: IP Address of Secondary PAN Name: Secondary_IP Type: String Value: !GetAtt - ISENode2 - PrivateIp ISENode1FqdnParameter: Type: AWS::SSM::Parameter Properties: Description: FQDN of Primary PAN Name: Primary_FQDN Type: String Value: !Join [".",[!Ref Node1Hostname, !Ref DNSDomain]] ISENode2FqdnParameter: Type: AWS::SSM::Parameter Properties: Description: FQDN of Secondary PAN Name: Secondary_FQDN Type: String Value: !Join [".",[!Ref Node2Hostname, !Ref DNSDomain]] AdminUsernameParameter: Type: AWS::SSM::Parameter Properties: Description: Admin username of ISE Name: ADMIN_USERNAME Type: String Value: !If - IsISE32Condition - iseadmin - admin AdminPasswordParameter: Type: AWS::SSM::Parameter Properties: Description: Admin password of ISE Name: ADMIN_PASSWORD Type: String Value: !Ref password SyncStatusParameter: Type: AWS::SSM::Parameter Properties: Description: Sync status of deployment Name: SyncStatus Type: String Value: INITIAL MaintenanceParameter: Type: AWS::SSM::Parameter Properties: Description: Set this parameter to ENABLED for Maintenance Name: Maintenance Type: String Value: DISABLED DeploymentStateMachineLambdaRole: Metadata: cfn-lint: config: ignore_checks: - EIAMPolicyWildcardResource ignore_reasons: - EIAMPolicyWildcardResource: "Wildcard resource allowed by design" Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - states:StartExecution Resource: "*" DeploymentStateMachine: Type: AWS::StepFunctions::StateMachine DependsOn: - CheckISEStatusLambda - SetPrimaryPANLambda - RegisterSecondaryNodeLambda - CheckSyncStatusLambda - CallbackLambda Properties: RoleArn: !GetAtt StatesExecutionRole.Arn DefinitionString: !Sub |- { "Comment": "A state machine that performs ISE nodes deployment.", "StartAt": "Explicit Wait for ISE boot start", "States": { "Explicit Wait for ISE boot start": { "Type": "Wait", "Seconds": 1500, "Next": "Wait for ISE to come up" }, "Check ISE status": { "Type": "Task", "Resource": "${CheckISEStatusLambda.Arn}", "Next": "If ISE is up and running", "InputPath": "$", "ResultPath": "$.taskresult" }, "If ISE is up and running": { "Type": "Choice", "Choices": [ { "And": [ { "Variable": "$.taskresult.IseState", "StringEquals": "pending" }, { "Variable": "$.taskresult.retries", "StringEquals": "0" } ], "Next": "ISE instance is not running" }, { "Variable": "$.taskresult.IseState", "StringEquals": "running", "Next": "Set Primary PAN" } ], "Default": "Wait for ISE to come up", "InputPath": "$" }, "Wait for ISE to come up": { "Type": "Wait", "Seconds": 180, "Next": "Check ISE status" }, "Set Primary PAN": { "Type": "Task", "Resource": "${SetPrimaryPANLambda.Arn}", "InputPath": "$", "Next": "Register Secondary Node", "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "Fail: Error Occured" } ], "ResultPath": "$.taskresult" }, "Register Secondary Node": { "Type": "Task", "Resource": "${RegisterSecondaryNodeLambda.Arn}", "InputPath": "$", "Next": "Check Sync status", "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "Fail: Error Occured" } ], "ResultPath": "$.taskresult" }, "Check Sync status": { "Type": "Task", "Resource": "${CheckSyncStatusLambda.Arn}", "InputPath": "$", "Next": "Is Sync complete?", "ResultPath": "$.taskresult", "Retry": [ { "ErrorEquals": [ "States.ALL" ], "BackoffRate": 1, "IntervalSeconds": 240, "MaxAttempts": 7 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "Is Sync complete?", "ResultPath": "$.taskresult.SyncStatus" } ] }, "Is Sync complete?": { "Type": "Choice", "Choices": [ { "Variable": "$.taskresult.SyncStatus", "StringEquals": "SYNC_COMPLETED", "Next": "Send Cfn Response" }, { "Variable": "$.taskresult.SyncStatus.Error", "StringEquals": "Exception", "Next": "Secondary node Sync failed" }, { "Variable": "$.taskresult.SyncStatus", "StringEquals": "SYNC_FAILED", "Next": "Secondary node Sync failed" } ], "Default": "Check Sync status", "InputPath": "$" }, "Send Cfn Response": { "Type": "Task", "Resource": "${CallbackLambda.Arn}", "InputPath": "$", "End": true, "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "Fail: Error Occured" } ] }, "Fail: Error Occured": { "Type": "Fail" }, "Secondary node Sync failed": { "Type": "Fail" }, "ISE instance is not running": { "Type": "Fail" } } } StatesExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - !Sub states.${AWS::Region}.amazonaws.com Action: - "sts:AssumeRole" StatesExecutionPolicy: Metadata: cfn-lint: config: ignore_checks: - EIAMPolicyWildcardResource ignore_reasons: - EIAMPolicyWildcardResource: "Wildcard resource allowed by design" Type: AWS::IAM::Policy Properties: PolicyName: StatesExecutionPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: ["lambda:InvokeFunction", "sns:publish"] Resource: "*" Roles: - !Ref StatesExecutionRole # Set up Wait Condition for State Machine DeploymentWaitHandle: Type: AWS::CloudFormation::WaitConditionHandle DeploymentWaitCondition: Type: AWS::CloudFormation::WaitCondition DependsOn: ISEDeployment Properties: Count: 1 Handle: !Ref DeploymentWaitHandle Timeout: 7200 CheckISEStatusLambda: Type: AWS::Lambda::Function Properties: Description: Checks status of both ISE nodes post instance creation Timeout: 300 Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os import socket import urllib3 urllib3.disable_warnings() logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) def get_ssm_parameter(ssm_client, ssm_parameter_name,WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def set_ssm_parameter(ssm_client, ssm_parameter_name, value, Overwrite=True, Type="String"): response = ssm_client.put_parameter( Name=ssm_parameter_name, Value=value, Overwrite=True, Type=Type ) return response def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') requests_data=json.dumps(data=dict(Status='FAILURE',Reason='Lambda timeout',UniqueId='ISENodeStates',Data='failed due to timeout')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) sys.exit(1) def handler(event, context): runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) try: retries = 10 #Polling rate to restrict Step functions looping if "taskresult" in event: retries = int(event['taskresult']['retries']) else: PPAN_fqdn = get_ssm_parameter(ssm_client,"Primary_FQDN") SPAN_fqdn = get_ssm_parameter(ssm_client,"Secondary_FQDN") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD") #logger.info("current admin password : {}".format(ADMIN_PASSWORD)) isenode01_ip = socket.gethostbyname_ex(PPAN_fqdn)[2][-1] isenode02_ip = socket.gethostbyname_ex(SPAN_fqdn)[2][-1] logger.info("Installed ISE Instance IPs are:\n PPAN - {} , SPAN - {}".format(isenode01_ip, isenode02_ip)) logger.info("#Setting SSM parameters...") set_ssm_parameter(ssm_client,"Primary_IP",isenode01_ip) set_ssm_parameter(ssm_client,"Secondary_IP",isenode02_ip) set_ssm_parameter(ssm_client,"ADMIN_USERNAME",ADMIN_USERNAME) set_ssm_parameter(ssm_client,"ADMIN_PASSWORD",ADMIN_PASSWORD,Type="SecureString") logger.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) # logger.info("current admin password : {}".format(ADMIN_PASSWORD)) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} Secondary_FQDN = get_ssm_parameter(ssm_client,"Secondary_FQDN") logger.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logger.info("Secondary Polcy Administration node ip : {}".format(Secondary_IP)) logger.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) #logger.info("API_AUTH : {}".format(API_AUTH)) logger.info("API_HEADER : {}".format(API_HEADER)) logger.info("Secondary Polcy Administration node fqdn : {}".format(Secondary_FQDN)) data = {} nodes_to_check = [Primary_IP, Secondary_IP] nodes_list = [Primary_IP, Secondary_IP] for ip in nodes_to_check: url = 'https://{}/api/v1/deployment/node'.format(ip) try: resp = requests.get(url, headers=API_HEADER, auth=API_AUTH, data=json.dumps(data), verify=False) logger.info("API response for {} is {} ".format(ip, resp.text)) if resp.status_code == 200: nodes_list.remove(ip) logger.info("ISE - {} is up and running".format(ip)) except Exception as e: logging.error('Exception: %s' % e, exc_info=True) logger.error("Exception occured while executing get node details api for {}".format(ip)) retries -= 1 return { "IseState": "pending", "retries": str(retries) } if nodes_list: timer.cancel() retries -= 1 return { "IseState": "pending", "retries": str(retries) } else: timer.cancel() return { "IseState": "running", "retries": "0" } except Exception as e: logging.error('Exception: %s' % e, exc_info=True) Handler: index.handler Role: !GetAtt CheckISEStatusExecutionRole.Arn Runtime: python3.9 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer CheckISEStatusExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* # Lambda - Sets one node to Primary PAN SetPrimaryPANLambda: Type: AWS::Lambda::Function Properties: Description: Promotes ISENode01 to Primary Administration Node Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os import urllib3 urllib3.disable_warnings() logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) class PromoteToPrimaryFailed(Exception): """Raised when the Set Primary PAN Lambda is failed""" pass def get_ssm_parameter(ssm_client, ssm_parameter_name,WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') requests_data=json.dumps(data=dict(Status='FAILURE',Reason='Lambda timeout',UniqueId='ISENodeStates',Data='failed due to timeout')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) sys.exit(1) def handler(event, context): runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) logger.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} logger.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logger.info("Secondary Polcy Administration node ip : {}".format(Secondary_IP)) logger.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) #logger.info("API_AUTH : {}".format(API_AUTH)) logger.info("API_HEADER : {}".format(API_HEADER)) try: url = 'https://{}/api/v1/deployment/primary'.format(Primary_IP) data = {} resp = requests.post(url, headers=API_HEADER, auth=API_AUTH, data=json.dumps(data), verify=False) logger.info("API Response is : {}".format(resp)) if resp.status_code == 200: logger.info("##### Standalone to Primary Successful on {} #####".format(Primary_IP)) timer.cancel() return { "task_status": "Done" } raise PromoteToPrimaryFailed({"Setting ISE Node to Primary Failed"}) except Exception as e: requests_data=json.dumps(dict(Status='FAILURE',Reason='Setting ISE Node to Primary Failed',UniqueId='ISENodeStates',Data='exception')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) logger.info(response) logging.error('Exception: %s' % e, exc_info=True) timer.cancel() Handler: index.handler Role: !GetAtt SetPrimaryPANLambdaExecutionRole.Arn Runtime: python3.9 Timeout: 300 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer SetPrimaryPANLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* RegisterSecondaryNodeLambda: Type: AWS::Lambda::Function Properties: Description: Registers ISENode02 to Primary Administration Node ISENode01 (forming a 2-node deployment) Timeout: 300 Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os import urllib3 urllib3.disable_warnings() logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) class RegisterSecondaryNodeFailed(Exception): """Raised when the Set Primary PAN Lambda is failed""" pass def get_ssm_parameter(ssm_client, ssm_parameter_name,WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') requests_data=json.dumps(data=dict(Status='FAILURE',Reason='Lambda timeout',UniqueId='ISENodeStates',Data='failed due to timeout')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) sys.exit(1) def handler(event, context): runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) logger.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") Secondary_FQDN = get_ssm_parameter(ssm_client,"Secondary_FQDN") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} logger.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logger.info("Secondary Polcy Administration node ip : {}".format(Secondary_IP)) logger.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) #logger.info("API_AUTH : {}".format(API_AUTH)) logger.info("API_HEADER : {}".format(API_HEADER)) logger.info("# Register Secondary node to deployment - start...") try: roles_enabled = ["SecondaryAdmin","SecondaryMonitoring"] service_enabled = ["Session", "Profiler", "pxGrid"] url = 'https://{}/api/v1/deployment/node'.format(Primary_IP) data = {"allowCertImport": True, "fqdn": Secondary_FQDN, "userName": ADMIN_USERNAME, "password": ADMIN_PASSWORD, "roles": roles_enabled, "services": service_enabled, } logger.info('Url: {}, Data: {}'.format(url, data)) resp = requests.post(url, headers=API_HEADER, auth=API_AUTH, data=json.dumps(data), verify=False) logger.info('Register secondary response: {}, {}'.format(resp.status_code, resp.text)) if resp.status_code == 200: logger.info("Register Secondary node is successfull, API response is {}".format(resp.text)) timer.cancel() return { "task_status": "Done" } raise RegisterSecondaryNodeFailed({"Register Secondary node to form deployment Failed"}) except RegisterSecondaryNodeFailed: requests_data=json.dumps(dict(Status='FAILURE',Reason='Secondary Node Registration Failed',UniqueId='ISENodeStates',Data='exception')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) logger.info(response) timer.cancel() logging.error('Exception: %s', exc_info=True) except Exception as e: requests_data=json.dumps(dict(Status='FAILURE',Reason='Secondary Node Registration Failed',UniqueId='ISENodeStates',Data='exception')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) logger.info(response) timer.cancel() logging.error('Exception: %s' % e, exc_info=True) Handler: index.handler Role: !GetAtt RegisterSecondaryNodeExecutionRole.Arn Runtime: python3.9 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer RegisterSecondaryNodeExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* # Lambda - Describes Primary PAN status. In this case, used for starting and stopping instances CheckSyncStatusLambda: Type: AWS::Lambda::Function Properties: Description: Checks for Successful 2-Node Deployment until Sync Completes between ISENode01 and ISENode02 Timeout: 300 Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os import urllib3 urllib3.disable_warnings() logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) def get_ssm_parameter(ssm_client, ssm_parameter_name,WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def set_ssm_parameter(ssm_client, ssm_parameter_name, value, Overwrite=True, Type="String"): response = ssm_client.put_parameter( Name=ssm_parameter_name, Value=value, Overwrite=True, Type=Type ) return response def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') requests_data=json.dumps(data=dict(Status='FAILURE',Reason='Lambda timeout',UniqueId='ISENodeStates',Data='failed due to timeout')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) sys.exit(1) def handler(event, context): timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) try: logger.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} logger.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logger.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) #logger.info("API_AUTH : {}".format(API_AUTH)) logger.info("API_HEADER : {}".format(API_HEADER)) data = {} url = 'https://{}/api/v1/deployment/node'.format(Primary_IP) try: resp = requests.get(url, headers=API_HEADER, auth=API_AUTH, data=json.dumps(data), verify=False) logger.info("API response for checking node sync status and roles is: {} ".format(resp.text)) if resp.status_code == 200: json_resp = json.loads(resp.content.decode("utf-8")) if json_resp["response"][0]['nodeStatus'] == "Connected" and json_resp["response"][1]['nodeStatus'] == "Connected": set_ssm_parameter(ssm_client,"SyncStatus","SYNC_COMPLETED") timer.cancel() return { "SyncStatus": "SYNC_COMPLETED", "retries": "0" } elif json_resp["response"][1]['nodeStatus'] == "RegistrationFailed": set_ssm_parameter(ssm_client,"SyncStatus","SYNC_FAILED") requests_data=json.dumps(dict(Status='FAILURE',Reason='Secondary Node Sync or Registration failed',UniqueId='ISENodeStates',Data='exception')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) logger.info(response) timer.cancel() return { "SyncStatus": "SYNC_FAILED" } else: timer.cancel() raise Exception("Sync still in progress") except Exception as e: logging.error('Exception: %s' % e, exc_info=True) logger.info("Exception occured while executing get node details api for {}".format(Primary_IP)) timer.cancel() raise Exception("Sync still in progress") except Exception as e: timer.cancel() logging.error('Exception: %s' % e, exc_info=True) # retries -= 1 # return { # "SyncStatus": "SYNC_INPROGRESS", # "retries": str(retries) # } raise Exception("Sync still in progress") Handler: index.handler Role: !GetAtt CheckSyncStatusExecutionRole.Arn Runtime: python3.9 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer CheckSyncStatusExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* # Lambda - Sends Callback to the CloudFormation Wait Condition, signaling whether the Deployment was created successfully CallbackLambda: Type: AWS::Lambda::Function Properties: Description: Sends callback to CloudFormation to continue Code: ZipFile: | import json import logging import threading import requests import sys import boto3 import os logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') requests_data=json.dumps(data=dict(Status='FAILURE',Reason='Lambda timeout',UniqueId='ISENodeStates',Data='failed due to timeout')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) sys.exit(1) def get_ssm_parameter(ssm_client, ssm_parameter_name, WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def handler(event, context): timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 2, timeout, args=[event, context]) timer.start() logger.info('Received event: %s' % json.dumps(event)) logger.info(event['ResourceProperties']['WaitHandle']) runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) try: logger.info("#Retriving SSM parameters...") SYNC_STATUS = get_ssm_parameter(ssm_client,"SyncStatus") if SYNC_STATUS == 'SYNC_COMPLETED': logger.info("Sync is successful") requests_data=json.dumps(dict(Status='SUCCESS',Reason='Deployment creation successful',UniqueId='ISENodeStates',Data='success')).encode('utf-8') else: logger.info("Sync failed") requests_data=json.dumps(dict(Status='FAILURE',Reason='Invalid Deployment Status',UniqueId='ISENodeStates',Data='failure')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) logger.info(response) timer.cancel() except Exception as e: logger.info("Sync failed") logging.error('Exception: %s' % e, exc_info=True) requests_data=json.dumps(dict(Status='FAILURE',Reason='Exception: %s' % e,UniqueId='ISENodeStates',Data='exception')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) logger.info(response) timer.cancel() Handler: index.handler Runtime: python3.9 Role: !GetAtt CallbackRole.Arn Timeout: 300 Layers: - !Ref CiscoISELayer CallbackRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* #The lambda needs to be written. Trigger needs to be defined TriggerFailoverLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AWSStepFunctionsFullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSNSFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* TriggerFailoverLambda: Type: AWS::Lambda::Function Properties: Description: Triggers Failover when Primary PAN is down Timeout: 300 Role: !GetAtt 'TriggerFailoverLambdaExecutionRole.Arn' Handler: index.handler Runtime: python3.9 Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) def handler(event, context): def get_ssm_parameter(ssm_client, ssm_parameter_name, WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def set_ssm_parameter(ssm_client, ssm_parameter_name, value, Overwrite=True, Type="String"): response = ssm_client.put_parameter( Name=ssm_parameter_name, Value=value, Overwrite=True, Type=Type ) return response def send_sns_publish(subject, message): client = boto3.client('sns') TopicArn = [item["TopicArn"] for item in client.list_topics()['Topics'] if "NotifyAdminTopic" in item["TopicArn"]] logger.info("Sending SNS publish..") response = client.publish( Subject= subject, Message= message, TopicArn= TopicArn[0] ) runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) try: logger.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") SyncStatus = get_ssm_parameter(ssm_client,"SyncStatus") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} retries = 5 Maintenance = get_ssm_parameter(ssm_client,"Maintenance") logger.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logger.info("Secondary Polcy Administration node ip : {}".format(Secondary_IP)) logger.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) #logger.info("API_AUTH : {}".format(API_AUTH)) logger.info("API_HEADER : {}".format(API_HEADER)) logger.info("Retries : {}".format(retries)) node_status = ["NotApplicable", "NotInSync", "NotUpgraded", "RegistrationFailed", "ReplicationStopped"] data = {} if (not Maintenance == "ENABLED"): if SyncStatus == "SYNC_INPROGRESS" or SyncStatus == "INITIAL": logger.info("Sync status is {}", SyncStatus) logger.info("Application still being deployed. Failover not required!") return "skip" elif SyncStatus == "SYNC_COMPLETED": stepFunction = boto3.client('stepfunctions') response = stepFunction.list_state_machines( maxResults=123 ) response = response['stateMachines'] failover_state_machine_Arn = "" for item in response: logger.debug("Identifying Failover statemachine Arn") if "FailoverStateMachine" in item['stateMachineArn']: logger.debug("FailoverStateMachine Arn : {}".format(item['stateMachineArn'])) failover_state_machine_Arn = item['stateMachineArn'] logger.info("Triggering state machine with arn {}".format(failover_state_machine_Arn)) response = stepFunction.start_execution( stateMachineArn=failover_state_machine_Arn ) else: logger.info("Need not execute Failover state machine. Sync status is unknown.") return "skip" else: logger.info("Deployment is in Maintenance. Health check service is not required.") return "skip" except Exception as e: logging.error('Exception: %s' % e, exc_info=True) MemorySize: 3008 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer FailoverLambdaSchedulePermission: Type: "AWS::Lambda::Permission" Properties: Action: 'lambda:InvokeFunction' FunctionName: !Sub ${TriggerFailoverLambda.Arn} Principal: 'events.amazonaws.com' SourceArn: !Sub ${FailoverLambdaSchedule.Arn} DependsOn: - FailoverLambdaSchedule FailoverLambdaSchedule: Type: "AWS::Events::Rule" Properties: Description: > A schedule for the Failover Lambda function.. ScheduleExpression: !Ref FailoverRate State: !Ref AutoFailover Targets: - Arn: !Sub ${TriggerFailoverLambda.Arn} Id: FailoverLambdaSchedule DependsOn: - TriggerFailoverLambda - DeploymentStateMachine HealthCheckLambdaSchedulePermission: Type: "AWS::Lambda::Permission" Properties: Action: 'lambda:InvokeFunction' FunctionName: !Sub ${HealthCheckLambda.Arn} Principal: 'events.amazonaws.com' SourceArn: !Sub ${HealthCheckLambdaSchedule.Arn} DependsOn: - HealthCheckLambdaSchedule HealthCheckLambdaSchedule: Type: "AWS::Events::Rule" Properties: Description: > A schedule for the Health check Lambda function.. ScheduleExpression: !Ref HealthcheckRate State: ENABLED Targets: - Arn: !Sub ${HealthCheckLambda.Arn} Id: HealthCheckLambdaSchedule DependsOn: - HealthCheckLambda - DeploymentStateMachine HealthCheckLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSNSFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* HealthCheckLambda: Type: AWS::Lambda::Function Properties: Description: Monitors the deployment and modifies Sync status in SSM parameter store Timeout: 300 Role: !GetAtt 'HealthCheckLambdaExecutionRole.Arn' Handler: index.handler Runtime: python3.9 Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) import urllib3 urllib3.disable_warnings() def handler(event, context): def get_ssm_parameter(ssm_client, ssm_parameter_name, WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def set_ssm_parameter(ssm_client, ssm_parameter_name, value, Overwrite=True, Type="String"): response = ssm_client.put_parameter( Name=ssm_parameter_name, Value=value, Overwrite=True, Type=Type ) return response def send_sns_publish(subject, message): client = boto3.client('sns') TopicArn = [item["TopicArn"] for item in client.list_topics()['Topics'] if "NotifyAdminTopic" in item["TopicArn"]] logger.info("Sending SNS publish..") response = client.publish( Subject= subject, Message= message, TopicArn= TopicArn[0] ) runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) try: logger.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") Primary_FQDN = get_ssm_parameter(ssm_client,"Primary_FQDN") Secondary_FQDN = get_ssm_parameter(ssm_client,"Secondary_FQDN") SyncStatus = get_ssm_parameter(ssm_client,"SyncStatus") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} retries = 5 Maintenance = get_ssm_parameter(ssm_client,"Maintenance") SyncStatus_OldValue = get_ssm_parameter(ssm_client,"SyncStatus") logger.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logger.info("Secondary Polcy Administration node ip : {}".format(Secondary_IP)) logger.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) logger.info("Maintenance : {}".format(Maintenance)) #logger.info("API_AUTH : {}".format(API_AUTH)) logger.info("API_HEADER : {}".format(API_HEADER)) logger.info("Retries : {}".format(retries)) node_status = ["NotApplicable", "NotInSync", "NotUpgraded", "RegistrationFailed", "ReplicationStopped"] data = {} if not Maintenance == "ENABLED": url = 'https://{}/api/v1/deployment/node'.format(Primary_IP) resp = requests.get(url, headers=API_HEADER, auth=API_AUTH, data=json.dumps(data), verify=False) logger.info("API response for checking node sync status and roles is: ") logger.info(resp.text) if resp.status_code == 401: logger.info("Username/Password is incorrect. Please check the ISE credentials in SSM Parameter Store.") send_sns_publish(subject="Cisco ISE [{}]: Deployment health check notification".format(runtime_region), message = "Admin Username/Password credentials are incorrect.") return "skip" json_resp = json.loads(resp.text) if resp.status_code == 200: if json_resp["response"][0]['nodeStatus'] == "Connected" and json_resp["response"][1]['nodeStatus'] == "Connected": set_ssm_parameter(ssm_client,"SyncStatus","SYNC_COMPLETED") else : set_ssm_parameter(ssm_client,"SyncStatus","SYNC_FAILED") send_sns_publish(subject="Cisco ISE [{}]: Deployment health check notification".format(runtime_region), message = "Secondary Node {} is not in sync with Primary Node {}. Please investigate".format(Secondary_FQDN, Primary_FQDN)) else: set_ssm_parameter(ssm_client,"SyncStatus","SYNC_FAILED") send_sns_publish(subject="Cisco ISE [{}]: Deployment health check notification".format(runtime_region), message = "Deployment is unstable. Primary Node {} returned status_code {}. Please investigate.".format(Primary_FQDN, str(resp.status_code))) else: logger.info("Deployment is in Maintenance. Health check service is not required.") except Exception as e: logging.error('Exception: %s' % e, exc_info=True) send_sns_publish(subject="Cisco ISE [{}]: Deployment health check notification".format(runtime_region), message = "Primary Node {} is down. Please investigate.".format(Primary_FQDN)) MemorySize: 3008 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer FailoverStateMachine: Type: AWS::StepFunctions::StateMachine DependsOn: - CheckSPANGatewayStatusLambda - PerformFailoverLambda - CheckFailoverSuccessfulLambda - DeploymentStateMachine Properties: RoleArn: !GetAtt StatesExecutionRole.Arn DefinitionString: !Sub |- { "Comment": "A state machine that performs Failover by promoting SPAN to PPAN.", "StartAt": "Check PPAN Status", "States": { "Check PPAN Status": { "Type": "Task", "Resource": "${CheckPPANStatusLambda.Arn}", "Next": "Choice-Trigger Failover?", "InputPath": "$", "OutputPath": "$", "Retry": [ { "ErrorEquals": [ "States.ALL" ], "BackoffRate": 1, "IntervalSeconds": 180, "MaxAttempts": 5, "Comment": "Retry PPAN lambda status Every 3mins" } ], "TimeoutSeconds": 120, "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "Choice-Trigger Failover?", "ResultPath": "$.PrimaryState" } ] }, "Choice-Trigger Failover?": { "Type": "Choice", "Choices": [ { "Variable": "$.PrimaryState", "StringEquals": "running", "Next": "PPAN is healthy. Failover not required!" }, { "Variable": "$.PrimaryState.Error", "StringEquals": "States.Timeout", "Next": "Check SPAN Gateway status" }, { "Variable": "$.PrimaryState.Error", "StringMatches": "Exception", "Next": "Check SPAN Gateway status" } ], "Default": "Check PPAN Status", "InputPath": "$" }, "Check SPAN Gateway status": { "Type": "Task", "Resource": "${CheckSPANGatewayStatusLambda.Arn}", "Next": "Choice - Is SPAN API Gateway enabled?", "ResultPath": "$.SecondaryState", "InputPath": "$" }, "Perform Failover": { "Type": "Task", "Resource": "${PerformFailoverLambda.Arn}", "Next": "Wait for Promotion", "ResultPath": "$.PromotionStatus", "InputPath": "$", "Catch": [ { "ErrorEquals": [ "States.Timeout" ], "Next": "Wait for Promotion" } ], "TimeoutSeconds": 180 }, "Choice - Is SPAN API Gateway enabled?": { "Type": "Choice", "Choices": [ { "Variable": "$.SecondaryState", "StringEquals": "GatewayDown", "Next": "SNSPublishGatewayDisabledDefault" }, { "Variable": "$.SecondaryState", "StringEquals": "running", "Next": "Perform Failover" } ], "Default": "SNSPublishGatewayDisabledDefault" }, "SNSPublishGatewayDisabledDefault": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "Parameters": { "Subject": "Update from Failover State machine", "Message": "API Gateway on SPAN is not accessible. Cannot proceed with failover!", "TopicArn": "${NotifyAdminTopic}" }, "Next": "SPAN Gateway is down!" }, "SPAN Gateway is down!": { "Type": "Fail", "Error": "Please check the logs", "Cause": "Cannot proceed failover without SPAN Gateway" }, "Check Failover Successful": { "Type": "Task", "Resource": "${CheckFailoverSuccessfulLambda.Arn}", "Next": "Choice - Is Failover successful?", "InputPath": "$", "OutputPath": "$", "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "Wait for Promotion" } ] }, "Wait for Promotion": { "Type": "Wait", "Next": "Check Failover Successful", "Seconds": 180 }, "Choice - Is Failover successful?": { "Type": "Choice", "Choices": [ { "Variable": "$.FailoverStatus", "StringEquals": "success", "Next": "SNSPublishFailoverSuccessful" }, { "And": [ { "Variable": "$.FailoverStatus", "StringEquals": "pending" }, { "Variable": "$.failover_retries", "StringEquals": "1" } ], "Next": "Wait for Promotion" }, { "Variable": "$.FailoverStatus", "StringEquals": "failure", "Next": "SNSPublishFailoverUnsuccessful" } ], "Default": "Wait for Promotion", "InputPath": "$" }, "SNSPublishFailoverUnsuccessful": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "Parameters": { "Subject": "Cisco ISE: Update from Failover State machine", "Message": "Failover is not successful! Please check the logs.", "TopicArn": "${NotifyAdminTopic}" }, "Next": "Failure is unsuccessful" }, "SNSPublishFailoverSuccessful": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "Parameters": { "Subject": "Cisco ISE: Update from Failover State machine", "Message": "Failover is successful!", "TopicArn": "${NotifyAdminTopic}" }, "Next": "Failover Successful" }, "Failure is unsuccessful": { "Type": "Fail", "Error": "Please check the new PPAN for more information" }, "Failover Successful": { "Type": "Succeed" }, "PPAN is healthy. Failover not required!": { "Type": "Succeed" } } } CheckPPANStatusLambda: Type: AWS::Lambda::Function Properties: Description: Monitors the health status of PPAN node Timeout: 300 Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os import urllib3 urllib3.disable_warnings() logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) def handler(event, context): def get_ssm_parameter(ssm_client, ssm_parameter_name, WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def set_ssm_parameter(ssm_client, ssm_parameter_name, value, Overwrite=True, Type="String"): response = ssm_client.put_parameter( Name=ssm_parameter_name, Value=value, Overwrite=True, Type=Type ) return response def send_sns_publish(subject, message): client = boto3.client('sns') TopicArn = [item["TopicArn"] for item in client.list_topics()['Topics'] if "NotifyAdminTopic" in item["TopicArn"]] logger.info("Sending SNS publish..") response = client.publish( Subject= subject, Message= message, TopicArn= TopicArn[0] ) runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) try: logger.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") SyncStatus = get_ssm_parameter(ssm_client,"SyncStatus") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} retries = 5 logger.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logger.info("Secondary Polcy Administration node ip : {}".format(Secondary_IP)) logger.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) #logger.info("API_AUTH : {}".format(API_AUTH)) logger.info("API_HEADER : {}".format(API_HEADER)) logger.info("Retries : {}".format(retries)) data = {} url = 'https://{}/api/v1/deployment/node'.format(Primary_IP) try: resp = requests.get(url, headers=API_HEADER, auth=API_AUTH, data=json.dumps(data), verify=False) logger.info("API response for {} is {} ".format(Primary_IP, resp.text)) parse_resp = json.loads(resp.text) hosts = parse_resp['response'] if resp.status_code == 200: for item in hosts: if "hostname" in item.keys(): logger.info("ISE - {} is up and running".format(Primary_IP)) logger.info("Promotion not required") return { "PrimaryState": "running", "retries": "0" } elif resp.status_code == 401: logger.info("Username/Password is incorrect. Please check the ISE credentials in SSM Parameter Store.") send_sns_publish(subject="Cisco ISE [{}]: Deployment health check notification".format(runtime_region), message = "Admin Username/Password credentials are incorrect.") raise RuntimeError('Admin Username/Password credentials are incorrect') else: raise RuntimeError('Failed to get successful response from PPAN') except Exception as e: logger.error("Exception occured while executing get node details api for {}".format(Primary_IP)) logger.info("Promoting SPAN - {} to PPAN".format(Secondary_IP)) logging.error('Exception: %s' % e, exc_info=True) raise Exception('Failed to get successful response from PPAN') except Exception as e: logging.error('Exception: %s' % e, exc_info=True) raise Exception('Failed to get successful response from PPAN') Handler: index.handler Role: !GetAtt CheckPPANStatusExecutionRole.Arn Runtime: python3.9 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer CheckPPANStatusExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* CheckSPANGatewayStatusLambda: Type: AWS::Lambda::Function Properties: Timeout: 300 Description: Checks whether SPAN has API Gateway enabled under Administration -> Settings -> API Settings Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os import urllib3 urllib3.disable_warnings() logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) def get_ssm_parameter(ssm_client, ssm_parameter_name,WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def set_ssm_parameter(ssm_client, ssm_parameter_name, value, Overwrite=True, Type="String"): response = ssm_client.put_parameter( Name=ssm_parameter_name, Value=value, Overwrite=True, Type=Type ) return response def send_sns_publish(subject, message): client = boto3.client('sns') TopicArn = [item["TopicArn"] for item in client.list_topics()['Topics'] if "NotifyAdminTopic" in item["TopicArn"]] logger.info("Sending SNS publish..") response = client.publish( Subject= subject, Message= message, TopicArn= TopicArn[0] ) def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') requests_data=json.dumps(data=dict(Status='FAILURE',Reason='Lambda timeout',UniqueId='ISENodeStates',Data='failed due to timeout')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) sys.exit(1) def handler(event, context): timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) try: logger.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") SyncStatus = get_ssm_parameter(ssm_client,"SyncStatus") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} logging.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logging.info("Secondary Polcy Administration node ip : {}".format(Secondary_IP)) logging.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) #logging.info("API_AUTH : {}".format(API_AUTH)) logging.info("API_HEADER : {}".format(API_HEADER)) logger.info("SyncStatus : {}".format(SyncStatus)) data = {} url = 'https://{}/api/v1/deployment/node'.format(Secondary_IP) try: resp = requests.get(url, headers=API_HEADER, auth=API_AUTH, data=json.dumps(data), verify=False) # logging.info("API response for {} is {} ".format(Secondary_IP, resp.text)) parse_resp = json.loads(resp.text) hosts = parse_resp['response'] if resp.status_code == 200: for item in hosts: if "hostname" in item.keys(): logging.info("SPAN - {} has API gateway enabled".format(Secondary_IP)) timer.cancel() return "running" else: raise RuntimeError('Failed to get successful response from SPAN') except Exception as e: logger.error("Exception occured while executing get node details api for {}".format(Secondary_IP)) logger.error("API Gateway is not enabled".format(Secondary_IP)) logging.error('Exception: %s' % e, exc_info=True) timer.cancel() return "GatewayDown" except Exception as e: timer.cancel() logging.error('Exception: %s' % e, exc_info=True) Handler: index.handler Role: !GetAtt CheckSPANGatewayStatusExecutionRole.Arn Runtime: python3.9 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer CheckSPANGatewayStatusExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* PerformFailoverLambda: Type: AWS::Lambda::Function Properties: Description: Promote SPAN to PPAN Timeout: 180 Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os import urllib3 urllib3.disable_warnings() logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) def get_ssm_parameter(ssm_client, ssm_parameter_name,WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def set_ssm_parameter(ssm_client, ssm_parameter_name, value, Overwrite=True, Type="String"): response = ssm_client.put_parameter( Name=ssm_parameter_name, Value=value, Overwrite=True, Type=Type ) return response def send_sns_publish(subject, message): client = boto3.client('sns') TopicArn = [item["TopicArn"] for item in client.list_topics()['Topics'] if "NotifyAdminTopic" in item["TopicArn"]] logger.info("Sending SNS publish..") response = client.publish( Subject= subject, Message= message, TopicArn= TopicArn[0] ) def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') requests_data=json.dumps(data=dict(Status='FAILURE',Reason='Lambda timeout',UniqueId='ISENodeStates',Data='failed due to timeout')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) sys.exit(1) def handler(event, context): timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) try: logger.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") SyncStatus = get_ssm_parameter(ssm_client,"SyncStatus") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} logging.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logging.info("Secondary Polcy Administration node ip : {}".format(Secondary_IP)) logging.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) logging.info("API_AUTH : {}".format(API_AUTH)) logging.info("API_HEADER : {}".format(API_HEADER)) data = {} url = 'https://{}/api/v1/deployment/promote'.format(Secondary_IP) try: resp = requests.post(url, headers=API_HEADER, auth=API_AUTH, data=json.dumps(data), verify=False) logging.info("API response for {} is {} ".format(Secondary_IP, resp.text)) if resp.status_code == 200 and "success" in json.loads(resp.text): logging.info("SPAN to PPAN Promotion successful for - {}".format(Secondary_IP)) timer.cancel() return "running" else: raise RuntimeError('Failed to get promote SPAN to PPAN') except Exception as e: logging.error("Exception occured while promoting SPAN {}".format(Secondary_IP)) logging.error('Exception: %s' % e, exc_info=True) timer.cancel() return "down" except Exception as e: timer.cancel() logging.error('Exception: %s' % e, exc_info=True) Handler: index.handler Role: !GetAtt PerformFailoverExecutionRole.Arn Runtime: python3.9 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer PerformFailoverExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSNSFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* CheckFailoverSuccessfulLambda: Type: AWS::Lambda::Function Properties: Description: Check status of new PPAN and update SSM parameter store with updated PPAN/SPAN IP/FQDN details Timeout: 300 Code: ZipFile: !Sub | import json import logging import threading import time import requests import boto3 import sys import os import urllib3 urllib3.disable_warnings() logging.basicConfig(stream = sys.stdout) logger = logging.getLogger() logger.setLevel(logging.INFO) def get_ssm_parameter(ssm_client, ssm_parameter_name,WithDecryption=False): param_value = ssm_client.get_parameter( Name=ssm_parameter_name, WithDecryption=WithDecryption ) return param_value.get('Parameter').get('Value') def set_ssm_parameter(ssm_client, ssm_parameter_name, value, Overwrite=True, Type="String"): response = ssm_client.put_parameter( Name=ssm_parameter_name, Value=value, Overwrite=True, Type=Type ) return response def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') requests_data=json.dumps(data=dict(Status='FAILURE',Reason='Lambda timeout',UniqueId='ISENodeStates',Data='failed due to timeout')).encode('utf-8') response = requests.put(event['ResourceProperties']['WaitHandle'], data=requests_data, headers={'Content-Type':''}) sys.exit(1) def handler(event, context): timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) runtime_region = os.environ['AWS_REGION'] ssm_client = boto3.client('ssm',region_name=runtime_region) try: logging.info("#Retriving SSM parameters...") Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") Primary_FQDN = get_ssm_parameter(ssm_client,"Primary_FQDN") Secondary_FQDN = get_ssm_parameter(ssm_client,"Secondary_FQDN") SyncStatus = get_ssm_parameter(ssm_client,"SyncStatus") ADMIN_USERNAME = get_ssm_parameter(ssm_client,"ADMIN_USERNAME") ADMIN_PASSWORD = get_ssm_parameter(ssm_client,"ADMIN_PASSWORD",WithDecryption=True) API_AUTH = (ADMIN_USERNAME, ADMIN_PASSWORD) API_HEADER = {'Content-Type': 'application/json', 'Accept': 'application/json'} retries = 15 logging.info("Primmary Polcy Administration node ip : {}".format(Primary_IP)) logging.info("Secondary Polcy Administration node ip : {}".format(Secondary_IP)) logging.info("ADMIN_USERNAME : {}".format(ADMIN_USERNAME)) #logging.info("API_AUTH : {}".format(API_AUTH)) logging.info("API_HEADER : {}".format(API_HEADER)) data = {} url = 'https://{}/api/v1/deployment/node'.format(Secondary_IP) try: resp = requests.get(url, headers=API_HEADER, auth=API_AUTH, data=json.dumps(data), verify=False) logging.info("API response for {} is {} ".format(Secondary_IP, resp.text)) parse_resp = json.loads(resp.text) hosts = parse_resp['response'] if resp.status_code == 200: for item in hosts: if "hostname" in item.keys(): logging.info("Promotion is successful for {}".format(Secondary_IP)) timer.cancel() logging.info("Updating SSM parameters for SPAN and PPAN") logging.info("Old PPAN = {} , Old SPAN = {}".format(Primary_IP, Secondary_IP)) set_ssm_parameter(ssm_client,"Primary_IP", Secondary_IP) set_ssm_parameter(ssm_client,"Secondary_IP", Primary_IP) set_ssm_parameter(ssm_client,"Primary_FQDN", Secondary_FQDN) set_ssm_parameter(ssm_client,"Secondary_FQDN", Primary_FQDN) Primary_IP = get_ssm_parameter(ssm_client,"Primary_IP") Secondary_IP = get_ssm_parameter(ssm_client,"Secondary_IP") Primary_FQDN = get_ssm_parameter(ssm_client,"Primary_FQDN") Secondary_FQDN = get_ssm_parameter(ssm_client,"Secondary_FQDN") logging.info("New PPAN = {} , SPAN = {}".format(Primary_IP, Secondary_IP)) logging.info("New Primary_FQDN = {} , Secondary_FQDN = {}".format(Primary_FQDN, Secondary_FQDN)) return { "FailoverStatus": "success", "failover_retries": str(retries) } else: raise RuntimeError('Failed to get node details from new PPAN') except Exception as e: print("Exception occured while executing get node details api for {}".format(Secondary_IP)) if "failover_retries" in event: retries = int(event['failover_retries']) if retries == 0: return { "FailoverStatus": "failure" } else: retries = int(event['failover_retries']) - 1 return { "FailoverStatus": "pending", "failover_retries": str(retries) } else: return { "FailoverStatus": "pending", "failover_retries": str(retries) } except Exception as e: # timer.cancel() logging.error('Exception: %s' % e, exc_info=True) Handler: index.handler Role: !GetAtt CheckFailoverSuccessfulExecutionRole.Arn Runtime: python3.9 VpcConfig: SecurityGroupIds: - !Ref ISESecurityGroup SubnetIds: - !Ref PrivateSubnet1A - !Ref PrivateSubnet1B Layers: - !Ref CiscoISELayer CheckFailoverSuccessfulExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2FullAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeInstance - ec2:CreateNetworkInterface Resource: !Sub arn:${AWS::Partition}:ec2:*:*:* Outputs: ISENode1ID: Description: 'InstanceID of the newly created ISE Node 1 ' Value: !Ref ISENode1 ISENode1PrivateDnsName: Description: Private DNSName of the newly created ISE Node 1 Value: !GetAtt - ISENode1 - PrivateDnsName ISENode1PrivateIp: Description: Private IP address of the newly created ISE Node 1 Value: !GetAtt - ISENode1 - PrivateIp ISENode2ID: Description: 'InstanceID of the newly created ISE Node 2 ' Value: !Ref ISENode2 ISENode2PrivateDnsName: Description: Private DNSName of the newly created ISE Node 2 Value: !GetAtt - ISENode2 - PrivateDnsName ISENode2PrivateIp: Description: Private IP address of the newly created ISE Node 2 Value: !GetAtt - ISENode2 - PrivateIp