|
| 1 | +#!/bin/python3 |
| 2 | +# |
| 3 | +# This script takes an FSx for ONTAP file system ID as input and generates |
| 4 | +# a CloudFormation template for the file system, its volumes, and its |
| 5 | +# storage virtual machines. The output is printed to the console in JSON format. |
| 6 | +################################################################################ |
| 7 | + |
| 8 | +import json |
| 9 | +import boto3 |
| 10 | +import sys |
| 11 | +import optparse |
| 12 | +# |
| 13 | +# Get the file system ID from the command line |
| 14 | +parser = optparse.OptionParser() |
| 15 | +parser.add_option('-f', dest='filesystemId', help='The ID of the FSx for ONTAP file system to generate the CloudFormation template for.') |
| 16 | +parser.add_option('-n', dest='nameAppend', help='A string to append to the names of the resources in the CloudFormation template to make them unique. This is optional.') |
| 17 | +opts, args = parser.parse_args() |
| 18 | + |
| 19 | +if opts.filesystemId is None: |
| 20 | + print("Error: -f option is required", file=sys.stderr) |
| 21 | + sys.exit(1) |
| 22 | +filesystemId = opts.filesystemId |
| 23 | +nameAppend = opts.nameAppend if opts.nameAppend is not None else "" |
| 24 | +# |
| 25 | +# Create boto3 client for fsx and ec2. |
| 26 | +fsxClient = boto3.client('fsx') |
| 27 | +ec2Client = boto3.client('ec2') |
| 28 | +# |
| 29 | +# Get the file system details |
| 30 | +response = fsxClient.describe_file_systems(FileSystemIds=[filesystemId]) |
| 31 | +if response.get('FileSystems') is None or len(response['FileSystems']) == 0: |
| 32 | + print(f"No file system found with ID {filesystemId}", file=sys.stderr) |
| 33 | + sys.exit(1) |
| 34 | +# |
| 35 | +# Build the CloudFormation template for the file system |
| 36 | +cfTemplate = { |
| 37 | + "Description": f"FSx File System template for {filesystemId}.", |
| 38 | + "Resources": {} |
| 39 | +} |
| 40 | +cfTemplate['Parameters'] = { |
| 41 | + "fsxadminPassword": { |
| 42 | + "Type": "String", |
| 43 | + "Description": "The AWS Secrets Manager secret that has the password for the fsxadmin user. It should have a key named 'password' that contains the password." |
| 44 | + } |
| 45 | +} |
| 46 | +fileSystem = response['FileSystems'][0] |
| 47 | +fsCfTemplate = {} |
| 48 | +fsCfTemplate['Type'] = 'AWS::FSx::FileSystem' |
| 49 | +fsCfTemplate['Properties'] = {} |
| 50 | +for prop in ['NetworkType', 'FileSystemType', 'KmsKeyId', 'StorageCapacity', 'SubnetIds', 'StorageType', 'Tags']: |
| 51 | + if prop in fileSystem: |
| 52 | + fsCfTemplate['Properties'][prop] = fileSystem[prop] |
| 53 | +# |
| 54 | +# Get the security groups from the ENIs. |
| 55 | +fsCfTemplate['Properties']['SecurityGroupIds'] = [] |
| 56 | +securityGroups = {} # Use a dictionary to store the security groups to avoid duplicates. |
| 57 | +response = ec2Client.describe_network_interfaces(NetworkInterfaceIds=fileSystem['NetworkInterfaceIds']) |
| 58 | +for eni in response['NetworkInterfaces']: |
| 59 | + for group in eni['Groups']: |
| 60 | + securityGroups[group['GroupId']] = 1 |
| 61 | +for group in securityGroups.keys(): |
| 62 | + fsCfTemplate['Properties']['SecurityGroupIds'].append(group) |
| 63 | +# |
| 64 | +# Copy the ONTAP configuration. |
| 65 | +fsCfTemplate['Properties']['OntapConfiguration'] = {"FsxAdminPassword": {"Fn::Sub": "{{resolve:secretsmanager:${fsxadminPassword}:SecretString:password}}"}} |
| 66 | +for prop in ['AutomaticBackupRetentionDays', 'DailyAutomaticBackupStartTime', 'DeploymentType', |
| 67 | + 'EndpointIpAddressRange', 'EndpointIpv6AddressRange', 'PreferredSubnetId', 'RouteTableIds', |
| 68 | + 'WeeklyMaintenanceStartTime', 'HAPairs', 'ThroughputCapacityPerHAPair']: |
| 69 | + if prop in fileSystem['OntapConfiguration']: |
| 70 | + fsCfTemplate['Properties']['OntapConfiguration'][prop] = fileSystem['OntapConfiguration'][prop] |
| 71 | +if fileSystem['OntapConfiguration']['DiskIopsConfiguration']['Mode'] == 'AUTOMATIC': |
| 72 | + fsCfTemplate['Properties']['OntapConfiguration']['DiskIopsConfiguration'] = {'Mode': 'AUTOMATIC'} |
| 73 | +else: |
| 74 | + fsCfTemplate['Properties']['OntapConfiguration']['DiskIopsConfiguration'] = fileSystem['OntapConfiguration']['DiskIopsConfiguration'] |
| 75 | +# |
| 76 | +# If using the default endpoint IP address range, remove it from the |
| 77 | +# CloudFormation template since AWS will automatically use a new default |
| 78 | +# address range if the 'EndpointIpAddressRange' is not specified. |
| 79 | +if 'EndpointIpAddressRange' in fsCfTemplate['Properties']['OntapConfiguration']: |
| 80 | + if fsCfTemplate['Properties']['OntapConfiguration']['EndpointIpAddressRange'].startswith("198.19"): |
| 81 | + del fsCfTemplate['Properties']['OntapConfiguration']['EndpointIpAddressRange'] |
| 82 | + else: |
| 83 | + cfTemplate['Parameters']['endpointIpAddressRange'] = { |
| 84 | + "Type": "String", |
| 85 | + "Description": "The IP address range to use for the file system's endpoints.", |
| 86 | + "Default": fsCfTemplate['Properties']['OntapConfiguration']['EndpointIpAddressRange'] |
| 87 | + } |
| 88 | + fsCfTemplate['Properties']['OntapConfiguration']['EndpointIpAddressRange'] = {"Ref": "endpointIpAddressRange"} |
| 89 | + |
| 90 | +cfTemplate['Resources'].update({filesystemId.replace("-", ""): fsCfTemplate}) |
| 91 | +# |
| 92 | +# Get all the volumes for the file system. Getting the volumes before the SVMs |
| 93 | +# since I need the list of volumes to get the security style of the root volume |
| 94 | +# for each SVMs. |
| 95 | +response = fsxClient.describe_volumes(Filters=[{'Name': 'file-system-id', 'Values': [filesystemId]}]) |
| 96 | +volumes = response['Volumes'] |
| 97 | +for volume in volumes: |
| 98 | + if volume['OntapConfiguration']['StorageVirtualMachineRoot']: |
| 99 | + continue |
| 100 | + volumeCfTemplate = {} |
| 101 | + volumeCfTemplate['Type'] = 'AWS::FSx::Volume' |
| 102 | + volumeCfTemplate['Properties'] = {} |
| 103 | + for property in ['Name', 'VolumeType', 'Tags']: |
| 104 | + if property in volume: |
| 105 | + volumeCfTemplate['Properties'][property] = volume[property] |
| 106 | + volumeCfTemplate['Properties']['Name'] = volumeCfTemplate['Properties']['Name'] + nameAppend |
| 107 | + |
| 108 | + volumeCfTemplate['Properties']['OntapConfiguration'] = {} |
| 109 | + for property in ['AggregateName', 'CopyTagsToBackups', 'OntapVolumeType', 'SecurityStyle', 'SizeInMegabytes', |
| 110 | + 'StorageEfficiencyEnabled', 'VolumeStyle', 'JunctionPath', |
| 111 | + 'SnapshotPolicy', 'TieringPolicy', 'SnaplockConfiguration']: |
| 112 | + if property in volume['OntapConfiguration']: |
| 113 | + volumeCfTemplate['Properties']['OntapConfiguration'][property] = volume['OntapConfiguration'][property] |
| 114 | + volumeCfTemplate['Properties']['OntapConfiguration']['StorageVirtualMachineId'] = {"Ref" : volume['OntapConfiguration']['StorageVirtualMachineId'].replace("-", "")} |
| 115 | + # |
| 116 | + # DP volumes can't have JunctionPath, StorageEfficiency, SnapshotPolicy or SecurityStyle properties |
| 117 | + if volume['OntapConfiguration']['OntapVolumeType'] == 'DP': |
| 118 | + for prop in ['JunctionPath', 'StorageEfficiencyEnabled', 'SnapshotPolicy', 'SecurityStyle']: |
| 119 | + if prop in volumeCfTemplate['Properties']['OntapConfiguration']: |
| 120 | + print(f"Warning: Volume {volume['Name']} is a DP volume and cannot have the {prop} property, removing it from the CloudFormation template.", file=sys.stderr) |
| 121 | + del volumeCfTemplate['Properties']['OntapConfiguration'][prop] |
| 122 | + else: |
| 123 | + if 'JunctionPath' not in volumeCfTemplate['Properties']['OntapConfiguration']: |
| 124 | + print(f"Warning: Volume {volume['Name']} does not have a junction path yet it is required for a CloudFormation template so setting it to /{volumeCfTemplate['Properties']['Name']}", file=sys.stderr) |
| 125 | + volumeCfTemplate['Properties']['OntapConfiguration']['JunctionPath'] = "/" + volumeCfTemplate['Properties']['Name'] |
| 126 | + |
| 127 | + cfTemplate['Resources'].update({volume['VolumeId'].replace("-", ""): volumeCfTemplate}) |
| 128 | +# |
| 129 | +# Get all the storage virtual machines for the file system. |
| 130 | +response = fsxClient.describe_storage_virtual_machines(Filters=[{'Name': 'file-system-id', 'Values': [filesystemId]}]) |
| 131 | +for svm in response['StorageVirtualMachines']: |
| 132 | + svmCfTemplate = {} |
| 133 | + svmCfTemplate['Type'] = 'AWS::FSx::StorageVirtualMachine' |
| 134 | + svmCfTemplate['Properties'] = {"FileSystemId": {"Ref" : filesystemId.replace("-", "")}} |
| 135 | + for prop in ['ActiveDirectoryConfiguration', 'Name', 'RootVolumeSecurityStyle', 'Tags']: |
| 136 | + if prop in svm: |
| 137 | + svmCfTemplate['Properties'][prop] = svm[prop] |
| 138 | + svmCfTemplate['Properties']['Name'] = svmCfTemplate['Properties']['Name'] + nameAppend |
| 139 | + |
| 140 | + if 'ActiveDirectoryConfiguration' in svm: |
| 141 | + if len(svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['NetBiosName']) > 10 and len(nameAppend) > 0: |
| 142 | + svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['NetBiosName'] = svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['NetBiosName'][:10] + nameAppend.upper() |
| 143 | + else: |
| 144 | + svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['NetBiosName'] = svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['NetBiosName'] + nameAppend.upper() |
| 145 | + svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['NetBiosName'] = svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['NetBiosName'][:15] |
| 146 | + if 'SelfManagedActiveDirectoryConfiguration' in svm['ActiveDirectoryConfiguration']: |
| 147 | + if 'OrganizationalUnitDistinguishedName' in svm['ActiveDirectoryConfiguration']['SelfManagedActiveDirectoryConfiguration']: |
| 148 | + # |
| 149 | + # Since CF can only handle organizational unit distinguish names that have a |
| 150 | + # parent of OU, we need to check if the parent of the organizational unit is |
| 151 | + # OU and if not, we need to remove the organizational unit distinguish name (DN) |
| 152 | + # from the CloudFormation template and print a warning message. |
| 153 | + dnParent=svm['ActiveDirectoryConfiguration']['SelfManagedActiveDirectoryConfiguration']['OrganizationalUnitDistinguishedName'].split(",")[0] |
| 154 | + dnParent = dnParent.split("=")[0] |
| 155 | + if dnParent != "OU": |
| 156 | + # |
| 157 | + # The default value from ONTAP is 'CN=Computers' which does not have a |
| 158 | + # parent of OU, but CF requires that the parent is OU, therefore we will |
| 159 | + # just ignore the organizational unit distinguish name. |
| 160 | + if svm['ActiveDirectoryConfiguration']['SelfManagedActiveDirectoryConfiguration']['OrganizationalUnitDistinguishedName'] != "CN=Computers": |
| 161 | + print(f'Warning: The organizational unit distinguish name for the SVM {svm["Name"]} is "{svm["ActiveDirectoryConfiguration"]["SelfManagedActiveDirectoryConfiguration"]["OrganizationalUnitDistinguishedName"]}" which does not have a parent of a OU and CloudFormation requires that, therefore the distinguished name is ignored. This will cause the SVM to be put into the "default" computer location', file=sys.stderr) |
| 162 | + del svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['SelfManagedActiveDirectoryConfiguration']['OrganizationalUnitDistinguishedName'] |
| 163 | + |
| 164 | + secretParameterId = f'{svm["Name"].replace("-", "").replace("_", "")}AdminCredentials' |
| 165 | + cfTemplate['Parameters'][secretParameterId] = { |
| 166 | + "Type": "String", |
| 167 | + "Description": f"The AWS Secrets Manager secret that has the Active Directory credentials for the {svm['Name']} storage virtual machine. It should have two keys named 'username' and 'password'." |
| 168 | + } |
| 169 | + svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['SelfManagedActiveDirectoryConfiguration']['UserName'] = {"Fn::Sub": "{{resolve:secretsmanager:${" + secretParameterId + "}:SecretString:username}}"} |
| 170 | + svmCfTemplate['Properties']['ActiveDirectoryConfiguration']['SelfManagedActiveDirectoryConfiguration']['Password'] = {"Fn::Sub": "{{resolve:secretsmanager:${" + secretParameterId + "}:SecretString:password}}"} |
| 171 | + # |
| 172 | + # Get the security style for the SVM's root volume. Assume the root volume is <svm_name>_root |
| 173 | + for volume in volumes: |
| 174 | + if volume['OntapConfiguration']['StorageVirtualMachineId'] == svm['StorageVirtualMachineId'] and volume['OntapConfiguration']['StorageVirtualMachineRoot']: |
| 175 | + svmCfTemplate['Properties']['RootVolumeSecurityStyle'] = volume['OntapConfiguration']['SecurityStyle'] |
| 176 | + break |
| 177 | + if svmCfTemplate['Properties'].get('RootVolumeSecurityStyle') is None: |
| 178 | + if 'ActiveDirectoryConfiguration' in svmCfTemplate['Properties']: |
| 179 | + svmCfTemplate['Properties']['RootVolumeSecurityStyle'] = 'NTFS' |
| 180 | + else: |
| 181 | + svmCfTemplate['Properties']['RootVolumeSecurityStyle'] = 'UNIX' |
| 182 | + print(f"Warning: Could not find root volume for SVM {svm['Name']}. Setting the security style to {svmCfTemplate['Properties']['RootVolumeSecurityStyle']}.", file=sys.stderr) |
| 183 | + cfTemplate['Resources'].update({svm['StorageVirtualMachineId'].replace("-", ""): svmCfTemplate}) |
| 184 | + |
| 185 | +cfTemplate['Outputs'] = { |
| 186 | + "FileSystemId": { |
| 187 | + "Description": "The ID of the FSx for ONTAP file system.", |
| 188 | + "Value": {"Ref": filesystemId.replace("-", "")} |
| 189 | + } |
| 190 | +} |
| 191 | +# Print the CloudFormation template in JSON format |
| 192 | +print(json.dumps(cfTemplate, indent=4)) |
0 commit comments