├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.txt ├── README.md ├── deployment ├── client_vpn.yaml ├── content-creation-workstation.template ├── outputs │ └── vfx_packaged.yaml ├── parameters │ └── test-param.json ├── vfxhost.yaml └── vpc.yaml ├── documentation ├── Content-Creation-Workstation-Implementation-Guide.pdf └── images │ ├── Default_Architectural_Diagram.jpg │ └── Default_Architectural_Diagram.jpg.license └── source ├── crhelper-2.0.6.dist-info ├── INSTALLER ├── LICENSE ├── METADATA ├── NOTICE ├── RECORD ├── WHEEL └── top_level.txt ├── crhelper ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-37.pyc │ ├── log_helper.cpython-37.pyc │ ├── resource_helper.cpython-37.pyc │ └── utils.cpython-37.pyc ├── log_helper.py ├── resource_helper.py └── utils.py ├── lambda_function.py └── tests ├── __init__.py ├── __pycache__ ├── __init__.cpython-37.pyc ├── test_log_helper.cpython-37.pyc ├── test_resource_helper.cpython-37.pyc └── test_utils.cpython-37.pyc ├── test_log_helper.py ├── test_resource_helper.py ├── test_utils.py └── unit ├── __init__.py └── __pycache__ └── __init__.cpython-37.pyc /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog for Content Creation Workstation 2 | 3 | ## [0.1.7] - 2020-08-14 4 | 5 | ### Added 6 | 7 | - Added: AWS Solution project files and modified directory structure as recommended. 8 | 9 | ## [0.1.6] - 2020-07-21 10 | 11 | ### Added 12 | 13 | - Added: VPC Flow Logs Option 14 | - Added: Feature to have ability to enable termination protection on EC2 instance. 15 | - Added: Feature to create S3 bucket for file storage and sync it with VFX Host. 16 | - Added: Windows now has AWS CLI Downloaded onto the instance. 17 | - Added: IAM role for EC2 instance to have ability to talk to the S3 bucket. (least privilege). 18 | 19 | ### Removed 20 | 21 | - Removed: FTP Server functionality. 22 | 23 | ## [0.1.5] - 2020-06-11 24 | 25 | ### Fixed/Updated 26 | 27 | - Fixed: Client VPN Bug: Client VPN Route error when creating new Client VPN endpoint on a new VPC. 28 | - Fixed: Blender Installation Bug: Blender Installation failure when downloading Blender 2.82. Updated to use chocolatey for download. 29 | - Updated: "CreateVPNEndpoint" parameter to only allow values 'true' and 'false' 30 | - Updated: cfn-init to leverage Powershell file for the cfn-init process. 31 | - Updated: Reference architecture to reflect current installation progress. 32 | - Updated: Updated README.md 33 | 34 | ### Added 35 | 36 | - Added: Scripts to install Wacom drivers on Windows VFX host. 37 | 38 | ### Removed 39 | 40 | - Removed: Unneeded Architecture Diagram Pictures. 41 | 42 | ## [0.1.4] - 2020-06-05 43 | 44 | ### Added 45 | 46 | - Added: Name to VFX Host 47 | - Added: Description to Security Group inbound rules. 48 | 49 | ## [0.1.3] - 2020-05-30 50 | 51 | ### Added 52 | 53 | - Added: FTP Server to Windows VFX Host 54 | 55 | ## [0.1.2] - 2020-05-29 56 | 57 | ### Added 58 | 59 | - Added: FTP Server to Linux VFX Host 60 | 61 | ## [0.1.1] - 2020-05-19 62 | 63 | ### Added 64 | 65 | - Added:Reference Architecture. 66 | 67 | ### Removed 68 | 69 | - Added: FSx Lustre Resources. 70 | 71 | ## [0.1.0] - 2020-05-15 72 | 73 | ### Added 74 | 75 | - Initial solution for deploying Teradici Cloud Access on CentOS or Windows 2019 EC2 instances. 76 | - Architectural Diagrams in README 77 | - Client VPN Endpoint Resources 78 | - FSx for Lustre Resrouces 79 | - Additional Security Group, IAM Role and other AWS Resources. 80 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check [existing open](https://github.com/awslabs/content-creation-workstation/issues), or [recently closed](https://github.com/awslabs/content-creation-workstation/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 14 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 15 | 16 | * A reproducible test case or series of steps 17 | * The version of our code being used 18 | * Any modifications you've made relevant to the bug 19 | * Anything unusual about your environment or deployment 20 | 21 | ## Contributing via Pull Requests 22 | 23 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 24 | 25 | 1. You are working against the latest source on the *master* branch. 26 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 27 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 28 | 29 | To send us a pull request, please: 30 | 31 | 1. Fork the repository. 32 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 33 | 3. Ensure local tests pass. 34 | 4. Commit to your fork using clear commit messages. 35 | 5. Send us a pull request, answering any default questions in the pull request interface. 36 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 37 | 38 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 39 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 40 | 41 | ## Finding contributions to work on 42 | 43 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/content-creation-workstation/labels/help%20wanted) issues is a great place to start. 44 | 45 | ## Code of Conduct 46 | 47 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 48 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 49 | opensource-codeofconduct@amazon.com with any additional questions or comments. 50 | 51 | ## Security issue notifications 52 | 53 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 54 | 55 | ## Licensing 56 | 57 | See the [LICENSE](https://github.com/awslabs/content-creation-workstation/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 58 | 59 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Content Creation Workstation AWS Sample 2 | 3 | Based off the [Creating a Virtual Workstation on AWS](https://studio-in-the-cloud-tutorials.s3-us-west-1.amazonaws.com/streaming-workstation/Creating+a+Virtual+Workstation+on+AWS.pdf), the following CloudFormation template has been created for deploying a Teradici Cloud Access Software on either CentOS 7 or Windows Server 2019 GPU-Enabled EC2 instances. The CloudFormation also provides the ability to provision Client VPN Endpoint if elected during deployment. 4 | 5 | Below are architecture diagrams of some deployable configurations. These are not all configurations as the solutions offers ability to chose VPC deployment location (new or existing), subnet (public or private), and Client VPN deployment(true or false). This results in 8 possible deployment combinations. 6 | 7 | ## Content Creation Workstation deployed in public subnet in a new VPC 8 | 9 | ![architecture diagram](documentation/images/Default_Architectural_Diagram.jpg "Diagram of default deployment") 10 | 11 | ## Notes 12 | 13 | Teradici CloudAccess Software is accessible using Marketplace AMI which costs $0.50/hr in addition to EC2 computing costs. EBS volumes are encrypted using the default `aws/ebs` KMS key. 14 | 15 | ## Prerequisites 16 | 17 | Please note that the template will be expecting your environment to be configured in the following way prior to running the template. 18 | 19 | 1. An EC2 Keypair must be created. Click [here](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html) to learn how to create an EC2 Keypair. 20 | 2. If deploying to an existing VPC. Ensure that the VPC must have **DNS Hostnames** and **DNS Support** enabled. For further details click [here](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html). 21 | 3. If deploying a Client VPN endpoint. User must generate a server/client certificate and upload those certificates to AWS Certificate Manager. Click [here](https://docs.aws.amazon.com/vpn/latest/clientvpn-admin/authentication-authorization.html#mutual) to see instructions on how to create server/client certs. 22 | 23 | ## Deploying the Cloudformation Templates 24 | 25 | Please note that the below instructions is how to deploy using the AWS CLI. For more detailed instructions on how to deploy this template, reference the [deployment guide](documentation/Content-Creation-Workstation-Implementation-Guide.pdf). 26 | 27 | ### 1. Specify correct parameter values for the CloudFormation template 28 | 29 | Modify existing parameters file (deployment/parameters/test-param.json) or create your own. Values must be specified for all the following parameters: 30 | 31 | | Parameters | Details | Default Value | 32 | | --------------------------- |:---------------------------------------------|:-------------------| 33 | |OSType | Specify whether you want to run Teradici on Linux or Windows OS. Allowed values "linux" or "windows". | linux | 34 | |EBSVolumeSize | Volume size for the VFX Host, in GiB | 100 | 35 | |VFXHostInstanceType | Amazon EC2 instance type for the VFX workstations. | g4dn.xlarge | 36 | |KeyPairName | Name of AWS EC2 Key Pair. | No Default | 37 | |VFXHostAccessCIDR | CIDR Range that will access the VFX Host. Input your network's current public or private IP depending if the VFX is being placed in a public or private subnet | 10.64.0.0/16 | 38 | |VFXHostSubnetPlacement | Specify if VFX host should be placed in "Public" or "Private" subnet. | Public | 39 | |EnableDeleteProtection | Specify if VFX host should have delete protection enabled. | false | 40 | |InstallBlenderSoftware | Specify if VFX host should download and install Blender software. | true | 41 | |CreateS3StorageBucket | Specify if template should create an AWS S3 Bucket and connect the host to sync files between local system and S3 bucket. | true | 42 | |ExistingVPCID | If solution should deploy into an existing VPC, Specify existing VPC ID. | 'N/A' | 43 | |ExistingSubnetID | If solution should deply into an existing VPN, Specify subnet id in which the VFX Host should be placed in. |'N/A' | 44 | |VPCCIDR | If solution should create a new VPC, specify CIDR Block for the VPC | 10.64.0.0/16 | 45 | |PublicSubnet1CIDR | If solution should create a new VPC, specify CIDR Block for the public subnet 1 located in Availability Zone 2 | 10.64.32.0/20 | 46 | |PrivateSubnet1CIDR | If solution should create a new VPC, specify CIDR Block for the private subnet 1 located in Availability Zone 2 | 10.64.96.0/20 | 47 | |EnableVPCFlowLogs | Specify if newly created VPC should have VPC flow logs enabled. The CloudFormation template will create a new S3 bucket to store the logs. It will also capture ALL logs including ACCEPTS and REJECTS.| false | 48 | |CreateVPNEndpoint | Should the CloudFormation create a Client VPN Endpoint. It is recommended if VFX Host is placed in private subnet and there is no other provisions created to connect to private subnet.(Specify 'true' or 'false') | false | 49 | |ClientCidrBlock | If creating Client VPN endpoint in the solution, specify the IPv4 address range. It should be in CIDR notation from which to assign client IP addresses. The address range cannot overlap with the local CIDR of the VPC in which the associated subnet is located, or the routes that you add manually. | 10.50.0.0/20 | 50 | |ServerCertArn | If creating Client VPN endpoint in the solution, specify Server Cert Arn for VPN endpoint. | 'N/A' | 51 | |ClientCertificateArn | If creating Client VPN endpoint in the solution, specify Client Cert Arn for VPN endpoint. | 'N/A' | 52 | |TargetNetworkCidr | If creating Client VPN endpoint in the solution, specify the IPv4 address range, in CIDR notation, of the network for which access is being authorized. | 10.64.0.0/16 | 53 | 54 | ### 2. Prepare the CloudFormation template 55 | 56 | Due to the use of Nested Templates in this solution, the parent template must be packaged in order to properly reference the child template(s). More information regarding CloudFormation packaging can be found [here.](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-cli-package.html) 57 | 58 | Use the following command to package the template: 59 | 60 | ```bash 61 | aws cloudformation package --template-file ./deployment/content-creation-workstation.template --s3-bucket [desired s3 bucket for CF artifacts] --output-template-file ./deployment/outputs/vfx_packaged.yaml --region [AWS deployment region, should be the same region where S3 bucket is located.] 62 | ``` 63 | 64 | ### 3. Create CloudFormation stack 65 | 66 | Use the following command to create the CloudFormation Stack: 67 | 68 | ```bash 69 | aws cloudformation create-stack --template-body file://deployment/outputs/vfx_packaged.yaml --parameters file://deployment/parameters/test-param.json --stack-name [desired stack name] --region [AWS Deployment Region] --capabilities CAPABILITY_AUTO_EXPAND CAPABILITY_IAM 70 | ``` 71 | -------------------------------------------------------------------------------- /deployment/client_vpn.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | AWSTemplateFormatVersion: '2010-09-09' 5 | Description: 'This template provisions Client VPN Resources' 6 | Parameters: 7 | ClientCidrBlock: 8 | Description: The IPv4 address range, in CIDR notation, from which to assign client IP addresses. The address range cannot overlap with the local CIDR of the VPC in which the associated subnet is located, or the routes that you add manually. 9 | 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-9]|3[0-2]))$ 10 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-32 11 | Type: String 12 | ServerCertArn: 13 | Description: Specify Server Cert Arn for VPN endpoint. 14 | Type: String 15 | ClientCertificateArn: 16 | Description: Specify Client Cert Arn for VPN endpoint. 17 | Type: String 18 | SubnetID: 19 | Description: The ID of the subnet to associate with the Client VPN endpoint. 20 | Type: String 21 | TargetNetworkCidr: 22 | Description: The IPv4 address range, in CIDR notation, of the network for which access is being authorized. 23 | 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])(\/(0[0-9]|1[0-9]|2[0-9]|3[0-2]))$ 24 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/0-32 25 | Type: String 26 | VPC: 27 | Description: The ID of the VPC to associate with the Client VPN endpoint. 28 | Type: String 29 | #Conditions: 30 | 31 | Resources: 32 | ClientVpnEndpoint: 33 | Type: AWS::EC2::ClientVpnEndpoint 34 | Properties: 35 | AuthenticationOptions: 36 | - 37 | MutualAuthentication: 38 | ClientRootCertificateChainArn: !Ref ClientCertificateArn 39 | Type: "certificate-authentication" 40 | ClientCidrBlock: !Ref ClientCidrBlock 41 | ConnectionLogOptions: 42 | Enabled: false 43 | Description: "Client VPN to connect to VFX Host" 44 | ServerCertificateArn: !Ref ServerCertArn 45 | VpcId: !Ref VPC 46 | SecurityGroupIds: 47 | - !Ref VPNSecurityGroup 48 | Tags: 49 | - Key: Name 50 | Value: VFX-ClientVPN 51 | VpnEndpointTargetNetworkAssociation: 52 | Type: AWS::EC2::ClientVpnTargetNetworkAssociation 53 | Properties: 54 | ClientVpnEndpointId: !Ref ClientVpnEndpoint 55 | SubnetId: !Ref SubnetID 56 | VpnEndpointAuthorizationRule: 57 | Type: AWS::EC2::ClientVpnAuthorizationRule 58 | Properties: 59 | ClientVpnEndpointId: !Ref ClientVpnEndpoint 60 | AuthorizeAllGroups: true 61 | Description: Allow access to VFX Host subnet. 62 | TargetNetworkCidr: !Ref TargetNetworkCidr 63 | InternetAuthRule: 64 | Type: "AWS::EC2::ClientVpnAuthorizationRule" 65 | Properties: 66 | ClientVpnEndpointId: !Ref ClientVpnEndpoint 67 | AuthorizeAllGroups: true 68 | TargetNetworkCidr: "0.0.0.0/0" 69 | Description: "Allow access to internet" 70 | InternetRoute: 71 | Type: "AWS::EC2::ClientVpnRoute" 72 | DependsOn: VpnEndpointTargetNetworkAssociation 73 | Properties: 74 | ClientVpnEndpointId: !Ref ClientVpnEndpoint 75 | TargetVpcSubnetId: !Ref SubnetID 76 | DestinationCidrBlock: "0.0.0.0/0" 77 | Description: "Route to the internet" 78 | 79 | VPNSecurityGroup: 80 | Type: AWS::EC2::SecurityGroup 81 | Properties: 82 | GroupName: Security Group for VPN Clients 83 | GroupDescription: This security group is attached to the Client VPN Endpoint. 84 | VpcId: !Ref VPC 85 | VPNHostSecurityGroupIngress1: 86 | Type: AWS::EC2::SecurityGroupIngress 87 | Properties: 88 | GroupId: !Ref VPNSecurityGroup 89 | IpProtocol: -1 90 | SourceSecurityGroupId: !Ref VPNSecurityGroup 91 | Outputs: 92 | VPNSecurityGroupID: 93 | Description: VPN Security Group ID 94 | Value: !GetAtt VPNSecurityGroup.GroupId 95 | Export: 96 | Name: !Sub "${AWS::StackName}-vpn-security-group-id" -------------------------------------------------------------------------------- /deployment/content-creation-workstation.template: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | AWSTemplateFormatVersion: 2010-09-09 5 | Transform: 'AWS::Serverless-2016-10-31' 6 | Description: >- 7 | This is a quick start template that deploys the VFX Workstation along with other optional resources such as VPC and Client VPN. 8 | Metadata: 9 | 'AWS::CloudFormation::Interface': 10 | ParameterGroups: 11 | - Label: 12 | default: Amazon EC2 Configuration 13 | Parameters: 14 | - OSType 15 | - EBSVolumeSize 16 | - KeyPairName 17 | - VFXHostInstanceType 18 | - VFXHostAccessCIDR 19 | - VFXHostSubnetPlacement 20 | - EnableDeleteProtection 21 | - InstallBlenderSoftware 22 | - CreateS3StorageBucket 23 | - Label: 24 | default: Existing VPC Configuration 25 | Parameters: 26 | - ExistingVPCID 27 | - ExistingSubnetID 28 | - Label: 29 | default: New VPC Configuration 30 | Parameters: 31 | - VPCCIDR 32 | - PublicSubnet1CIDR 33 | - PrivateSubnet1CIDR 34 | - EnableVPCFlowLogs 35 | - Label: 36 | default: VPN Endpoint Configuration 37 | Parameters: 38 | - CreateVPNEndpoint 39 | - ClientCidrBlock 40 | - ServerCertArn 41 | - ClientCertificateArn 42 | - TargetNetworkCidr 43 | 44 | 45 | ParameterLabels: 46 | OSType: 47 | default: VFX Host Operating System 48 | EBSVolumeSize: 49 | default: EBS Volume size for EC2 instance 50 | VFXHostInstanceType: 51 | default: VFX Host Instance Type 52 | KeyPairName: 53 | default: Key Pair Name 54 | VFXHostAccessCIDR: 55 | default: VFX Host Access CIDR 56 | VFXHostSubnetPlacement: 57 | default: VFX Host subnet placement. 58 | EnableDeleteProtection: 59 | default: Enable Termination Protection 60 | InstallBlenderSoftware: 61 | default: Install Blender Software 62 | CreateS3StorageBucket: 63 | default: Creates S3 bucket to store files and then sync with workstation. 64 | ExistingVPCID: 65 | default: Existing VPC ID 66 | ExistingSubnetID: 67 | default: Existing Subnet ID 68 | VPCCIDR: 69 | default: VPC CIDR Range 70 | PublicSubnet1CIDR: 71 | default: CIDR Range for Public Subnet in new VPC 72 | PrivateSubnet1CIDR: 73 | default: CIDR Range for Private Subnet in new VPC 74 | EnableVPCFlowLogs: 75 | default: Enable VPC Flow Logs 76 | CreateVPNEndpoint: 77 | default: Create VPN Endpoint. 78 | ClientCidrBlock: 79 | default: Client CIDR for VPN Endpoint. 80 | ServerCertArn: 81 | default: Specify Server Cert Arn for VPN endpoint. 82 | ClientCertificateArn: 83 | default: Specify Client Cert Arn for VPN endpoint. 84 | TargetNetworkCidr: 85 | default: Target Network CIDR for VPN Endpoint. 86 | 87 | Parameters: 88 | ############################ 89 | ### VFX Host Parameters##### 90 | ############################ 91 | OSType: 92 | AllowedValues: 93 | - linux 94 | - windows 95 | Default: linux 96 | Description: Specify whether you want to run Teradici on Linux or Windows OS. 97 | Type: String 98 | EBSVolumeSize: 99 | Default: '100' 100 | Description: 'Volume size for the VFX Host, in GiB' 101 | MaxValue: '16000' 102 | MinValue: '100' 103 | Type: Number 104 | VFXHostInstanceType: 105 | AllowedValues: 106 | - g4dn.xlarge 107 | - g4dn.2xlarge 108 | - g4dn.4xlarge 109 | - g4dn.8xlarge 110 | - g4dn.12xlarge 111 | - g4dn.16xlarge 112 | Default: g4dn.xlarge 113 | Description: Amazon EC2 instance type for the VFX workstations 114 | Type: String 115 | KeyPairName: 116 | Description: >- 117 | Name of AWS EC2 Key Pair. 118 | Type: 'AWS::EC2::KeyPair::KeyName' 119 | VFXHostAccessCIDR: 120 | AllowedPattern: >- 121 | ^(([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-9]|3[0-2]))$ 122 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-32 123 | Default: '10.64.0.0/16' 124 | Description: CIDR Block from which the VFX Host will be accessible. 125 | Type: String 126 | VFXHostSubnetPlacement: 127 | AllowedValues: 128 | - Public 129 | - Private 130 | ConstraintDescription: Specify if VFX host should be placed in "Public" or "Private" subnet. 131 | Default: 'Public' 132 | Description: Specify if VFX host should be placed in "Public" or "Private" subnet. 133 | Type: String 134 | EnableDeleteProtection: 135 | AllowedValues: 136 | - "true" 137 | - "false" 138 | ConstraintDescription: Value must be either a true or false. 139 | Default: 'false' 140 | Description: Specify if VFX host should have delete protection enabled. 141 | Type: String 142 | InstallBlenderSoftware: 143 | AllowedValues: 144 | - "true" 145 | - "false" 146 | ConstraintDescription: Value must be either a true or false. 147 | Default: 'true' 148 | Description: Specify if VFX host should download and install Blender software. 149 | Type: String 150 | CreateS3StorageBucket: 151 | AllowedValues: 152 | - "true" 153 | - "false" 154 | ConstraintDescription: Value must be either a true or false. 155 | Default: 'true' 156 | Description: Specify if template should create an AWS S3 Bucket and connect the host to sync files between local system and S3 bucket. 157 | Type: String 158 | ############################ 159 | ## Existing VPC Parameters## 160 | ############################ 161 | ExistingVPCID: 162 | Default: 'N/A' 163 | Description: If solution should deploy into an existing VPC, Specify existing VPC ID. 164 | Type: String 165 | ExistingSubnetID: 166 | Default: 'N/A' 167 | Description: If solution should deply into an existing VPN, Specify subnet id in which the VFX Host should be placed in. 168 | Type: String 169 | ############################ 170 | ##### New VPC Parameters#### 171 | ############################ 172 | VPCCIDR: 173 | AllowedPattern: >- 174 | ^(([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]))$ 175 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 176 | Default: 10.64.0.0/16 177 | Description: If solution should create a new VPC, specify CIDR Block for the VPC 178 | Type: String 179 | PublicSubnet1CIDR: 180 | AllowedPattern: >- 181 | ^(([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]))$ 182 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 183 | Default: 10.64.32.0/20 184 | Description: If solution should create a new VPC, specify CIDR Block for the public subnet 1 located in Availability Zone 2 185 | Type: String 186 | PrivateSubnet1CIDR: 187 | AllowedPattern: >- 188 | ^(([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]))$ 189 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 190 | Default: 10.64.96.0/20 191 | Description: If solution should create a new VPC, specify CIDR Block for the private subnet 1 located in Availability Zone 2 192 | Type: String 193 | EnableVPCFlowLogs: 194 | AllowedValues: 195 | - "true" 196 | - "false" 197 | ConstraintDescription: Value must be either a true or false. 198 | Default: 'false' 199 | Description: Specify if newly created VPC should have VPC flow logs enabled. The CloudFormation template will create a new S3 bucket to store the logs. It will also capture ALL logs including ACCEPTS and REJECTS. 200 | Type: String 201 | ############################ 202 | ### VPN Parameters### 203 | ############################ 204 | CreateVPNEndpoint: 205 | ConstraintDescription: Must specify 'true' or 'false' 206 | AllowedValues: 207 | - 'false' 208 | - 'true' 209 | Default: 'false' 210 | Description: "Should the CloudFormation create a Client VPN Endpoint. It is recommended if VFX Host is placed in private subnet and there is no other provisions created to connect to private subnet.(Specify 'true' or 'false')" 211 | Type: String 212 | ClientCidrBlock: 213 | Description: If creating Client VPN endpoint in the solution, specify the IPv4 address range. It should be in CIDR notation from which to assign client IP addresses. The address range cannot overlap with the local CIDR of the VPC in which the associated subnet. 214 | 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-9]|3[0-2]))$ 215 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-32 216 | Default: 10.50.0.0/20 217 | Type: String 218 | ServerCertArn: 219 | Description: If creating Client VPN endpoint in the solution, specify Server Cert Arn for VPN endpoint. 220 | Default: 'N/A' 221 | Type: String 222 | ClientCertificateArn: 223 | Description: If creating Client VPN endpoint in the solution, specify Client Cert Arn for VPN endpoint. 224 | Default: 'N/A' 225 | Type: String 226 | TargetNetworkCidr: 227 | Description: If creating Client VPN endpoint in the solution, specify the IPv4 address range, in CIDR notation, of the network for which access is being authorized. 228 | 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])(\/(0[0-9]|1[0-9]|2[0-9]|3[0-2]))$ 229 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/0-32 230 | Default: '10.64.0.0/16' 231 | Type: String 232 | 233 | Conditions: 234 | 235 | PublicVFXHostPlacement: !Equals [ !Ref VFXHostSubnetPlacement, Public ] 236 | PrivateVFXHostPlacement: !Equals [ !Ref VFXHostSubnetPlacement, Private ] 237 | ExistingVPC: !Not [ !Equals [ 'N/A', !Ref ExistingVPCID ]] 238 | NoExistingVPC: !Equals [ 'N/A', !Ref ExistingVPCID ] 239 | CreateVPN: !Equals [ 'true', !Ref CreateVPNEndpoint ] 240 | 241 | Resources: 242 | 243 | ############################## 244 | ### VPC Resources ##### 245 | ############################## 246 | VPCStack: 247 | Type: 'AWS::CloudFormation::Stack' 248 | Condition: "NoExistingVPC" 249 | Properties: 250 | TemplateURL: ./vpc.yaml 251 | Parameters: 252 | PublicAZBSubnetBlock: !Ref PublicSubnet1CIDR 253 | PrivateAZBSubnetBlock: !Ref PrivateSubnet1CIDR 254 | VpcCidrParam: !Ref VPCCIDR 255 | EnableVPCFlowLogs: !Ref EnableVPCFlowLogs 256 | 257 | 258 | ############################## 259 | ### VPN Resources ##### 260 | ############################## 261 | VPNStack: 262 | Type: 'AWS::CloudFormation::Stack' 263 | Condition: "CreateVPN" 264 | Properties: 265 | TemplateURL: ./client_vpn.yaml 266 | Parameters: 267 | ClientCidrBlock: !Ref ClientCidrBlock 268 | ServerCertArn: !Ref ServerCertArn 269 | ClientCertificateArn: !Ref ClientCertificateArn 270 | SubnetID: !If [ExistingVPC, !Ref ExistingSubnetID, !GetAtt VPCStack.Outputs.PrivateAZBSubnetId] 271 | TargetNetworkCidr: !Ref TargetNetworkCidr 272 | VPC: !If [ExistingVPC, !Ref ExistingVPCID, !GetAtt VPCStack.Outputs.VpcId] 273 | 274 | 275 | ############################## 276 | ### VFX Host Resources ##### 277 | ############################## 278 | CloudVFXHostStack: 279 | Type: 'AWS::CloudFormation::Stack' 280 | Properties: 281 | TemplateURL: ./vfxhost.yaml 282 | Parameters: 283 | EBSVolumeSize: !Ref EBSVolumeSize 284 | VFXHostInstanceType: !Ref VFXHostInstanceType 285 | KeyPairName: !Ref KeyPairName 286 | SubnetID: !If [ExistingVPC, !Ref ExistingSubnetID, !If [PublicVFXHostPlacement, !GetAtt VPCStack.Outputs.PublicAZBSubnetId, !GetAtt VPCStack.Outputs.PrivateAZBSubnetId]] 287 | VPCID: !If [ExistingVPC, !Ref ExistingVPCID, !GetAtt VPCStack.Outputs.VpcId] 288 | VFXHostAccessCIDR: !Ref VFXHostAccessCIDR 289 | AMIID: !GetAtt AMIInfo.AMIID 290 | OSType: !Ref OSType 291 | VFXHostSubnetPlacement: !Ref VFXHostSubnetPlacement 292 | AdditionalSecurityGroupId: !If [CreateVPN,!GetAtt VPNStack.Outputs.VPNSecurityGroupID, !Ref 'AWS::NoValue'] 293 | EnableDeleteProtection: !Ref EnableDeleteProtection 294 | InstallBlenderSoftware: !Ref InstallBlenderSoftware 295 | CreateS3StorageBucket: !Ref CreateS3StorageBucket 296 | 297 | ############################## 298 | ### AMI Lookup Resources ##### 299 | ############################## 300 | AMILookupLambda: 301 | Type: 'AWS::Serverless::Function' 302 | Properties: 303 | Handler: lambda_function.handler 304 | Runtime: python3.7 305 | CodeUri: ../source/ 306 | Role: !GetAtt LambdaExecutionRole.Arn 307 | 308 | 309 | LambdaExecutionRole: 310 | Type: AWS::IAM::Role 311 | Properties: 312 | AssumeRolePolicyDocument: 313 | Version: '2012-10-17' 314 | Statement: 315 | - Effect: Allow 316 | Principal: 317 | Service: 318 | - lambda.amazonaws.com 319 | Action: 320 | - sts:AssumeRole 321 | Path: "/" 322 | Policies: 323 | - PolicyName: root 324 | PolicyDocument: 325 | Version: '2012-10-17' 326 | Statement: 327 | - Effect: Allow 328 | Action: 329 | - logs:CreateLogGroup 330 | - logs:CreateLogStream 331 | - logs:PutLogEvents 332 | Resource: arn:aws:logs:*:*:* 333 | - Effect: Allow 334 | Action: 335 | - ec2:DescribeImages 336 | Resource: "*" 337 | 338 | 339 | AMIInfo: 340 | Type: AWS::CloudFormation::CustomResource 341 | Properties: 342 | ServiceToken: !GetAtt AMILookupLambda.Arn 343 | OSType: !Ref OSType 344 | 345 | -------------------------------------------------------------------------------- /deployment/outputs/vfx_packaged.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | AWSTemplateFormatVersion: 2010-09-09 5 | Transform: AWS::Serverless-2016-10-31 6 | Description: This is a quick start template that deploys the VFX Workstation along 7 | with other optional resources such as VPC and Client VPN. 8 | Metadata: 9 | AWS::CloudFormation::Interface: 10 | ParameterGroups: 11 | - Label: 12 | default: Amazon EC2 Configuration 13 | Parameters: 14 | - OSType 15 | - EBSVolumeSize 16 | - KeyPairName 17 | - VFXHostInstanceType 18 | - VFXHostAccessCIDR 19 | - VFXHostSubnetPlacement 20 | - EnableDeleteProtection 21 | - InstallBlenderSoftware 22 | - CreateS3StorageBucket 23 | - Label: 24 | default: Existing VPC Configuration 25 | Parameters: 26 | - ExistingVPCID 27 | - ExistingSubnetID 28 | - Label: 29 | default: New VPC Configuration 30 | Parameters: 31 | - VPCCIDR 32 | - PublicSubnet1CIDR 33 | - PrivateSubnet1CIDR 34 | - EnableVPCFlowLogs 35 | - Label: 36 | default: VPN Endpoint Configuration 37 | Parameters: 38 | - CreateVPNEndpoint 39 | - ClientCidrBlock 40 | - ServerCertArn 41 | - ClientCertificateArn 42 | - TargetNetworkCidr 43 | ParameterLabels: 44 | OSType: 45 | default: VFX Host Operating System 46 | EBSVolumeSize: 47 | default: EBS Volume size for EC2 instance 48 | VFXHostInstanceType: 49 | default: VFX Host Instance Type 50 | KeyPairName: 51 | default: Key Pair Name 52 | VFXHostAccessCIDR: 53 | default: VFX Host Access CIDR 54 | VFXHostSubnetPlacement: 55 | default: VFX Host subnet placement. 56 | EnableDeleteProtection: 57 | default: Enable Termination Protection 58 | InstallBlenderSoftware: 59 | default: Install Blender Software 60 | CreateS3StorageBucket: 61 | default: Creates S3 bucket to store files and then sync with workstation. 62 | ExistingVPCID: 63 | default: Existing VPC ID 64 | ExistingSubnetID: 65 | default: Existing Subnet ID 66 | VPCCIDR: 67 | default: VPC CIDR Range 68 | PublicSubnet1CIDR: 69 | default: CIDR Range for Public Subnet in new VPC 70 | PrivateSubnet1CIDR: 71 | default: CIDR Range for Private Subnet in new VPC 72 | EnableVPCFlowLogs: 73 | default: Enable VPC Flow Logs 74 | CreateVPNEndpoint: 75 | default: Create VPN Endpoint. 76 | ClientCidrBlock: 77 | default: Client CIDR for VPN Endpoint. 78 | ServerCertArn: 79 | default: Specify Server Cert Arn for VPN endpoint. 80 | ClientCertificateArn: 81 | default: Specify Client Cert Arn for VPN endpoint. 82 | TargetNetworkCidr: 83 | default: Target Network CIDR for VPN Endpoint. 84 | Parameters: 85 | OSType: 86 | AllowedValues: 87 | - linux 88 | - windows 89 | Default: linux 90 | Description: Specify whether you want to run Teradici on Linux or Windows OS. 91 | Type: String 92 | EBSVolumeSize: 93 | Default: '100' 94 | Description: Volume size for the VFX Host, in GiB 95 | MaxValue: '16000' 96 | MinValue: '100' 97 | Type: Number 98 | VFXHostInstanceType: 99 | AllowedValues: 100 | - g4dn.xlarge 101 | - g4dn.2xlarge 102 | - g4dn.4xlarge 103 | - g4dn.8xlarge 104 | - g4dn.12xlarge 105 | - g4dn.16xlarge 106 | Default: g4dn.xlarge 107 | Description: Amazon EC2 instance type for the VFX workstations 108 | Type: String 109 | KeyPairName: 110 | Description: Name of AWS EC2 Key Pair. 111 | Type: AWS::EC2::KeyPair::KeyName 112 | VFXHostAccessCIDR: 113 | 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-9]|3[0-2]))$ 114 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-32 115 | Default: 10.64.0.0/16 116 | Description: CIDR Block from which the VFX Host will be accessible. 117 | Type: String 118 | VFXHostSubnetPlacement: 119 | AllowedValues: 120 | - Public 121 | - Private 122 | ConstraintDescription: Specify if VFX host should be placed in "Public" or "Private" 123 | subnet. 124 | Default: Public 125 | Description: Specify if VFX host should be placed in "Public" or "Private" subnet. 126 | Type: String 127 | EnableDeleteProtection: 128 | AllowedValues: 129 | - 'true' 130 | - 'false' 131 | ConstraintDescription: Value must be either a true or false. 132 | Default: 'false' 133 | Description: Specify if VFX host should have delete protection enabled. 134 | Type: String 135 | InstallBlenderSoftware: 136 | AllowedValues: 137 | - 'true' 138 | - 'false' 139 | ConstraintDescription: Value must be either a true or false. 140 | Default: 'true' 141 | Description: Specify if VFX host should download and install Blender software. 142 | Type: String 143 | CreateS3StorageBucket: 144 | AllowedValues: 145 | - 'true' 146 | - 'false' 147 | ConstraintDescription: Value must be either a true or false. 148 | Default: 'true' 149 | Description: Specify if template should create an AWS S3 Bucket and connect the 150 | host to sync files between local system and S3 bucket. 151 | Type: String 152 | ExistingVPCID: 153 | Default: N/A 154 | Description: If solution should deploy into an existing VPC, Specify existing 155 | VPC ID. 156 | Type: String 157 | ExistingSubnetID: 158 | Default: N/A 159 | Description: If solution should deply into an existing VPN, Specify subnet id 160 | in which the VFX Host should be placed in. 161 | Type: String 162 | VPCCIDR: 163 | 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]))$ 164 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 165 | Default: 10.64.0.0/16 166 | Description: If solution should create a new VPC, specify CIDR Block for the VPC 167 | Type: String 168 | PublicSubnet1CIDR: 169 | 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]))$ 170 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 171 | Default: 10.64.32.0/20 172 | Description: If solution should create a new VPC, specify CIDR Block for the public 173 | subnet 1 located in Availability Zone 2 174 | Type: String 175 | PrivateSubnet1CIDR: 176 | 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]))$ 177 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 178 | Default: 10.64.96.0/20 179 | Description: If solution should create a new VPC, specify CIDR Block for the private 180 | subnet 1 located in Availability Zone 2 181 | Type: String 182 | EnableVPCFlowLogs: 183 | AllowedValues: 184 | - 'true' 185 | - 'false' 186 | ConstraintDescription: Value must be either a true or false. 187 | Default: 'false' 188 | Description: Specify if newly created VPC should have VPC flow logs enabled. The 189 | CloudFormation template will create a new S3 bucket to store the logs. It will 190 | also capture ALL logs including ACCEPTS and REJECTS. 191 | Type: String 192 | CreateVPNEndpoint: 193 | ConstraintDescription: Must specify 'true' or 'false' 194 | AllowedValues: 195 | - 'false' 196 | - 'true' 197 | Default: 'false' 198 | Description: Should the CloudFormation create a Client VPN Endpoint. It is recommended 199 | if VFX Host is placed in private subnet and there is no other provisions created 200 | to connect to private subnet.(Specify 'true' or 'false') 201 | Type: String 202 | ClientCidrBlock: 203 | Description: If creating Client VPN endpoint in the solution, specify the IPv4 204 | address range. It should be in CIDR notation from which to assign client IP 205 | addresses. The address range cannot overlap with the local CIDR of the VPC in 206 | which the associated subnet. 207 | 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-9]|3[0-2]))$ 208 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-32 209 | Default: 10.50.0.0/20 210 | Type: String 211 | ServerCertArn: 212 | Description: If creating Client VPN endpoint in the solution, specify Server Cert 213 | Arn for VPN endpoint. 214 | Default: N/A 215 | Type: String 216 | ClientCertificateArn: 217 | Description: If creating Client VPN endpoint in the solution, specify Client Cert 218 | Arn for VPN endpoint. 219 | Default: N/A 220 | Type: String 221 | TargetNetworkCidr: 222 | Description: If creating Client VPN endpoint in the solution, specify the IPv4 223 | address range, in CIDR notation, of the network for which access is being authorized. 224 | 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])(\/(0[0-9]|1[0-9]|2[0-9]|3[0-2]))$ 225 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/0-32 226 | Default: 10.64.0.0/16 227 | Type: String 228 | Conditions: 229 | PublicVFXHostPlacement: 230 | Fn::Equals: 231 | - Ref: VFXHostSubnetPlacement 232 | - Public 233 | PrivateVFXHostPlacement: 234 | Fn::Equals: 235 | - Ref: VFXHostSubnetPlacement 236 | - Private 237 | ExistingVPC: 238 | Fn::Not: 239 | - Fn::Equals: 240 | - N/A 241 | - Ref: ExistingVPCID 242 | NoExistingVPC: 243 | Fn::Equals: 244 | - N/A 245 | - Ref: ExistingVPCID 246 | CreateVPN: 247 | Fn::Equals: 248 | - 'true' 249 | - Ref: CreateVPNEndpoint 250 | Resources: 251 | VPCStack: 252 | Type: AWS::CloudFormation::Stack 253 | Condition: NoExistingVPC 254 | Properties: 255 | TemplateURL: https://s3.us-west-2.amazonaws.com/kashman-demo-vfxhost/aebb1ec94779960f49f61b0a9f3c0c65.template 256 | Parameters: 257 | PublicAZBSubnetBlock: 258 | Ref: PublicSubnet1CIDR 259 | PrivateAZBSubnetBlock: 260 | Ref: PrivateSubnet1CIDR 261 | VpcCidrParam: 262 | Ref: VPCCIDR 263 | EnableVPCFlowLogs: 264 | Ref: EnableVPCFlowLogs 265 | VPNStack: 266 | Type: AWS::CloudFormation::Stack 267 | Condition: CreateVPN 268 | Properties: 269 | TemplateURL: https://s3.us-west-2.amazonaws.com/kashman-demo-vfxhost/02a941e6be109965f30a8a29943a07e5.template 270 | Parameters: 271 | ClientCidrBlock: 272 | Ref: ClientCidrBlock 273 | ServerCertArn: 274 | Ref: ServerCertArn 275 | ClientCertificateArn: 276 | Ref: ClientCertificateArn 277 | SubnetID: 278 | Fn::If: 279 | - ExistingVPC 280 | - Ref: ExistingSubnetID 281 | - Fn::GetAtt: 282 | - VPCStack 283 | - Outputs.PrivateAZBSubnetId 284 | TargetNetworkCidr: 285 | Ref: TargetNetworkCidr 286 | VPC: 287 | Fn::If: 288 | - ExistingVPC 289 | - Ref: ExistingVPCID 290 | - Fn::GetAtt: 291 | - VPCStack 292 | - Outputs.VpcId 293 | CloudVFXHostStack: 294 | Type: AWS::CloudFormation::Stack 295 | Properties: 296 | TemplateURL: https://s3.us-west-2.amazonaws.com/kashman-demo-vfxhost/af58ed50609dc9b0ccce8108d15eb30e.template 297 | Parameters: 298 | EBSVolumeSize: 299 | Ref: EBSVolumeSize 300 | VFXHostInstanceType: 301 | Ref: VFXHostInstanceType 302 | KeyPairName: 303 | Ref: KeyPairName 304 | SubnetID: 305 | Fn::If: 306 | - ExistingVPC 307 | - Ref: ExistingSubnetID 308 | - Fn::If: 309 | - PublicVFXHostPlacement 310 | - Fn::GetAtt: 311 | - VPCStack 312 | - Outputs.PublicAZBSubnetId 313 | - Fn::GetAtt: 314 | - VPCStack 315 | - Outputs.PrivateAZBSubnetId 316 | VPCID: 317 | Fn::If: 318 | - ExistingVPC 319 | - Ref: ExistingVPCID 320 | - Fn::GetAtt: 321 | - VPCStack 322 | - Outputs.VpcId 323 | VFXHostAccessCIDR: 324 | Ref: VFXHostAccessCIDR 325 | AMIID: 326 | Fn::GetAtt: 327 | - AMIInfo 328 | - AMIID 329 | OSType: 330 | Ref: OSType 331 | VFXHostSubnetPlacement: 332 | Ref: VFXHostSubnetPlacement 333 | AdditionalSecurityGroupId: 334 | Fn::If: 335 | - CreateVPN 336 | - Fn::GetAtt: 337 | - VPNStack 338 | - Outputs.VPNSecurityGroupID 339 | - Ref: AWS::NoValue 340 | EnableDeleteProtection: 341 | Ref: EnableDeleteProtection 342 | InstallBlenderSoftware: 343 | Ref: InstallBlenderSoftware 344 | CreateS3StorageBucket: 345 | Ref: CreateS3StorageBucket 346 | AMILookupLambda: 347 | Type: AWS::Serverless::Function 348 | Properties: 349 | Handler: lambda_function.handler 350 | Runtime: python3.7 351 | CodeUri: s3://kashman-demo-vfxhost/479001191e1c9521f545f28f2e02b347 352 | Role: 353 | Fn::GetAtt: 354 | - LambdaExecutionRole 355 | - Arn 356 | LambdaExecutionRole: 357 | Type: AWS::IAM::Role 358 | Properties: 359 | AssumeRolePolicyDocument: 360 | Version: '2012-10-17' 361 | Statement: 362 | - Effect: Allow 363 | Principal: 364 | Service: 365 | - lambda.amazonaws.com 366 | Action: 367 | - sts:AssumeRole 368 | Path: / 369 | Policies: 370 | - PolicyName: root 371 | PolicyDocument: 372 | Version: '2012-10-17' 373 | Statement: 374 | - Effect: Allow 375 | Action: 376 | - logs:CreateLogGroup 377 | - logs:CreateLogStream 378 | - logs:PutLogEvents 379 | Resource: arn:aws:logs:*:*:* 380 | - Effect: Allow 381 | Action: 382 | - ec2:DescribeImages 383 | Resource: '*' 384 | AMIInfo: 385 | Type: AWS::CloudFormation::CustomResource 386 | Properties: 387 | ServiceToken: 388 | Fn::GetAtt: 389 | - AMILookupLambda 390 | - Arn 391 | OSType: 392 | Ref: OSType 393 | -------------------------------------------------------------------------------- /deployment/parameters/test-param.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "KeyPairName", 4 | "ParameterValue": "Customer-KeyPair-Name" 5 | }, 6 | { 7 | "ParameterKey": "VFXHostAccessCIDR", 8 | "ParameterValue": "123.45.67.89/32" 9 | }, 10 | { 11 | "ParameterKey": "VFXHostSubnetPlacement", 12 | "ParameterValue": "Public" 13 | }, 14 | { 15 | "ParameterKey": "OSType", 16 | "ParameterValue": "windows" 17 | }, 18 | { 19 | "ParameterKey": "EnableDeleteProtection", 20 | "ParameterValue": "false" 21 | }, 22 | { 23 | "ParameterKey": "InstallBlenderSoftware", 24 | "ParameterValue": "false" 25 | }, 26 | { 27 | "ParameterKey": "CreateS3StorageBucket", 28 | "ParameterValue": "true" 29 | }, 30 | { 31 | "ParameterKey": "EnableVPCFlowLogs", 32 | "ParameterValue": "false" 33 | }, 34 | { 35 | "ParameterKey": "ExistingVPCID", 36 | "ParameterValue": "vpc-id" 37 | }, 38 | { 39 | "ParameterKey": "ExistingSubnetID", 40 | "ParameterValue": "subnet-id" 41 | } 42 | ] -------------------------------------------------------------------------------- /deployment/vfxhost.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | AWSTemplateFormatVersion: '2010-09-09' 5 | Description: 'This template provisions EC2 instance with Teradici and Blender software installed.' 6 | Parameters: 7 | OSType: 8 | AllowedValues: 9 | - linux 10 | - windows 11 | Default: linux 12 | Description: Specify whether you want to run Teradici on Linux or Windows OS. 13 | Type: String 14 | EBSVolumeSize: 15 | Default: '100' 16 | Description: 'Volume size for the VFX Host, in GiB' 17 | MaxValue: '16000' 18 | MinValue: '100' 19 | Type: Number 20 | VFXHostInstanceType: 21 | # AllowedValues: 22 | # - g3.4xlarge 23 | # - g3.8xlarge 24 | # - g3.16xlarge 25 | # - g3s.xlarge 26 | Default: g4dn.xlarge 27 | Description: Amazon EC2 instance type for the VFX workstations 28 | Type: String 29 | KeyPairName: 30 | Description: >- 31 | Public/private key pairs allow you to securely connect to your instance 32 | after it launches 33 | Type: String 34 | VFXHostAccessCIDR: 35 | AllowedPattern: >- 36 | ^(([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]|3[0-2]))$ 37 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-32 38 | Default: 10.0.128.0/20 39 | Description: CIDR Block for the VFX Host to control authorized access. 40 | Type: String 41 | VPCID: 42 | Description: VPC ID in which the VFX Host placed in. 43 | Type: String 44 | SubnetID: 45 | Description: ID of the public subnet 1 in Availability Zone 1 (e.g., subnet-e3246d8e) 46 | Type: String 47 | AMIID: 48 | Description: VFX Host AMI ID. 49 | Type: String 50 | VFXHostSubnetPlacement: 51 | AllowedValues: 52 | - Public 53 | - Private 54 | ConstraintDescription: Specify if VFX host should be placed in "Public" or "Private" subnet. 55 | Default: 'Public' 56 | Description: Specify if VFX host should be placed in "Public" or "Private" subnet. 57 | Type: String 58 | AdditionalSecurityGroupId: 59 | Default: '0' 60 | Description: "Provide a security group id from which you want VFX Host to be accessed." 61 | Type: String 62 | EnableDeleteProtection: 63 | AllowedValues: 64 | - "true" 65 | - "false" 66 | ConstraintDescription: Value must be either a true or false. 67 | Default: 'false' 68 | Description: Specify if VFX host should have delete protection enabled.. 69 | Type: String 70 | InstallBlenderSoftware: 71 | AllowedValues: 72 | - "true" 73 | - "false" 74 | ConstraintDescription: Value must be either a true or false. 75 | Default: 'true' 76 | Description: Specify if VFX host should download and install Blender software. 77 | Type: String 78 | CreateS3StorageBucket: 79 | AllowedValues: 80 | - "true" 81 | - "false" 82 | ConstraintDescription: Value must be either a true or false. 83 | Default: 'true' 84 | Description: Specify if template should create an AWS S3 Bucket and connect the host to sync files between local system and S3 bucket. 85 | Type: String 86 | Conditions: 87 | PublicVFXHostPlacement: !Equals [ !Ref VFXHostSubnetPlacement, Public ] 88 | WindowsOS: !Equals [ !Ref OSType, 'windows' ] 89 | LinuxOS: !Equals [ !Ref OSType, 'linux' ] 90 | InstallBlenderSoftwareCond: !Equals [ !Ref InstallBlenderSoftware, 'true' ] 91 | CreateS3StorageBucketCond: !Equals [ !Ref CreateS3StorageBucket, 'true' ] 92 | AdditionalSecurityGroup: !Not [!Equals [ !Ref AdditionalSecurityGroupId, '0' ]] 93 | AdditionalSecurityGroupAndWindows: !And [ !Condition AdditionalSecurityGroup,!Condition WindowsOS ] 94 | AdditionalSecurityGroupAndLinux: !And [!Condition AdditionalSecurityGroup,!Condition LinuxOS ] 95 | Resources: 96 | ############################## 97 | #### VFX Host Resources ###### 98 | ############################## 99 | PublicEIP: 100 | Type: AWS::EC2::EIP 101 | Condition: PublicVFXHostPlacement 102 | Properties: 103 | InstanceId: !If [LinuxOS, !Ref LinuxVFXHost, !Ref WindowsVFXHost] 104 | VFXHostRole: 105 | Type: AWS::IAM::Role 106 | Condition: CreateS3StorageBucketCond 107 | Properties: 108 | Path: "/" 109 | AssumeRolePolicyDocument: 110 | Version: 2012-10-17 111 | Statement: 112 | - Effect: Allow 113 | Principal: 114 | Service: 115 | - ec2.amazonaws.com 116 | Action: 117 | - 'sts:AssumeRole' 118 | Policies: 119 | - 120 | PolicyName: "allow-access-to-s3-storage-bucket" 121 | PolicyDocument: 122 | Version: "2012-10-17" 123 | Statement: 124 | - 125 | Effect: "Allow" 126 | Action: 127 | - "s3:ListBucket" 128 | Resource: !GetAtt StorageBucket.Arn 129 | - 130 | Effect: "Allow" 131 | Action: 132 | - "s3:PutObject" 133 | - "s3:GetObject" 134 | - "s3:DeleteObject" 135 | Resource: !Join ["",[!GetAtt StorageBucket.Arn,"/*"]] 136 | VFXHostInstanceProfile: 137 | Type: 'AWS::IAM::InstanceProfile' 138 | Condition: CreateS3StorageBucketCond 139 | Properties: 140 | Path: / 141 | Roles: 142 | - !Ref VFXHostRole 143 | LinuxVFXHost: 144 | Type: AWS::EC2::Instance 145 | Condition: "LinuxOS" 146 | Metadata: 147 | AWS::CloudFormation::Init: 148 | install_base: 149 | files: 150 | # These files are needed for CloudFormation::Init to work 151 | /etc/cfn/cfn-hup.conf: 152 | content: !Sub | 153 | [main] 154 | stack=${AWS::StackId} 155 | region=${AWS::Region} 156 | interval=1 157 | /etc/cfn/hooks.d/cfn-auto-reloader.conf: 158 | content: !Sub | 159 | [cfn-auto-reloader-hook] 160 | triggers=post.update 161 | path=Resources.LinuxVFXHost.Metadata.AWS::CloudFormation::Init 162 | action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackId} --region ${AWS::Region} --resource LinuxVFXHost 163 | runas=root 164 | services: 165 | sysvinit: 166 | cfn-hup: 167 | enabled: true 168 | ensureRunning: true 169 | InstallBlender: 170 | commands: 171 | 30-install-snapd: 172 | command: "yum -y install snapd" 173 | 40-enable-snapd-socket: 174 | command: "systemctl enable --now snapd.socket" 175 | 50-create-symlink: 176 | command: "ln -s /var/lib/snapd/snap /snap" 177 | 60-snapd-wait: 178 | command: "snap wait system seed.loaded" 179 | 70-install-blender: 180 | command: "snap install blender --classic" 181 | Configure_S3: 182 | files: 183 | /root/configure_s3.sh: 184 | content: 185 | Fn::Sub: 186 | - | 187 | #!/bin/bash 188 | crontab -l > current_cron 189 | cat >> current_cron << EOF 190 | */1 * * * * aws s3 sync s3://${StorageBucket} /home/centos/s3 191 | */1 * * * * aws s3 sync /home/centos/s3 s3://${StorageBucket} 192 | EOF 193 | crontab < current_cron 194 | rm -f current_cron 195 | - StorageBucket: !If [CreateS3StorageBucketCond, !Ref StorageBucket, ''] 196 | commands: 197 | 10-make-directory: 198 | command: 'mkdir /home/centos/s3' 199 | 15-create-aws-sync: 200 | command: 'bash /root/configure_s3.sh' 201 | 202 | 203 | Success: 204 | commands: 205 | 10-send-success-signal: 206 | command: !Join ['',['/opt/aws/bin/cfn-signal -e 0 --stack ',!Ref 'AWS::StackName',' --resource LinuxVFXHost --region ', !Ref 'AWS::Region']] 207 | configSets: 208 | CentOSConfigSet: 209 | - "install_base" 210 | - Fn::If: [InstallBlenderSoftwareCond, "InstallBlender", !Ref "AWS::NoValue"] 211 | - Fn::If: [CreateS3StorageBucketCond, "Configure_S3", !Ref "AWS::NoValue"] 212 | - Success 213 | default: 214 | - 215 | ConfigSet: "CentOSConfigSet" 216 | CreationPolicy: 217 | ResourceSignal: 218 | Count: '1' 219 | Timeout: PT25M 220 | Properties: 221 | KeyName: !Ref KeyPairName 222 | DisableApiTermination: !Ref EnableDeleteProtection 223 | ImageId: !Ref AMIID #'ami-03dc6d7f59e5f1765' 224 | InstanceType: !Ref VFXHostInstanceType 225 | IamInstanceProfile: !If [CreateS3StorageBucketCond, !Ref VFXHostInstanceProfile, !Ref "AWS::NoValue"] 226 | SecurityGroupIds: 227 | - !Ref VFXSecurityGroup 228 | SubnetId: !Ref SubnetID 229 | BlockDeviceMappings: 230 | - DeviceName: /dev/sda1 231 | Ebs: 232 | VolumeType: gp2 233 | VolumeSize: !Ref EBSVolumeSize 234 | DeleteOnTermination: 'true' 235 | Encrypted: 'true' 236 | UserData: 237 | Fn::Base64: !Sub | 238 | #!/bin/bash 239 | yum install -y epel-release 240 | yum install -y awscli 241 | /usr/bin/easy_install --script-dir /opt/aws/bin https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz 242 | cp -v /usr/lib/python2*/site-packages/aws_cfn_bootstrap*/init/redhat/cfn-hup /etc/init.d 243 | chmod +x /etc/init.d/cfn-hup 244 | /opt/aws/bin/cfn-init --stack ${AWS::StackId} --resource LinuxVFXHost --region ${AWS::Region} 245 | /opt/aws/bin/cfn-signal -e 0 --stack ${AWS::StackName} --resource LinuxVFXHost --region ${AWS::Region} 246 | Tags: 247 | - Key: Name 248 | Value: VFXHost 249 | 250 | WindowsVFXHost: 251 | Type: AWS::EC2::Instance 252 | Condition: "WindowsOS" 253 | Metadata: 254 | AWS::CloudFormation::Init: 255 | InstallBlender: 256 | files: 257 | C:\cfn\scripts\install_blender.ps1: 258 | content: | 259 | <# 260 | .SYNOPSIS 261 | 262 | This script downloads and installs chocolatey package manager and then installs Blender for Windows. 263 | 264 | .DESCRIPTION 265 | 266 | The command enables Powershell to download the chocolatey package manager Then it installs Blender which is an open source 3D graphics software. 267 | 268 | .EXAMPLE 269 | 270 | C:\PS> .\install_blender.ps1 271 | #> 272 | 273 | 274 | # Write log to local txt file 275 | Start-Transcript -Path C:\cfn\log\install_chocolatey.ps1.txt -Append 276 | $ErrorActionPreference = "Stop" 277 | 278 | Set-ExecutionPolicy Bypass -Scope Process -Force 279 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 280 | iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) 281 | choco install --yes --limitoutput blender 282 | 283 | commands: 284 | 10-install-chocolatey: 285 | command: powershell.exe -Command ".\install_blender.ps1" 286 | cwd: C:\cfn\scripts\ 287 | waitAfterCompletion: 0 288 | Configure_EBS_Volume: 289 | files: 290 | C:\cfn\scripts\diskpart.txt: 291 | content: | 292 | select disk 1 293 | attributes disk clear readonly 294 | convert mbr 295 | create partition primary 296 | format quick fs=ntfs label=EBS_Volume 297 | assign letter=D 298 | commands: 299 | 10-configure-ebs: 300 | command: powershell.exe -Command "diskpart /s C:\cfn\scripts\diskpart.txt" 301 | waitAfterCompletion: 0 302 | Configure_S3: 303 | files: 304 | C:\cfn\scripts\s3_sync.ps1: 305 | content: 306 | Fn::Sub: 307 | - | 308 | # Write log to local txt file 309 | Start-Transcript -Path C:\cfn\log\s3_sync.ps1.txt -Append 310 | $ErrorActionPreference = "Stop" 311 | 312 | Set-ExecutionPolicy Bypass -Scope Process -Force 313 | 314 | aws s3 sync s3://${StorageBucket} D:\s3 315 | aws s3 sync D:\s3 s3://${StorageBucket} 316 | - StorageBucket: !If [CreateS3StorageBucketCond, !Ref StorageBucket, ''] 317 | C:\cfn\scripts\s3_sync.vbs: 318 | content: | 319 | command = "powershell.exe -nologo -command C:\cfn\scripts\s3_sync.ps1" 320 | set shell = CreateObject("WScript.Shell") 321 | shell.Run command,0 322 | C:\cfn\scripts\s3_configure.ps1: 323 | content: | 324 | <# 325 | .SYNOPSIS 326 | 327 | This script downloads and installs AWS CLI 328 | 329 | .DESCRIPTION 330 | 331 | The command enables Powershell to download the AWS CLI and then installs it. Later it creates a task definition for two AWS sync commands. 332 | 333 | .EXAMPLE 334 | 335 | C:\PS> .\s3_configure.ps1 336 | #> 337 | 338 | 339 | # Write log to local txt file 340 | Start-Transcript -Path C:\cfn\log\s3_configure.ps1.txt -Append 341 | $ErrorActionPreference = "Stop" 342 | 343 | Set-ExecutionPolicy Bypass -Scope Process -Force 344 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 345 | 346 | Invoke-Expression -Command "((New-Object System.Net.WebClient).DownloadFile('https://awscli.amazonaws.com/AWSCLIV2.msi', 'C:\cfn\scripts\AWSCLIV2.msi'))" 347 | 348 | # Install AWS CLI 349 | 350 | Start-Process msiexec.exe -Wait -ArgumentList '/I C:\cfn\scripts\AWSCLIV2.msi /quiet' 351 | 352 | # Create a New Folder for S3 Bucket 353 | 354 | New-Item -Path 'D:\s3' -ItemType Directory 355 | 356 | # Add Scheduled Task Definitions 357 | 358 | Schtasks /create /tn "Sync with AWS S3 Bucket" /sc minute /mo 1 /tr "C:\cfn\scripts\s3_sync.vbs" 359 | commands: 360 | 010-configure-s3-sync: 361 | command: powershell.exe -Command ".\s3_configure.ps1" 362 | cwd: C:\cfn\scripts\ 363 | waitAfterCompletion: 0 364 | Install_Wacom_Drivers: 365 | files: 366 | C:\cfn\scripts\install-wacom-drivers.ps1: 367 | content: | 368 | [CmdletBinding()] 369 | param( 370 | [Parameter(Mandatory=$false)] 371 | [string]$Source = 'http://cdn.wacom.com/u/productsupport/drivers/win/professional/WacomTablet_6.3.39-1.exe', 372 | # 'http://cdn.wacom.com/u/productsupport/drivers/win/professional/WacomTablet_6.3.29-6.exe' 373 | 374 | [Parameter(Mandatory=$false)] 375 | [string]$Destination = 'C:\cfn\downloads\wacom-installer.exe' 376 | ) 377 | 378 | try { 379 | $ErrorActionPreference = "Stop" 380 | 381 | $parentDir = Split-Path $Destination -Parent 382 | if (-not (Test-Path $parentDir)) { 383 | New-Item -Path $parentDir -ItemType directory -Force | Out-Null 384 | } 385 | 386 | Write-Verbose "Trying to download Wacom driver from $Source to $Destination" 387 | $tries = 5 388 | while ($tries -ge 1) { 389 | try { 390 | (New-Object System.Net.WebClient).DownloadFile($Source,$Destination) 391 | break 392 | } 393 | catch { 394 | $tries-- 395 | Write-Verbose "Exception:" 396 | Write-Verbose "$_" 397 | if ($tries -lt 1) { 398 | throw $_ 399 | } 400 | else { 401 | Write-Verbose "Failed download. Retrying again in 5 seconds" 402 | Start-Sleep 5 403 | } 404 | } 405 | } 406 | 407 | if ([System.IO.Path]::GetExtension($Destination) -eq '.exe') { 408 | Write-Verbose "Start install of Wacom drivers ..." 409 | # '/NoPostReboot' - to prevent reboot 410 | # 411 | Start-Process -FilePath $Destination -ArgumentList '/S', '/NoPostReboot' -Wait 412 | 413 | } else { 414 | throw "Unable to install Wacom drivers, not .exe extension" 415 | } 416 | Write-Verbose "Install Wacom drivers complete" 417 | } 418 | catch { 419 | Write-Verbose "catch: $_" 420 | $_ | Write-AWSQuickStartException 421 | } 422 | commands: 423 | 10-install-wacom-drivers: 424 | command: powershell.exe -Command ".\install-wacom-drivers.ps1" 425 | cwd: C:\cfn\scripts\ 426 | waitAfterCompletion: 0 427 | SignalSuccess: 428 | commands: 429 | 200-signal-success: 430 | command: !Join ['',['powershell.exe -Command "cfn-signal.exe --success true --stack ', !Ref 'AWS::StackName', ' --resource WindowsVFXHost --region ', !Ref 'AWS::Region']] 431 | waitAfterCompletion: 0 432 | 433 | configSets: 434 | WindowsOSConfigSet: 435 | - Fn::If: [InstallBlenderSoftwareCond, "InstallBlender", !Ref "AWS::NoValue"] 436 | - "Configure_EBS_Volume" 437 | - Fn::If: [CreateS3StorageBucketCond, "Configure_S3", !Ref "AWS::NoValue"] 438 | - "Install_Wacom_Drivers" 439 | - "SignalSuccess" 440 | 441 | 442 | default: 443 | - 444 | ConfigSet: "WindowsOSConfigSet" 445 | CreationPolicy: 446 | ResourceSignal: 447 | Count: '1' 448 | Timeout: PT25M 449 | Properties: 450 | KeyName: !Ref KeyPairName 451 | ImageId: !Ref AMIID 452 | InstanceType: !Ref VFXHostInstanceType 453 | IamInstanceProfile: !If [CreateS3StorageBucketCond, !Ref VFXHostInstanceProfile, !Ref "AWS::NoValue"] 454 | DisableApiTermination: !Ref EnableDeleteProtection 455 | SecurityGroupIds: 456 | - !Ref VFXSecurityGroup 457 | SubnetId: !Ref SubnetID 458 | BlockDeviceMappings: 459 | - DeviceName: /dev/sda1 460 | Ebs: 461 | VolumeType: gp2 462 | VolumeSize: !Ref EBSVolumeSize 463 | DeleteOnTermination: 'true' 464 | Encrypted: 'true' 465 | UserData: 466 | Fn::Base64: !Sub | 467 | 468 | Start-Transcript -Path "C:\cfn\log\userdata.log" 469 | cfn-init.exe --stack ${AWS::StackName} --resource WindowsVFXHost --region ${AWS::Region} 470 | Stop-Transcript 471 | 472 | false 473 | Tags: 474 | - Key: Name 475 | Value: VFXHost 476 | 477 | 478 | VFXSecurityGroup: 479 | Type: AWS::EC2::SecurityGroup 480 | Properties: 481 | GroupDescription: This security group is based on recommended settings for Teradici Cloud Access Software for CentOS 7 version 20.01.1-a provided by Teradici 482 | VpcId: !Ref VPCID 483 | VFXHostSecurityGroupIngress1: 484 | Type: AWS::EC2::SecurityGroupIngress 485 | Properties: 486 | GroupId: !Ref VFXSecurityGroup 487 | FromPort: 4172 488 | ToPort: 4172 489 | IpProtocol: tcp 490 | CidrIp: !Ref VFXHostAccessCIDR 491 | Description: 'PCoIP Session Establishment Port (Access from approved CIDR)' 492 | VFXHostSecurityGroupIngress2: 493 | Type: AWS::EC2::SecurityGroupIngress 494 | Properties: 495 | GroupId: !Ref VFXSecurityGroup 496 | FromPort: 4172 497 | ToPort: 4172 498 | IpProtocol: udp 499 | CidrIp: !Ref VFXHostAccessCIDR 500 | Description: 'PCoIP Session Data (Access from approved CIDR)' 501 | VFXHostSecurityGroupIngress3: 502 | Type: AWS::EC2::SecurityGroupIngress 503 | Properties: 504 | GroupId: !Ref VFXSecurityGroup 505 | Description: 506 | FromPort: 443 507 | ToPort: 443 508 | IpProtocol: tcp 509 | CidrIp: !Ref VFXHostAccessCIDR 510 | Description: 'Client Authentication (Access from approved CIDR)' 511 | VFXHostSecurityGroupIngress4: 512 | Type: AWS::EC2::SecurityGroupIngress 513 | Condition: LinuxOS 514 | Properties: 515 | GroupId: !Ref VFXSecurityGroup 516 | FromPort: 22 517 | ToPort: 22 518 | IpProtocol: tcp 519 | CidrIp: !Ref VFXHostAccessCIDR 520 | Description: 'SSH Port (Access from approved CIDR)' 521 | VFXHostSecurityGroupIngress5: 522 | Type: AWS::EC2::SecurityGroupIngress 523 | Condition: WindowsOS 524 | Properties: 525 | GroupId: !Ref VFXSecurityGroup 526 | FromPort: 3389 527 | ToPort: 3389 528 | IpProtocol: tcp 529 | CidrIp: !Ref VFXHostAccessCIDR 530 | Description: 'Remote Desktop Port (Access from approved CIDR)' 531 | VFXHostSecurityGroupIngress6: 532 | Type: AWS::EC2::SecurityGroupIngress 533 | Condition: AdditionalSecurityGroup 534 | Properties: 535 | GroupId: !Ref VFXSecurityGroup 536 | FromPort: 4172 537 | ToPort: 4172 538 | IpProtocol: tcp 539 | SourceSecurityGroupId: !Ref AdditionalSecurityGroupId 540 | Description: 'PCoIP Session Establishment Port (Access from VPN Security Group)' 541 | VFXHostSecurityGroupIngress7: 542 | Type: AWS::EC2::SecurityGroupIngress 543 | Condition: AdditionalSecurityGroup 544 | Properties: 545 | GroupId: !Ref VFXSecurityGroup 546 | FromPort: 4172 547 | ToPort: 4172 548 | IpProtocol: udp 549 | SourceSecurityGroupId: !Ref AdditionalSecurityGroupId 550 | Description: 'PCoIP Session Data (Access from VPN Security Group)' 551 | VFXHostSecurityGroupIngress8: 552 | Type: AWS::EC2::SecurityGroupIngress 553 | Condition: AdditionalSecurityGroup 554 | Properties: 555 | GroupId: !Ref VFXSecurityGroup 556 | FromPort: 443 557 | ToPort: 443 558 | IpProtocol: tcp 559 | SourceSecurityGroupId: !Ref AdditionalSecurityGroupId 560 | Description: 'Client Authentication (Access from VPN Security Group)' 561 | VFXHostSecurityGroupIngress9: 562 | Type: AWS::EC2::SecurityGroupIngress 563 | Condition: AdditionalSecurityGroupAndLinux 564 | Properties: 565 | GroupId: !Ref VFXSecurityGroup 566 | FromPort: 22 567 | ToPort: 22 568 | IpProtocol: tcp 569 | SourceSecurityGroupId: !Ref AdditionalSecurityGroupId 570 | Description: 'SSH Port (Access from VPN Security Group)' 571 | VFXHostSecurityGroupIngress10: 572 | Type: AWS::EC2::SecurityGroupIngress 573 | Condition: AdditionalSecurityGroupAndWindows 574 | Properties: 575 | GroupId: !Ref VFXSecurityGroup 576 | FromPort: 3389 577 | ToPort: 3389 578 | IpProtocol: tcp 579 | SourceSecurityGroupId: !Ref AdditionalSecurityGroupId 580 | Description: 'Remote Desktop Port (Access from VPN Security Group)' 581 | 582 | 583 | 584 | 585 | 586 | 587 | ############################## 588 | ###### S3 Resources ########## 589 | ############################## 590 | StorageBucket: 591 | Type: "AWS::S3::Bucket" 592 | Condition: CreateS3StorageBucketCond 593 | DeletionPolicy: Retain 594 | Properties: 595 | BucketEncryption: 596 | ServerSideEncryptionConfiguration: 597 | - ServerSideEncryptionByDefault: 598 | SSEAlgorithm: AES256 599 | BucketName: !Join 600 | - "-" 601 | - - "vfx-storage" 602 | - !Select 603 | - 0 604 | - !Split 605 | - "-" 606 | - !Select 607 | - 2 608 | - !Split 609 | - "/" 610 | - !Ref "AWS::StackId" 611 | Outputs: 612 | StorageS3Bucket: 613 | Description: "Storage S3 Bucket" 614 | Condition: CreateS3StorageBucketCond 615 | Value: !Ref StorageBucket 616 | Export: 617 | Name: !Sub "${AWS::StackName}:StorageS3Bucket" 618 | WindowsVFXInstanceID: 619 | Description: Instance ID of the VFX Host. 620 | Condition: WindowsOS 621 | Value: !Ref WindowsVFXHost 622 | Export: 623 | Name: !Sub "${AWS::StackName}:VFXInstanceID" 624 | WindowsVFXInstanceIP: 625 | Description: IP address of VFX Host. 626 | Condition: WindowsOS 627 | Value: !If [PublicVFXHostPlacement, !GetAtt WindowsVFXHost.PublicIp, !GetAtt WindowsVFXHost.PrivateIp] 628 | Export: 629 | Name: !Sub "${AWS::StackName}:VFXInstanceIP" 630 | LinuxVFXInstanceID: 631 | Description: Instance ID of the VFX Host. 632 | Condition: LinuxOS 633 | Value: !Ref LinuxVFXHost 634 | Export: 635 | Name: !Sub "${AWS::StackName}VFXInstanceID" 636 | LinuxVFXInstanceIP: 637 | Description: IP address of VFX Host. 638 | Condition: LinuxOS 639 | Value: !If [PublicVFXHostPlacement, !GetAtt LinuxVFXHost.PublicIp, !GetAtt LinuxVFXHost.PrivateIp] 640 | Export: 641 | Name: !Sub "${AWS::StackName}:VFXInstanceIP" -------------------------------------------------------------------------------- /deployment/vpc.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | AWSTemplateFormatVersion: 2010-09-09 5 | Description: > 6 | Creates a VPC with public subnet for a given AWS Account. 7 | Parameters: 8 | VpcCidrParam: 9 | Type: String 10 | Description: VPC CIDR. For more info, see http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html#VPC_Sizing 11 | AllowedPattern: "^(10|172|192)\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\/(16|17|18|19|20|21|22|23|24|25|26|27|28)$" 12 | ConstraintDescription: must be valid IPv4 CIDR block (/16 to /28) from the private address ranges defined in RFC 1918. 13 | 14 | # Public Subnets 15 | PublicAZBSubnetBlock: 16 | Type: String 17 | Description: Subnet CIDR for first Availability Zone 18 | AllowedPattern: "^(10|172|192)\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\/(16|17|18|19|20|21|22|23|24|25|26|27|28)$" 19 | ConstraintDescription: must be valid IPv4 CIDR block (/16 to /28) from the private address ranges defined in RFC 1918. 20 | PrivateAZBSubnetBlock: 21 | Type: String 22 | Description: Subnet CIDR for second Availability Zone (e.g. us-west-2b, us-east-1c) 23 | AllowedPattern: "^(10|172|192)\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\/(16|17|18|19|20|21|22|23|24|25|26|27|28)$" 24 | ConstraintDescription: must be valid IPv4 CIDR block (/16 to /28) from the private address ranges defined in RFC 1918. 25 | 26 | # VPC Flow Logs 27 | EnableVPCFlowLogs: 28 | AllowedValues: 29 | - "true" 30 | - "false" 31 | ConstraintDescription: Value must be either a true or false. 32 | Default: 'false' 33 | Description: Specify if newly created VPC should have VPC flow logs enabled. The CloudFormation template will create a new S3 bucket to store the logs. It will also capture ALL logs including ACCEPTS and REJECTS. 34 | Type: String 35 | Conditions: 36 | EnableVPCFlowLogsCond: !Equals [ !Ref EnableVPCFlowLogs, 'true' ] 37 | Outputs: 38 | VpcId: 39 | Description: VPC Id 40 | Value: !Ref Vpc 41 | Export: 42 | Name: !Sub "${AWS::StackName}-vpc-cidr" 43 | VpcCidr: 44 | Description: VPC Cidr 45 | Value: !GetAtt Vpc.CidrBlock 46 | Export: 47 | Name: !Sub "${AWS::StackName}-vpc-id" 48 | 49 | PublicRouteTableId: 50 | Description: Route Table for public subnets 51 | Value: !Ref PublicRouteTable 52 | Export: 53 | Name: !Sub "${AWS::StackName}-public-rtb" 54 | 55 | PublicAZBSubnetId: 56 | Description: Availability Zone A public subnet Id 57 | Value: !Ref PublicAZBSubnet 58 | Export: 59 | Name: !Sub "${AWS::StackName}-public-az-a-subnet" 60 | 61 | PrivateAZBSubnetId: 62 | Description: Availability Zone B private subnet Id 63 | Value: !Ref PrivateAZBSubnet 64 | Export: 65 | Name: !Sub "${AWS::StackName}-private-az-b-subnet" 66 | 67 | VPCFlowLogsBucket: 68 | Description: S3 Bucket where VPC Flow logs are stored 69 | Condition: EnableVPCFlowLogsCond 70 | Value: !GetAtt VPCFlowLogsBucket.Arn 71 | Export: 72 | Name: !Sub "${AWS::StackName}-vpc-flowlogs-bucket" 73 | 74 | 75 | Resources: 76 | Vpc: 77 | Type: AWS::EC2::VPC 78 | Properties: 79 | CidrBlock: !Ref VpcCidrParam 80 | EnableDnsHostnames: True 81 | EnableDnsSupport: True 82 | Tags: 83 | - Key: Name 84 | Value: !Sub ${AWS::StackName} 85 | 86 | InternetGateway: 87 | Type: AWS::EC2::InternetGateway 88 | Properties: 89 | Tags: 90 | - Key: Name 91 | Value: !Sub ${AWS::StackName} 92 | 93 | VPCGatewayAttachment: 94 | Type: AWS::EC2::VPCGatewayAttachment 95 | Properties: 96 | InternetGatewayId: !Ref InternetGateway 97 | VpcId: !Ref Vpc 98 | 99 | # Public Subnets - Route Table 100 | PublicRouteTable: 101 | Type: AWS::EC2::RouteTable 102 | Properties: 103 | VpcId: !Ref Vpc 104 | Tags: 105 | - Key: Name 106 | Value: !Sub ${AWS::StackName}-public 107 | - Key: Type 108 | Value: public 109 | 110 | PublicSubnetsRoute: 111 | Type: AWS::EC2::Route 112 | Properties: 113 | RouteTableId: !Ref PublicRouteTable 114 | DestinationCidrBlock: 0.0.0.0/0 115 | GatewayId: !Ref InternetGateway 116 | DependsOn: VPCGatewayAttachment 117 | 118 | # Public Subnets 119 | # First Availability Zone 120 | PublicAZBSubnet: 121 | Type: AWS::EC2::Subnet 122 | Properties: 123 | VpcId: !Ref Vpc 124 | CidrBlock: !Ref PublicAZBSubnetBlock 125 | AvailabilityZone: !Select [1, !GetAZs ""] 126 | MapPublicIpOnLaunch: true 127 | Tags: 128 | - Key: Name 129 | Value: !Sub 130 | - ${AWS::StackName}-public-${AZ} 131 | - { AZ: !Select [1, !GetAZs ""] } 132 | - Key: Type 133 | Value: public 134 | 135 | PublicAZBSubnetRouteTableAssociation: 136 | Type: AWS::EC2::SubnetRouteTableAssociation 137 | Properties: 138 | SubnetId: !Ref PublicAZBSubnet 139 | RouteTableId: !Ref PublicRouteTable 140 | 141 | AZBNatGatewayEIP: 142 | Type: AWS::EC2::EIP 143 | Properties: 144 | Domain: vpc 145 | DependsOn: VPCGatewayAttachment 146 | 147 | AZBNatGateway: 148 | Type: AWS::EC2::NatGateway 149 | Properties: 150 | AllocationId: !GetAtt AZBNatGatewayEIP.AllocationId 151 | SubnetId: !Ref PublicAZBSubnet 152 | PrivateAZBSubnet: 153 | Type: AWS::EC2::Subnet 154 | Properties: 155 | VpcId: !Ref Vpc 156 | CidrBlock: !Ref PrivateAZBSubnetBlock 157 | AvailabilityZone: !Select [1, !GetAZs ""] 158 | Tags: 159 | - Key: Name 160 | Value: !Sub 161 | - ${AWS::StackName}-private-${AZ} 162 | - { AZ: !Select [1, !GetAZs ""] } 163 | - Key: Type 164 | Value: private 165 | 166 | PrivateAZBRouteTable: 167 | Type: AWS::EC2::RouteTable 168 | Properties: 169 | VpcId: !Ref Vpc 170 | Tags: 171 | - Key: Name 172 | Value: !Sub 173 | - ${AWS::StackName}-private-${AZ} 174 | - { AZ: !Select [1, !GetAZs ""] } 175 | - Key: Type 176 | Value: private 177 | 178 | PrivateAZBRoute: 179 | Type: AWS::EC2::Route 180 | Properties: 181 | RouteTableId: !Ref PrivateAZBRouteTable 182 | DestinationCidrBlock: 0.0.0.0/0 183 | NatGatewayId: !Ref AZBNatGateway 184 | 185 | PrivateAZBRouteTableAssociation: 186 | Type: AWS::EC2::SubnetRouteTableAssociation 187 | Properties: 188 | SubnetId: !Ref PrivateAZBSubnet 189 | RouteTableId: !Ref PrivateAZBRouteTable 190 | 191 | 192 | S3VPCEndpoint: 193 | Type: "AWS::EC2::VPCEndpoint" 194 | Properties: 195 | RouteTableIds: 196 | - !Ref PublicRouteTable 197 | ServiceName: !Join 198 | - "" 199 | - - com.amazonaws. 200 | - !Ref "AWS::Region" 201 | - .s3 202 | VpcId: !Ref Vpc 203 | 204 | #VPC Flow Logs. 205 | VPCFlowLogs: 206 | Type: AWS::EC2::FlowLog 207 | Condition: EnableVPCFlowLogsCond 208 | Properties: 209 | LogDestination: !GetAtt VPCFlowLogsBucket.Arn 210 | LogDestinationType: s3 211 | ResourceId: !Ref Vpc 212 | ResourceType: VPC 213 | TrafficType: ALL 214 | 215 | VPCFlowLogsBucket: 216 | Type: "AWS::S3::Bucket" 217 | DeletionPolicy: Retain 218 | Condition: EnableVPCFlowLogsCond 219 | Properties: 220 | VersioningConfiguration: 221 | Status: Enabled 222 | BucketName: !Join 223 | - "-" 224 | - - "vpcflowlogs" 225 | - !Select 226 | - 0 227 | - !Split 228 | - "-" 229 | - !Select 230 | - 2 231 | - !Split 232 | - "/" 233 | - !Ref "AWS::StackId" -------------------------------------------------------------------------------- /documentation/Content-Creation-Workstation-Implementation-Guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/documentation/Content-Creation-Workstation-Implementation-Guide.pdf -------------------------------------------------------------------------------- /documentation/images/Default_Architectural_Diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/documentation/images/Default_Architectural_Diagram.jpg -------------------------------------------------------------------------------- /documentation/images/Default_Architectural_Diagram.jpg.license: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /source/crhelper-2.0.6.dist-info/INSTALLER: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /source/crhelper-2.0.6.dist-info/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /source/crhelper-2.0.6.dist-info/METADATA: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: crhelper 3 | Version: 2.0.6 4 | Summary: crhelper simplifies authoring CloudFormation Custom Resources 5 | Home-page: https://github.com/aws-cloudformation/custom-resource-helper 6 | Author: Jay McConnell 7 | Author-email: jmmccon@amazon.com 8 | License: Apache2 9 | Platform: UNKNOWN 10 | Classifier: Programming Language :: Python :: 3.6 11 | Classifier: Programming Language :: Python :: 3.7 12 | Classifier: License :: OSI Approved :: Apache Software License 13 | Classifier: Operating System :: OS Independent 14 | Description-Content-Type: text/markdown 15 | 16 | ## Custom Resource Helper 17 | 18 | Simplify best practice Custom Resource creation, sending responses to CloudFormation and providing exception, timeout 19 | trapping, and detailed configurable logging. 20 | 21 | [![PyPI Version](https://img.shields.io/pypi/v/crhelper.svg)](https://pypi.org/project/crhelper/) 22 | ![Python Versions](https://img.shields.io/pypi/pyversions/crhelper.svg) 23 | [![Build Status](https://travis-ci.com/aws-cloudformation/custom-resource-helper.svg?branch=master)](https://travis-ci.com/aws-cloudformation/custom-resource-helper) 24 | [![Test Coverage](https://codecov.io/gh/aws-cloudformation/custom-resource-helper/branch/master/graph/badge.svg)](https://codecov.io/gh/aws-cloudformation/custom-resource-helper) 25 | 26 | ## Features 27 | 28 | * Dead simple to use, reduces the complexity of writing a CloudFormation custom resource 29 | * Guarantees that CloudFormation will get a response even if an exception is raised 30 | * Returns meaningful errors to CloudFormation Stack events in the case of a failure 31 | * Polling enables run times longer than the lambda 15 minute limit 32 | * JSON logging that includes request id's, stack id's and request type to assist in tracing logs relevant to a 33 | particular CloudFormation event 34 | * Catches function timeouts and sends CloudFormation a failure response 35 | * Static typing (mypy) compatible 36 | 37 | ## Installation 38 | 39 | Install into the root folder of your lambda function 40 | 41 | ```json 42 | cd my-lambda-function/ 43 | pip install crhelper -t . 44 | ``` 45 | 46 | ## Example Usage 47 | 48 | [This blog](https://aws.amazon.com/blogs/infrastructure-and-automation/aws-cloudformation-custom-resource-creation-with-python-aws-lambda-and-crhelper/) covers usage in more detail. 49 | 50 | ```python 51 | from __future__ import print_function 52 | from crhelper import CfnResource 53 | import logging 54 | 55 | logger = logging.getLogger(__name__) 56 | # Initialise the helper, all inputs are optional, this example shows the defaults 57 | helper = CfnResource(json_logging=False, log_level='DEBUG', boto_level='CRITICAL', sleep_on_delete=120) 58 | 59 | try: 60 | ## Init code goes here 61 | pass 62 | except Exception as e: 63 | helper.init_failure(e) 64 | 65 | 66 | @helper.create 67 | def create(event, context): 68 | logger.info("Got Create") 69 | # Optionally return an ID that will be used for the resource PhysicalResourceId, 70 | # if None is returned an ID will be generated. If a poll_create function is defined 71 | # return value is placed into the poll event as event['CrHelperData']['PhysicalResourceId'] 72 | # 73 | # To add response data update the helper.Data dict 74 | # If poll is enabled data is placed into poll event as event['CrHelperData'] 75 | helper.Data.update({"test": "testdata"}) 76 | 77 | # To return an error to cloudformation you raise an exception: 78 | if not helper.Data.get("test"): 79 | raise ValueError("this error will show in the cloudformation events log and console.") 80 | 81 | return "MyResourceId" 82 | 83 | 84 | @helper.update 85 | def update(event, context): 86 | logger.info("Got Update") 87 | # If the update resulted in a new resource being created, return an id for the new resource. 88 | # CloudFormation will send a delete event with the old id when stack update completes 89 | 90 | 91 | @helper.delete 92 | def delete(event, context): 93 | logger.info("Got Delete") 94 | # Delete never returns anything. Should not fail if the underlying resources are already deleted. 95 | # Desired state. 96 | 97 | 98 | @helper.poll_create 99 | def poll_create(event, context): 100 | logger.info("Got create poll") 101 | # Return a resource id or True to indicate that creation is complete. if True is returned an id 102 | # will be generated 103 | return True 104 | 105 | 106 | def handler(event, context): 107 | helper(event, context) 108 | ``` 109 | 110 | ### Polling 111 | 112 | If you need longer than the max runtime of 15 minutes, you can enable polling by adding additional decorators for 113 | `poll_create`, `poll_update` or `poll_delete`. When a poll function is defined for `create`/`update`/`delete` the 114 | function will not send a response to CloudFormation and instead a CloudWatch Events schedule will be created to 115 | re-invoke the lambda function every 2 minutes. When the function is invoked the matching `@helper.poll_` function will 116 | be called, logic to check for completion should go here, if the function returns `None` then the schedule will run again 117 | in 2 minutes. Once complete either return a PhysicalResourceID or `True` to have one generated. The schedule will be 118 | deleted and a response sent back to CloudFormation. If you use polling the following additional IAM policy must be 119 | attached to the function's IAM role: 120 | 121 | ```yaml 122 | { 123 | "Version": "2012-10-17", 124 | "Statement": [ 125 | { 126 | "Effect": "Allow", 127 | "Action": [ 128 | "lambda:AddPermission", 129 | "lambda:RemovePermission", 130 | "events:PutRule", 131 | "events:DeleteRule", 132 | "events:PutTargets", 133 | "events:RemoveTargets" 134 | ], 135 | "Resource": "*" 136 | } 137 | ] 138 | } 139 | ``` 140 | 141 | ## Credits 142 | 143 | Decorator implementation inspired by https://github.com/ryansb/cfn-wrapper-python 144 | 145 | Log implementation inspired by https://gitlab.com/hadrien/aws_lambda_logging 146 | 147 | ## License 148 | 149 | This library is licensed under the Apache 2.0 License. 150 | 151 | 152 | -------------------------------------------------------------------------------- /source/crhelper-2.0.6.dist-info/NOTICE: -------------------------------------------------------------------------------- 1 | Custom Resource Helper 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /source/crhelper-2.0.6.dist-info/RECORD: -------------------------------------------------------------------------------- 1 | crhelper-2.0.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 2 | crhelper-2.0.6.dist-info/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142 3 | crhelper-2.0.6.dist-info/METADATA,sha256=0FEfmNkHpgUGUHmR-GGoiZwcGJsEYmJE92mkBI_tQ1Q,5537 4 | crhelper-2.0.6.dist-info/NOTICE,sha256=gDru0mjdrGkrCJfnHTVboKMdS7U85Ha8bV_PQTCckfM,96 5 | crhelper-2.0.6.dist-info/RECORD,, 6 | crhelper-2.0.6.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92 7 | crhelper-2.0.6.dist-info/top_level.txt,sha256=pe_5uNErAyss8aUfseYKAjd3a1-LXM6bPjnkun7vbso,15 8 | crhelper/__init__.py,sha256=VSvHU2MKgP96DHSDXR1OYxnbC8j7yfuVhZubBLU7Pns,66 9 | crhelper/__pycache__/__init__.cpython-37.pyc,, 10 | crhelper/__pycache__/log_helper.cpython-37.pyc,, 11 | crhelper/__pycache__/resource_helper.cpython-37.pyc,, 12 | crhelper/__pycache__/utils.cpython-37.pyc,, 13 | crhelper/log_helper.py,sha256=18n4WKlGgxXL_iiYPqE8dWv9TW4sPZc4Ae3px5dbHmY,2665 14 | crhelper/resource_helper.py,sha256=jlFCL0YMi1lEN9kOqhRtKkMcDovoJJpwq1oTk3W5hX0,12637 15 | crhelper/utils.py,sha256=HX_ZnUy3DP81L5ofOVshhWK9NwYnZ9dzIWUPnOfFm5w,1384 16 | tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 17 | tests/__pycache__/__init__.cpython-37.pyc,, 18 | tests/__pycache__/test_log_helper.cpython-37.pyc,, 19 | tests/__pycache__/test_resource_helper.cpython-37.pyc,, 20 | tests/__pycache__/test_utils.cpython-37.pyc,, 21 | tests/test_log_helper.py,sha256=T25g-RnRYrwp05v__25thYiodWIIDtoSXDFAqe9Z7rQ,3256 22 | tests/test_resource_helper.py,sha256=5BzbcWX49kSZN0GveRpG8Bt3PHAYUGubJMOmbAigFP0,14462 23 | tests/test_utils.py,sha256=HbLMvoXfYbF952AMM-ey8RNasbYHFqfX17rqajluOKM,1407 24 | tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 25 | tests/unit/__pycache__/__init__.cpython-37.pyc,, 26 | -------------------------------------------------------------------------------- /source/crhelper-2.0.6.dist-info/WHEEL: -------------------------------------------------------------------------------- 1 | Wheel-Version: 1.0 2 | Generator: bdist_wheel (0.34.2) 3 | Root-Is-Purelib: true 4 | Tag: py3-none-any 5 | 6 | -------------------------------------------------------------------------------- /source/crhelper-2.0.6.dist-info/top_level.txt: -------------------------------------------------------------------------------- 1 | crhelper 2 | tests 3 | -------------------------------------------------------------------------------- /source/crhelper/__init__.py: -------------------------------------------------------------------------------- 1 | from crhelper.resource_helper import CfnResource, SUCCESS, FAILED 2 | -------------------------------------------------------------------------------- /source/crhelper/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/crhelper/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /source/crhelper/__pycache__/log_helper.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/crhelper/__pycache__/log_helper.cpython-37.pyc -------------------------------------------------------------------------------- /source/crhelper/__pycache__/resource_helper.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/crhelper/__pycache__/resource_helper.cpython-37.pyc -------------------------------------------------------------------------------- /source/crhelper/__pycache__/utils.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/crhelper/__pycache__/utils.cpython-37.pyc -------------------------------------------------------------------------------- /source/crhelper/log_helper.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import json 3 | import logging 4 | 5 | 6 | def _json_formatter(obj): 7 | """Formatter for unserialisable values.""" 8 | return str(obj) 9 | 10 | 11 | class JsonFormatter(logging.Formatter): 12 | """AWS Lambda Logging formatter. 13 | 14 | Formats the log message as a JSON encoded string. If the message is a 15 | dict it will be used directly. If the message can be parsed as JSON, then 16 | the parse d value is used in the output record. 17 | """ 18 | 19 | def __init__(self, **kwargs): 20 | super(JsonFormatter, self).__init__() 21 | self.format_dict = { 22 | 'timestamp': '%(asctime)s', 23 | 'level': '%(levelname)s', 24 | 'location': '%(name)s.%(funcName)s:%(lineno)d', 25 | } 26 | self.format_dict.update(kwargs) 27 | self.default_json_formatter = kwargs.pop( 28 | 'json_default', _json_formatter) 29 | 30 | def format(self, record): 31 | record_dict = record.__dict__.copy() 32 | record_dict['asctime'] = self.formatTime(record) 33 | 34 | log_dict = { 35 | k: v % record_dict 36 | for k, v in self.format_dict.items() 37 | if v 38 | } 39 | 40 | if isinstance(record_dict['msg'], dict): 41 | log_dict['message'] = record_dict['msg'] 42 | else: 43 | log_dict['message'] = record.getMessage() 44 | 45 | # Attempt to decode the message as JSON, if so, merge it with the 46 | # overall message for clarity. 47 | try: 48 | log_dict['message'] = json.loads(log_dict['message']) 49 | except (TypeError, ValueError): 50 | pass 51 | 52 | if record.exc_info: 53 | # Cache the traceback text to avoid converting it multiple times 54 | # (it's constant anyway) 55 | # from logging.Formatter:format 56 | if not record.exc_text: 57 | record.exc_text = self.formatException(record.exc_info) 58 | 59 | if record.exc_text: 60 | log_dict['exception'] = record.exc_text 61 | 62 | json_record = json.dumps(log_dict, default=self.default_json_formatter) 63 | 64 | if hasattr(json_record, 'decode'): # pragma: no cover 65 | json_record = json_record.decode('utf-8') 66 | 67 | return json_record 68 | 69 | 70 | def setup(level='DEBUG', formatter_cls=JsonFormatter, boto_level=None, **kwargs): 71 | if formatter_cls: 72 | for handler in logging.root.handlers: 73 | handler.setFormatter(formatter_cls(**kwargs)) 74 | 75 | logging.root.setLevel(level) 76 | 77 | if not boto_level: 78 | boto_level = level 79 | 80 | logging.getLogger('boto').setLevel(boto_level) 81 | logging.getLogger('boto3').setLevel(boto_level) 82 | logging.getLogger('botocore').setLevel(boto_level) 83 | logging.getLogger('urllib3').setLevel(boto_level) 84 | -------------------------------------------------------------------------------- /source/crhelper/resource_helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | TODO: 4 | * Async mode – take a wait condition handle as an input, increases max timeout to 12 hours 5 | * Idempotency – If a duplicate request comes in (say there was a network error in signaling back to cfn) the subsequent 6 | request should return the already created response, will need a persistent store of some kind... 7 | * Functional tests 8 | """ 9 | 10 | from __future__ import print_function 11 | import threading 12 | from crhelper.utils import _send_response 13 | from crhelper import log_helper 14 | import logging 15 | import random 16 | import boto3 17 | import string 18 | import json 19 | import os 20 | from time import sleep 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | SUCCESS = 'SUCCESS' 25 | FAILED = 'FAILED' 26 | 27 | 28 | class CfnResource(object): 29 | 30 | def __init__(self, json_logging=False, log_level='DEBUG', boto_level='ERROR', polling_interval=2, sleep_on_delete=120): 31 | self._sleep_on_delete= sleep_on_delete 32 | self._create_func = None 33 | self._update_func = None 34 | self._delete_func = None 35 | self._poll_create_func = None 36 | self._poll_update_func = None 37 | self._poll_delete_func = None 38 | self._timer = None 39 | self._init_failed = None 40 | self._json_logging = json_logging 41 | self._log_level = log_level 42 | self._boto_level = boto_level 43 | self._send_response = False 44 | self._polling_interval = polling_interval 45 | self.Status = "" 46 | self.Reason = "" 47 | self.PhysicalResourceId = "" 48 | self.StackId = "" 49 | self.RequestId = "" 50 | self.LogicalResourceId = "" 51 | self.Data = {} 52 | self._event = {} 53 | self._context = None 54 | self._response_url = "" 55 | self._sam_local = os.getenv('AWS_SAM_LOCAL') 56 | self._region = os.getenv('AWS_REGION') 57 | try: 58 | if not self._sam_local: 59 | self._lambda_client = boto3.client('lambda', region_name=self._region) 60 | self._events_client = boto3.client('events', region_name=self._region) 61 | self._logs_client = boto3.client('logs', region_name=self._region) 62 | if json_logging: 63 | log_helper.setup(log_level, boto_level=boto_level, RequestType='ContainerInit') 64 | else: 65 | log_helper.setup(log_level, formatter_cls=None, boto_level=boto_level) 66 | except Exception as e: 67 | logger.error(e, exc_info=True) 68 | self.init_failure(e) 69 | 70 | def __call__(self, event, context): 71 | try: 72 | self._log_setup(event, context) 73 | logger.debug(event) 74 | if not self._crhelper_init(event, context): 75 | return 76 | # Check for polling functions 77 | if self._poll_enabled() and self._sam_local: 78 | logger.info("Skipping poller functionality, as this is a local invocation") 79 | elif self._poll_enabled(): 80 | self._polling_init(event) 81 | # If polling is not enabled, then we should respond 82 | else: 83 | logger.debug("enabling send_response") 84 | self._send_response = True 85 | logger.debug("_send_response: %s" % self._send_response) 86 | if self._send_response: 87 | if self.RequestType == 'Delete': 88 | self._wait_for_cwlogs() 89 | self._cfn_response(event) 90 | except Exception as e: 91 | logger.error(e, exc_info=True) 92 | self._send(FAILED, str(e)) 93 | finally: 94 | if self._timer: 95 | self._timer.cancel() 96 | 97 | def _wait_for_cwlogs(self, sleep=sleep): 98 | time_left = int(self._context.get_remaining_time_in_millis() / 1000) - 15 99 | sleep_time = 0 100 | 101 | if time_left > self._sleep_on_delete: 102 | sleep_time = self._sleep_on_delete 103 | 104 | if sleep_time > 1: 105 | sleep(sleep_time) 106 | 107 | def _log_setup(self, event, context): 108 | if self._json_logging: 109 | log_helper.setup(self._log_level, boto_level=self._boto_level, RequestType=event['RequestType'], 110 | StackId=event['StackId'], RequestId=event['RequestId'], 111 | LogicalResourceId=event['LogicalResourceId'], aws_request_id=context.aws_request_id) 112 | else: 113 | log_helper.setup(self._log_level, boto_level=self._boto_level, formatter_cls=None) 114 | 115 | def _crhelper_init(self, event, context): 116 | self._send_response = False 117 | self.Status = SUCCESS 118 | self.Reason = "" 119 | self.PhysicalResourceId = "" 120 | self.StackId = event["StackId"] 121 | self.RequestId = event["RequestId"] 122 | self.LogicalResourceId = event["LogicalResourceId"] 123 | self.Data = {} 124 | if "CrHelperData" in event.keys(): 125 | self.Data = event["CrHelperData"] 126 | self.RequestType = event["RequestType"] 127 | self._event = event 128 | self._context = context 129 | self._response_url = event['ResponseURL'] 130 | if self._timer: 131 | self._timer.cancel() 132 | if self._init_failed: 133 | self._send(FAILED, str(self._init_failed)) 134 | return False 135 | self._set_timeout() 136 | self._wrap_function(self._get_func()) 137 | return True 138 | 139 | def _polling_init(self, event): 140 | # Setup polling on initial request 141 | logger.debug("pid1: %s" % self.PhysicalResourceId) 142 | if 'CrHelperPoll' not in event.keys() and self.Status != FAILED: 143 | logger.info("Setting up polling") 144 | self.Data["PhysicalResourceId"] = self.PhysicalResourceId 145 | self._setup_polling() 146 | self.PhysicalResourceId = None 147 | logger.debug("pid2: %s" % self.PhysicalResourceId) 148 | # if physical id is set, or there was a failure then we're done 149 | logger.debug("pid3: %s" % self.PhysicalResourceId) 150 | if self.PhysicalResourceId or self.Status == FAILED: 151 | logger.info("Polling complete, removing cwe schedule") 152 | self._remove_polling() 153 | self._send_response = True 154 | 155 | def generate_physical_id(self, event): 156 | return '_'.join([ 157 | event['StackId'].split('/')[1], 158 | event['LogicalResourceId'], 159 | self._rand_string(8) 160 | ]) 161 | 162 | def _cfn_response(self, event): 163 | # Use existing PhysicalResourceId if it's in the event and no ID was set 164 | if not self.PhysicalResourceId and "PhysicalResourceId" in event.keys(): 165 | logger.info("PhysicalResourceId present in event, Using that for response") 166 | self.PhysicalResourceId = event['PhysicalResourceId'] 167 | # Generate a physical id if none is provided 168 | elif not self.PhysicalResourceId or self.PhysicalResourceId is True: 169 | logger.info("No physical resource id returned, generating one...") 170 | self.PhysicalResourceId = self.generate_physical_id(event) 171 | self._send() 172 | 173 | def _poll_enabled(self): 174 | return getattr(self, "_poll_{}_func".format(self._event['RequestType'].lower())) 175 | 176 | def create(self, func): 177 | self._create_func = func 178 | return func 179 | 180 | def update(self, func): 181 | self._update_func = func 182 | return func 183 | 184 | def delete(self, func): 185 | self._delete_func = func 186 | return func 187 | 188 | def poll_create(self, func): 189 | self._poll_create_func = func 190 | return func 191 | 192 | def poll_update(self, func): 193 | self._poll_update_func = func 194 | return func 195 | 196 | def poll_delete(self, func): 197 | self._poll_delete_func = func 198 | return func 199 | 200 | def _wrap_function(self, func): 201 | try: 202 | self.PhysicalResourceId = func(self._event, self._context) if func else '' 203 | except Exception as e: 204 | logger.error(str(e), exc_info=True) 205 | self.Reason = str(e) 206 | self.Status = FAILED 207 | 208 | def _timeout(self): 209 | logger.error("Execution is about to time out, sending failure message") 210 | self._send(FAILED, "Execution timed out") 211 | 212 | def _set_timeout(self): 213 | self._timer = threading.Timer((self._context.get_remaining_time_in_millis() / 1000.00) - 0.5, 214 | self._timeout) 215 | self._timer.start() 216 | 217 | def _get_func(self): 218 | request_type = "_{}_func" 219 | if "CrHelperPoll" in self._event.keys(): 220 | request_type = "_poll" + request_type 221 | return getattr(self, request_type.format(self._event['RequestType'].lower())) 222 | 223 | def _send(self, status=None, reason="", send_response=_send_response): 224 | if len(str(str(self.Reason))) > 256: 225 | self.Reason = "ERROR: (truncated) " + str(self.Reason)[len(str(self.Reason)) - 240:] 226 | if len(str(reason)) > 256: 227 | reason = "ERROR: (truncated) " + str(reason)[len(str(reason)) - 240:] 228 | response_body = { 229 | 'Status': self.Status, 230 | 'PhysicalResourceId': str(self.PhysicalResourceId), 231 | 'StackId': self.StackId, 232 | 'RequestId': self.RequestId, 233 | 'LogicalResourceId': self.LogicalResourceId, 234 | 'Reason': str(self.Reason), 235 | 'Data': self.Data, 236 | } 237 | if status: 238 | response_body.update({'Status': status, 'Reason': reason}) 239 | send_response(self._response_url, response_body) 240 | 241 | def init_failure(self, error): 242 | self._init_failed = error 243 | logger.error(str(error), exc_info=True) 244 | 245 | def _cleanup_response(self): 246 | for k in ["CrHelperPoll", "CrHelperPermission", "CrHelperRule"]: 247 | if k in self.Data.keys(): 248 | del self.Data[k] 249 | 250 | @staticmethod 251 | def _rand_string(l): 252 | return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(l)) 253 | 254 | def _add_permission(self, rule_arn): 255 | sid = self._event['LogicalResourceId'] + self._rand_string(8) 256 | self._lambda_client.add_permission( 257 | FunctionName=self._context.function_name, 258 | StatementId=sid, 259 | Action='lambda:InvokeFunction', 260 | Principal='events.amazonaws.com', 261 | SourceArn=rule_arn 262 | ) 263 | return sid 264 | 265 | def _put_rule(self): 266 | response = self._events_client.put_rule( 267 | Name=self._event['LogicalResourceId'] + self._rand_string(8), 268 | ScheduleExpression='rate({} minutes)'.format(self._polling_interval), 269 | State='ENABLED', 270 | ) 271 | return response["RuleArn"] 272 | 273 | def _put_targets(self, func_name): 274 | region = self._event['CrHelperRule'].split(":")[3] 275 | account_id = self._event['CrHelperRule'].split(":")[4] 276 | partition = self._event['CrHelperRule'].split(":")[1] 277 | rule_name = self._event['CrHelperRule'].split("/")[1] 278 | logger.debug(self._event) 279 | self._events_client.put_targets( 280 | Rule=rule_name, 281 | Targets=[ 282 | { 283 | 'Id': '1', 284 | 'Arn': 'arn:%s:lambda:%s:%s:function:%s' % (partition, region, account_id, func_name), 285 | 'Input': json.dumps(self._event) 286 | } 287 | ] 288 | ) 289 | 290 | def _remove_targets(self, rule_arn): 291 | self._events_client.remove_targets( 292 | Rule=rule_arn.split("/")[1], 293 | Ids=['1'] 294 | ) 295 | 296 | def _remove_permission(self, sid): 297 | self._lambda_client.remove_permission( 298 | FunctionName=self._context.function_name, 299 | StatementId=sid 300 | ) 301 | 302 | def _delete_rule(self, rule_arn): 303 | self._events_client.delete_rule( 304 | Name=rule_arn.split("/")[1] 305 | ) 306 | 307 | def _setup_polling(self): 308 | self._event['CrHelperData'] = self.Data 309 | self._event['CrHelperPoll'] = True 310 | self._event['CrHelperRule'] = self._put_rule() 311 | self._event['CrHelperPermission'] = self._add_permission(self._event['CrHelperRule']) 312 | self._put_targets(self._context.function_name) 313 | 314 | def _remove_polling(self): 315 | if 'CrHelperData' in self._event.keys(): 316 | self._event.pop('CrHelperData') 317 | if "PhysicalResourceId" in self.Data.keys(): 318 | self.Data.pop("PhysicalResourceId") 319 | if 'CrHelperRule' in self._event.keys(): 320 | self._remove_targets(self._event['CrHelperRule']) 321 | else: 322 | logger.error("Cannot remove CloudWatch events rule, Rule arn not available in event") 323 | if 'CrHelperPermission' in self._event.keys(): 324 | self._remove_permission(self._event['CrHelperPermission']) 325 | else: 326 | logger.error("Cannot remove lambda events permission, permission id not available in event") 327 | if 'CrHelperRule' in self._event.keys(): 328 | self._delete_rule(self._event['CrHelperRule']) 329 | else: 330 | logger.error("Cannot remove CloudWatch events target, Rule arn not available in event") 331 | -------------------------------------------------------------------------------- /source/crhelper/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import json 3 | import logging as logging 4 | import time 5 | from urllib.parse import urlsplit, urlunsplit 6 | from http.client import HTTPSConnection 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def _send_response(response_url, response_body): 12 | try: 13 | json_response_body = json.dumps(response_body) 14 | except Exception as e: 15 | msg = "Failed to convert response to json: {}".format(str(e)) 16 | logger.error(msg, exc_info=True) 17 | response_body = {'Status': 'FAILED', 'Data': {}, 'Reason': msg} 18 | json_response_body = json.dumps(response_body) 19 | logger.debug("CFN response URL: {}".format(response_url)) 20 | logger.debug(json_response_body) 21 | headers = {'content-type': '', 'content-length': str(len(json_response_body))} 22 | split_url = urlsplit(response_url) 23 | host = split_url.netloc 24 | url = urlunsplit(("", "", *split_url[2:])) 25 | while True: 26 | try: 27 | connection = HTTPSConnection(host) 28 | connection.request(method="PUT", url=url, body=json_response_body, headers=headers) 29 | response = connection.getresponse() 30 | logger.info("CloudFormation returned status code: {}".format(response.reason)) 31 | break 32 | except Exception as e: 33 | logger.error("Unexpected failure sending response to CloudFormation {}".format(e), exc_info=True) 34 | time.sleep(5) 35 | -------------------------------------------------------------------------------- /source/lambda_function.py: -------------------------------------------------------------------------------- 1 | from crhelper import CfnResource 2 | import boto3 3 | import datetime 4 | 5 | helper = CfnResource() 6 | 7 | @helper.create 8 | @helper.update 9 | def get_ami(event, _): 10 | client = boto3.client('ec2') 11 | 12 | product_id = get_productid(event) 13 | print('Got ProductID: %s' % product_id) 14 | response = client.describe_images( 15 | Filters=[ 16 | 17 | { 18 | 'Name': 'product-code', 19 | 'Values': [product_id] 20 | }, 21 | { 22 | 'Name': 'product-code.type', 23 | 'Values': ['marketplace'] 24 | } 25 | ] 26 | # ImageIds=[ 27 | # 'ami-03dc6d7f59e5f1765'], 28 | 29 | ) 30 | #rint(response) 31 | images = response['Images'] 32 | latest_creation_date = datetime.datetime.min 33 | id = '' 34 | for image in images: 35 | 36 | date_time_str = image['CreationDate'] 37 | date_time_obj = datetime.datetime.strptime(date_time_str, "%Y-%m-%dT%H:%M:%S.%fZ") 38 | 39 | print ('This image has AMI name: %s and the image-id is: %s. It was created on: %s' % (image['Name'], image['ImageId'], image['CreationDate'])) 40 | print ('Proudct ID: %s' % image['ProductCodes']) 41 | print ('Owner is %s' % image['OwnerId']) 42 | if date_time_obj >= latest_creation_date: 43 | latest_creation_date = date_time_obj 44 | id = image['ImageId'] 45 | 46 | print('Latest AMI ID: %s' % id) 47 | print('Latest Creation Date: %s' % latest_creation_date) 48 | helper.Data['AMIID'] = id 49 | 50 | 51 | def get_productid(event): 52 | productids = {'linux': 'ai7s3sm9muv8bw9y12z9t6o8i', 'windows': '4af6zv023dsu3c28day09b2a9'} 53 | os_type = event['ResourceProperties']['OSType'] 54 | return productids.get(os_type) 55 | 56 | @helper.delete 57 | def no_op(_, __): 58 | pass 59 | 60 | def handler(event, context): 61 | #get_ami(event,context) 62 | helper(event, context) -------------------------------------------------------------------------------- /source/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/tests/__init__.py -------------------------------------------------------------------------------- /source/tests/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/tests/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /source/tests/__pycache__/test_log_helper.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/tests/__pycache__/test_log_helper.cpython-37.pyc -------------------------------------------------------------------------------- /source/tests/__pycache__/test_resource_helper.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/tests/__pycache__/test_resource_helper.cpython-37.pyc -------------------------------------------------------------------------------- /source/tests/__pycache__/test_utils.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/tests/__pycache__/test_utils.cpython-37.pyc -------------------------------------------------------------------------------- /source/tests/test_log_helper.py: -------------------------------------------------------------------------------- 1 | from crhelper.log_helper import * 2 | import unittest 3 | import logging 4 | 5 | 6 | class TestLogHelper(unittest.TestCase): 7 | 8 | def test_logging_no_formatting(self): 9 | logger = logging.getLogger('1') 10 | handler = logging.StreamHandler() 11 | logger.addHandler(handler) 12 | orig_formatters = [] 13 | for c in range(len(logging.root.handlers)): 14 | orig_formatters.append(logging.root.handlers[c].formatter) 15 | setup(level='DEBUG', formatter_cls=None, boto_level='CRITICAL') 16 | new_formatters = [] 17 | for c in range(len(logging.root.handlers)): 18 | new_formatters.append(logging.root.handlers[c].formatter) 19 | self.assertEqual(orig_formatters, new_formatters) 20 | 21 | def test_logging_boto_explicit(self): 22 | logger = logging.getLogger('2') 23 | handler = logging.StreamHandler() 24 | logger.addHandler(handler) 25 | setup(level='DEBUG', formatter_cls=None, boto_level='CRITICAL') 26 | for t in ['boto', 'boto3', 'botocore', 'urllib3']: 27 | b_logger = logging.getLogger(t) 28 | self.assertEqual(b_logger.level, 50) 29 | 30 | def test_logging_json(self): 31 | logger = logging.getLogger('3') 32 | handler = logging.StreamHandler() 33 | logger.addHandler(handler) 34 | setup(level='DEBUG', formatter_cls=JsonFormatter, RequestType='ContainerInit') 35 | for handler in logging.root.handlers: 36 | self.assertEqual(JsonFormatter, type(handler.formatter)) 37 | 38 | def test_logging_boto_implicit(self): 39 | logger = logging.getLogger('4') 40 | handler = logging.StreamHandler() 41 | logger.addHandler(handler) 42 | setup(level='DEBUG', formatter_cls=JsonFormatter, RequestType='ContainerInit') 43 | for t in ['boto', 'boto3', 'botocore', 'urllib3']: 44 | b_logger = logging.getLogger(t) 45 | self.assertEqual(b_logger.level, 10) 46 | 47 | def test_logging_json_keys(self): 48 | with self.assertLogs() as ctx: 49 | logger = logging.getLogger() 50 | handler = logging.StreamHandler() 51 | logger.addHandler(handler) 52 | setup(level='DEBUG', formatter_cls=JsonFormatter, RequestType='ContainerInit') 53 | logger.info("test") 54 | logs = json.loads(ctx.output[0]) 55 | self.assertEqual(["timestamp", "level", "location", "RequestType", "message"], list(logs.keys())) 56 | 57 | def test_logging_json_parse_message(self): 58 | with self.assertLogs() as ctx: 59 | logger = logging.getLogger() 60 | handler = logging.StreamHandler() 61 | logger.addHandler(handler) 62 | setup(level='DEBUG', formatter_cls=JsonFormatter, RequestType='ContainerInit') 63 | logger.info("{}") 64 | logs = json.loads(ctx.output[0]) 65 | self.assertEqual({}, logs["message"]) 66 | 67 | def test_logging_json_exception(self): 68 | with self.assertLogs() as ctx: 69 | logger = logging.getLogger() 70 | handler = logging.StreamHandler() 71 | logger.addHandler(handler) 72 | setup(level='DEBUG', formatter_cls=JsonFormatter, RequestType='ContainerInit') 73 | try: 74 | 1 + 't' 75 | except Exception as e: 76 | logger.info("[]", exc_info=True) 77 | logs = json.loads(ctx.output[0]) 78 | self.assertIn("exception", logs.keys()) 79 | -------------------------------------------------------------------------------- /source/tests/test_resource_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import crhelper 3 | import unittest 4 | from unittest.mock import call, patch, Mock 5 | import threading 6 | 7 | test_events = { 8 | "Create": { 9 | "RequestType": "Create", 10 | "RequestId": "test-event-id", 11 | "StackId": "arn/test-stack-id/guid", 12 | "LogicalResourceId": "TestResourceId", 13 | "ResponseURL": "response_url" 14 | }, 15 | "Update": { 16 | "RequestType": "Update", 17 | "RequestId": "test-event-id", 18 | "StackId": "test-stack-id", 19 | "LogicalResourceId": "TestResourceId", 20 | "PhysicalResourceId": "test-pid", 21 | "ResponseURL": "response_url" 22 | }, 23 | "Delete": { 24 | "RequestType": "Delete", 25 | "RequestId": "test-event-id", 26 | "StackId": "test-stack-id", 27 | "LogicalResourceId": "TestResourceId", 28 | "PhysicalResourceId": "test-pid", 29 | "ResponseURL": "response_url" 30 | } 31 | } 32 | 33 | 34 | class MockContext(object): 35 | 36 | function_name = "test-function" 37 | ms_remaining = 9000 38 | 39 | @staticmethod 40 | def get_remaining_time_in_millis(): 41 | return MockContext.ms_remaining 42 | 43 | 44 | class TestCfnResource(unittest.TestCase): 45 | def setUp(self): 46 | os.environ['AWS_REGION'] = 'us-east-1' 47 | 48 | def tearDown(self): 49 | os.environ.pop('AWS_REGION', None) 50 | 51 | @patch('crhelper.log_helper.setup', return_value=None) 52 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 53 | def test_init(self, mock_method): 54 | crhelper.resource_helper.CfnResource() 55 | mock_method.assert_called_once_with('DEBUG', boto_level='ERROR', formatter_cls=None) 56 | 57 | crhelper.resource_helper.CfnResource(json_logging=True) 58 | mock_method.assert_called_with('DEBUG', boto_level='ERROR', RequestType='ContainerInit') 59 | 60 | @patch('crhelper.log_helper.setup', return_value=None) 61 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 62 | def test_init_failure(self, mock_method): 63 | mock_method.side_effect = Exception("test") 64 | c = crhelper.resource_helper.CfnResource(json_logging=True) 65 | self.assertTrue(c._init_failed) 66 | 67 | @patch('crhelper.log_helper.setup', Mock()) 68 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 69 | @patch('crhelper.resource_helper.CfnResource._polling_init', Mock()) 70 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 71 | @patch('crhelper.resource_helper.CfnResource._send') 72 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 73 | @patch('crhelper.resource_helper.CfnResource._wrap_function', Mock()) 74 | def test_init_failure_call(self, mock_send): 75 | c = crhelper.resource_helper.CfnResource() 76 | c.init_failure(Exception('TestException')) 77 | 78 | event = test_events["Create"] 79 | c.__call__(event, MockContext) 80 | 81 | self.assertEqual([call('FAILED', 'TestException')], mock_send.call_args_list) 82 | 83 | @patch('crhelper.log_helper.setup', Mock()) 84 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 85 | @patch('crhelper.resource_helper.CfnResource._polling_init', Mock()) 86 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 87 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 88 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 89 | @patch('crhelper.resource_helper.CfnResource._wrap_function', Mock()) 90 | @patch('crhelper.resource_helper.CfnResource._cfn_response', return_value=None) 91 | def test_call(self, cfn_response_mock): 92 | c = crhelper.resource_helper.CfnResource() 93 | event = test_events["Create"] 94 | c.__call__(event, MockContext) 95 | self.assertTrue(c._send_response) 96 | cfn_response_mock.assert_called_once_with(event) 97 | 98 | c._sam_local = True 99 | c._poll_enabled = Mock(return_value=True) 100 | c._polling_init = Mock() 101 | c.__call__(event, MockContext) 102 | c._polling_init.assert_not_called() 103 | self.assertEqual(1, len(cfn_response_mock.call_args_list)) 104 | 105 | c._sam_local = False 106 | c._send_response = False 107 | c.__call__(event, MockContext) 108 | c._polling_init.assert_called() 109 | self.assertEqual(1, len(cfn_response_mock.call_args_list)) 110 | 111 | event = test_events["Delete"] 112 | c._wait_for_cwlogs = Mock() 113 | c._poll_enabled = Mock(return_value=False) 114 | c.__call__(event, MockContext) 115 | c._wait_for_cwlogs.assert_called() 116 | 117 | c._send = Mock() 118 | cfn_response_mock.side_effect = Exception("test") 119 | c.__call__(event, MockContext) 120 | c._send.assert_called_with('FAILED', "test") 121 | 122 | @patch('crhelper.log_helper.setup', Mock()) 123 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 124 | @patch('crhelper.resource_helper.CfnResource._polling_init', Mock()) 125 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 126 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 127 | @patch('crhelper.resource_helper.CfnResource._wrap_function', Mock()) 128 | @patch('crhelper.resource_helper.CfnResource._cfn_response', Mock(return_value=None)) 129 | def test_wait_for_cwlogs(self): 130 | 131 | c = crhelper.resource_helper.CfnResource() 132 | c._context = MockContext 133 | s = Mock() 134 | c._wait_for_cwlogs(sleep=s) 135 | s.assert_not_called() 136 | MockContext.ms_remaining = 140000 137 | c._wait_for_cwlogs(sleep=s) 138 | s.assert_called_once() 139 | 140 | @patch('crhelper.log_helper.setup', Mock()) 141 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 142 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 143 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 144 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 145 | @patch('crhelper.resource_helper.CfnResource._wrap_function', Mock()) 146 | @patch('crhelper.resource_helper.CfnResource._cfn_response', Mock()) 147 | def test_polling_init(self): 148 | c = crhelper.resource_helper.CfnResource() 149 | event = test_events['Create'] 150 | c._setup_polling = Mock() 151 | c._remove_polling = Mock() 152 | c._polling_init(event) 153 | c._setup_polling.assert_called_once() 154 | c._remove_polling.assert_not_called() 155 | self.assertEqual(c.PhysicalResourceId, None) 156 | 157 | c.Status = 'FAILED' 158 | c._setup_polling.assert_called_once() 159 | c._setup_polling.assert_called_once() 160 | 161 | c = crhelper.resource_helper.CfnResource() 162 | event = test_events['Create'] 163 | c._setup_polling = Mock() 164 | c._remove_polling = Mock() 165 | event['CrHelperPoll'] = "Some stuff" 166 | c.PhysicalResourceId = None 167 | c._polling_init(event) 168 | c._remove_polling.assert_not_called() 169 | c._setup_polling.assert_not_called() 170 | 171 | c.Status = 'FAILED' 172 | c._polling_init(event) 173 | c._remove_polling.assert_called_once() 174 | c._setup_polling.assert_not_called() 175 | 176 | c.Status = '' 177 | c.PhysicalResourceId = "some-id" 178 | c._remove_polling.assert_called() 179 | c._setup_polling.assert_not_called() 180 | 181 | @patch('crhelper.log_helper.setup', Mock()) 182 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 183 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 184 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 185 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 186 | @patch('crhelper.resource_helper.CfnResource._wrap_function', Mock()) 187 | def test_cfn_response(self): 188 | c = crhelper.resource_helper.CfnResource() 189 | event = test_events['Create'] 190 | c._send = Mock() 191 | 192 | orig_pid = c.PhysicalResourceId 193 | self.assertEqual(orig_pid, '') 194 | c._cfn_response(event) 195 | c._send.assert_called_once() 196 | print("RID: [%s]" % [c.PhysicalResourceId]) 197 | self.assertEqual(True, c.PhysicalResourceId.startswith('test-stack-id_TestResourceId_')) 198 | 199 | c._send = Mock() 200 | c.PhysicalResourceId = 'testpid' 201 | c._cfn_response(event) 202 | c._send.assert_called_once() 203 | self.assertEqual('testpid', c.PhysicalResourceId) 204 | 205 | c._send = Mock() 206 | c.PhysicalResourceId = True 207 | c._cfn_response(event) 208 | c._send.assert_called_once() 209 | self.assertEqual(True, c.PhysicalResourceId.startswith('test-stack-id_TestResourceId_')) 210 | 211 | c._send = Mock() 212 | c.PhysicalResourceId = '' 213 | event['PhysicalResourceId'] = 'pid-from-event' 214 | c._cfn_response(event) 215 | c._send.assert_called_once() 216 | self.assertEqual('pid-from-event', c.PhysicalResourceId) 217 | 218 | @patch('crhelper.log_helper.setup', Mock()) 219 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 220 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 221 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 222 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 223 | def test_wrap_function(self): 224 | c = crhelper.resource_helper.CfnResource() 225 | 226 | def func(e, c): 227 | return 'testpid' 228 | 229 | c._wrap_function(func) 230 | self.assertEqual('testpid', c.PhysicalResourceId) 231 | self.assertNotEqual('FAILED', c.Status) 232 | 233 | def func(e, c): 234 | raise Exception('test exception') 235 | 236 | c._wrap_function(func) 237 | self.assertEqual('FAILED', c.Status) 238 | self.assertEqual('test exception', c.Reason) 239 | 240 | @patch('crhelper.log_helper.setup', Mock()) 241 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 242 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 243 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 244 | def test_send(self): 245 | c = crhelper.resource_helper.CfnResource() 246 | s = Mock() 247 | c._send(send_response=s) 248 | s.assert_called_once() 249 | 250 | @patch('crhelper.log_helper.setup', Mock()) 251 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 252 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 253 | @patch('crhelper.resource_helper.CfnResource._send', return_value=None) 254 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 255 | def test_timeout(self, s): 256 | c = crhelper.resource_helper.CfnResource() 257 | c._timeout() 258 | s.assert_called_with('FAILED', "Execution timed out") 259 | 260 | @patch('crhelper.log_helper.setup', Mock()) 261 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 262 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 263 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 264 | def test_set_timeout(self): 265 | c = crhelper.resource_helper.CfnResource() 266 | c._context = MockContext() 267 | def func(): 268 | return None 269 | 270 | c._set_timeout() 271 | t = threading.Timer(1000, func) 272 | self.assertEqual(type(t), type(c._timer)) 273 | t.cancel() 274 | c._timer.cancel() 275 | 276 | @patch('crhelper.log_helper.setup', Mock()) 277 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 278 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 279 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 280 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 281 | def test_cleanup_response(self): 282 | c = crhelper.resource_helper.CfnResource() 283 | c.Data = {"CrHelperPoll": 1, "CrHelperPermission": 2, "CrHelperRule": 3} 284 | c._cleanup_response() 285 | self.assertEqual({}, c.Data) 286 | 287 | @patch('crhelper.log_helper.setup', Mock()) 288 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 289 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 290 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 291 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 292 | def test_remove_polling(self): 293 | c = crhelper.resource_helper.CfnResource() 294 | c._context = MockContext() 295 | 296 | c._events_client.remove_targets = Mock() 297 | c._events_client.delete_rule = Mock() 298 | c._lambda_client.remove_permission = Mock() 299 | 300 | with self.assertRaises(Exception) as e: 301 | c._remove_polling() 302 | 303 | self.assertEqual("failed to cleanup CloudWatch event polling", str(e)) 304 | c._events_client.remove_targets.assert_not_called() 305 | c._events_client.delete_rule.assert_not_called() 306 | c._lambda_client.remove_permission.assert_not_called() 307 | 308 | c._event["CrHelperRule"] = "1/2" 309 | c._event["CrHelperPermission"] = "1/2" 310 | c._remove_polling() 311 | c._events_client.remove_targets.assert_called() 312 | c._events_client.delete_rule.assert_called() 313 | c._lambda_client.remove_permission.assert_called() 314 | 315 | @patch('crhelper.log_helper.setup', Mock()) 316 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 317 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 318 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 319 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 320 | def test_setup_polling(self): 321 | c = crhelper.resource_helper.CfnResource() 322 | c._context = MockContext() 323 | c._event = test_events["Update"] 324 | c._lambda_client.add_permission = Mock() 325 | c._events_client.put_rule = Mock(return_value={"RuleArn": "arn:aws:lambda:blah:blah:function:blah/blah"}) 326 | c._events_client.put_targets = Mock() 327 | c._setup_polling() 328 | c._events_client.put_targets.assert_called() 329 | c._events_client.put_rule.assert_called() 330 | c._lambda_client.add_permission.assert_called() 331 | 332 | @patch('crhelper.log_helper.setup', Mock()) 333 | @patch('crhelper.resource_helper.CfnResource._poll_enabled', Mock(return_value=False)) 334 | @patch('crhelper.resource_helper.CfnResource._wait_for_cwlogs', Mock()) 335 | @patch('crhelper.resource_helper.CfnResource._send', Mock()) 336 | @patch('crhelper.resource_helper.CfnResource._set_timeout', Mock()) 337 | def test_wrappers(self): 338 | c = crhelper.resource_helper.CfnResource() 339 | 340 | def func(): 341 | pass 342 | 343 | for f in ["create", "update", "delete", "poll_create", "poll_update", "poll_delete"]: 344 | self.assertEqual(None, getattr(c, "_%s_func" % f)) 345 | getattr(c, f)(func) 346 | self.assertEqual(func, getattr(c, "_%s_func" % f)) 347 | -------------------------------------------------------------------------------- /source/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import patch, Mock 3 | from crhelper import utils 4 | import unittest 5 | 6 | 7 | class TestLogHelper(unittest.TestCase): 8 | TEST_URL = "https://test_url/this/is/the/url?query=123#aaa" 9 | 10 | @patch('crhelper.utils.HTTPSConnection', autospec=True) 11 | def test_send_succeeded_response(self, https_connection_mock): 12 | utils._send_response(self.TEST_URL, {}) 13 | https_connection_mock.assert_called_once_with("test_url") 14 | https_connection_mock.return_value.request.assert_called_once_with( 15 | body='{}', 16 | headers={"content-type": "", "content-length": "2"}, 17 | method="PUT", 18 | url="/this/is/the/url?query=123#aaa", 19 | ) 20 | 21 | @patch('crhelper.utils.HTTPSConnection', autospec=True) 22 | def test_send_failed_response(self, https_connection_mock): 23 | utils._send_response(self.TEST_URL, Mock()) 24 | https_connection_mock.assert_called_once_with("test_url") 25 | response = json.loads(https_connection_mock.return_value.request.call_args[1]["body"]) 26 | expected_body = '{"Status": "FAILED", "Data": {}, "Reason": "' + response["Reason"] + '"}' 27 | https_connection_mock.return_value.request.assert_called_once_with( 28 | body=expected_body, 29 | headers={"content-type": "", "content-length": str(len(expected_body))}, 30 | method="PUT", 31 | url="/this/is/the/url?query=123#aaa", 32 | ) 33 | -------------------------------------------------------------------------------- /source/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/tests/unit/__init__.py -------------------------------------------------------------------------------- /source/tests/unit/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/content-creation-workstation/cd69718d2cc83e751b126f77d11b1bce38a89b88/source/tests/unit/__pycache__/__init__.cpython-37.pyc --------------------------------------------------------------------------------