diff --git a/IDEA_VERSION.txt b/IDEA_VERSION.txt index 50e47c8..6ebad14 100644 --- a/IDEA_VERSION.txt +++ b/IDEA_VERSION.txt @@ -1 +1 @@ -3.1.1 \ No newline at end of file +3.1.2 \ No newline at end of file diff --git a/deployment/ecr/idea-administrator/Dockerfile b/deployment/ecr/idea-administrator/Dockerfile deleted file mode 100644 index 6b613d1..0000000 --- a/deployment/ecr/idea-administrator/Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -FROM public.ecr.aws/docker/library/python:3.9.16-slim - -WORKDIR /root - -RUN apt-get update && \ - apt-get -y install \ - curl \ - tar \ - unzip \ - locales \ - && apt-get clean - - -ENV DEBIAN_FRONTEND=noninteractive -ENV LC_ALL="en_US.UTF-8" \ - LC_CTYPE="en_US.UTF-8" \ - LANG="en_US.UTF-8" - -RUN sed -i -e "s/# $LANG.*/$LANG UTF-8/" /etc/locale.gen \ - && locale-gen "en_US.UTF-8" \ - && dpkg-reconfigure locales - -# install aws cli -RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \ - unzip -qq awscliv2.zip && \ - ./aws/install && \ - rm -rf ./aws awscliv2.zip - -# install nvm and node -RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \ - && apt-get install -y nodejs \ - && apt-get clean all - -# add all packaged artifacts to container -ARG PUBLIC_ECR_TAG -ENV PUBLIC_ECR_TAG=${PUBLIC_ECR_TAG} -ADD all-*.tar.gz /root/.idea/downloads/ - -# install administrator app -RUN mkdir -p /root/.idea/downloads/idea-administrator-${PUBLIC_ECR_TAG} && \ - tar -xvf /root/.idea/downloads/idea-administrator-*.tar.gz -C /root/.idea/downloads/idea-administrator-${PUBLIC_ECR_TAG} && \ - /bin/bash /root/.idea/downloads/idea-administrator-${PUBLIC_ECR_TAG}/install.sh && \ - rm -rf /root/.idea/downloads/idea-administrator-${PUBLIC_ECR_TAG} - -CMD ["bash"] - - diff --git a/deployment/integrated-digital-engineering-on-aws.template b/deployment/integrated-digital-engineering-on-aws.template deleted file mode 100644 index 840e68f..0000000 --- a/deployment/integrated-digital-engineering-on-aws.template +++ /dev/null @@ -1,234 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Description: (SO0072) Integrated Digital Engineering on AWS (IDEA) - -Metadata: - - AWS::CloudFormation::Interface: - ParameterGroups: - - - Label: - default: Linux Distribution - Parameters: - - ClusterName - - BaseOS - - - Label: - default: Network and Security - Parameters: - - VpcCidr - - ClientIp - - SSHKeyPair - - - Label: - default: Cluster Administrator - Parameters: - - AdministratorEmail - - - Label: - default: Installer EC2 Instance - Parameters: - - InstallerAmiId - - InstallerCdkToolkitPolicyArn - - InstallerCreateAccessPolicyArn - - InstallerDeleteAccessPolicyArn - - ParameterLabels: - InstallerAmiId: - default: The AMI ID for the installer EC2 Instance - InstallerCdkToolkitPolicyArn: - default: IDEA Administrator CDK ToolKit IAM Policy ARN - InstallerCreateAccessPolicyArn: - default: IDEA Administrator Create Access IAM Policy ARN - InstallerDeleteAccessPolicyArn: - default: IDEA Administrator Delete Access IAM Policy ARN - -Parameters: - - InstallerAmiId: - Type: 'AWS::SSM::Parameter::Value' - Description: Do not change this value. We will use the latest Amazon Linux 2 instance AMI ID based on your region. - Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 - - InstallerCdkToolkitPolicyArn: - Type: String - Description: | - The ARN of the IAM Policy to be attached to the Installer EC2 Instance. This policy should contain all the permissions - listed under "deployment/idea/aws/idea-admin-cdk-toolkit-policy.json" - - InstallerCreateAccessPolicyArn: - Type: String - Description: | - The ARN of the IAM Policy to be attached to the Installer EC2 Instance. This policy should contain all the permissions - listed under "deployment/idea/aws/idea-admin-create-policy.json" - - InstallerDeleteAccessPolicyArn: - Type: String - Description: | - The ARN of the IAM Policy to be attached to the Installer EC2 Instance. This policy should contain all the permissions - listed under "deployment/idea/aws/idea-admin-delete-policy.json" - - ClusterName: - Type: String - Description: Name of your cluster. - AllowedPattern: 'idea-.+' - ConstraintDescription: The name of the cluster must start with "idea-". - - BaseOS: - Type: String - "AllowedValues": [ - "amazonlinux2", - "centos7", - "rhel7" - ] - "Description": IMPORTANT CENTOS USERS > You MUST subscribe to https://aws.amazon.com/marketplace/pp/B00O7WM7QW first if using CentOS - "Default": amazonlinux2 - - VpcCidr: - Type: String - Description: Choose the Cidr block (/16 down to /24) you want to use for your VPC (eg 10.0.0.0/16 down to 10.0.0.0/24) - AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/(1[6-9]|2[0-4])' - ConstraintDescription: Your VPC must use x.x.x.x/16 - x.x.x.x/24 CIDR range - Default: 10.0.0.0/16 - - ClientIp: - Type: String - Description: Default IP(s) allowed to directly SSH into the scheduler and access ElasticSearch. 0.0.0.0/0 means ALL INTERNET access. You probably want to change it with your own IP/subnet (x.x.x.x/32 for your own ip or x.x.x.x/24 for range. Replace x.x.x.x with your own PUBLIC IP. You can get your public IP using tools such as https://ifconfig.co/). Make sure to keep it restrictive! - AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' - ConstraintDescription: ClientIP must be a valid IP or network range of the form x.x.x.x/x. If you want to add everyone (not recommended) use 0.0.0.0/0 otherwise specify your IP/NETMASK (e.g x.x.x/32 or x.x.x.x/24 for subnet range) - - SSHKeyPair: - Type: AWS::EC2::KeyPair::KeyName - Description: Default SSH pem keys used to SSH into cluster instances. - AllowedPattern: .+ - - AdministratorEmail: - Type: String - Description: | - Provide an Email Address for the cluster administrator account. You will receive an email with your temporary credentials during cluster installation. After the solution is deployed, you can use the temporary credentials to login - and reset the password. - MinLength: 3 - - -Resources: - - InstallerIamRole: - Type: AWS::IAM::Role - Properties: - Path: / - AssumeRolePolicyDocument: - Statement: - - Effect: Allow - Principal: - Service: - - !Sub ec2.${AWS::URLSuffix} - - !Sub ssm.${AWS::URLSuffix} - Action: - - sts:AssumeRole - ManagedPolicyArns: - - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore - - !Ref InstallerCdkToolkitPolicyArn - - !Ref InstallerCreateAccessPolicyArn - - !Ref InstallerDeleteAccessPolicyArn - - InstallerInstanceProfile: - Type: AWS::IAM::InstanceProfile - Properties: - Path: / - Roles: - - !Ref InstallerIamRole - - InstallerEc2: - Type: 'AWS::EC2::Instance' - CreationPolicy: - ResourceSignal: - Timeout: PT2H - Properties: - SecurityGroups: - - !Ref InstallerEc2SecurityGroup - KeyName: !Ref SSHKeyPair - ImageId: !Ref InstallerAmiId - IamInstanceProfile: !Ref InstallerInstanceProfile - InstanceType: t3.medium - Tags: - - Key: Name - Value: !Sub ${ClusterName}-installer - UserData: - "Fn::Base64": !Sub | - #!/bin/bash - - set -x - - IDEA_ECR_REPO="public.ecr.aws/g8j8s8q8/idea-administrator" - IDEA_REVISION="v3.1.1" - INSTANCE_PUBLIC_IP=$(TOKEN=$(curl --silent -X PUT 'http://169.254.169.254/latest/api/token' -H 'X-aws-ec2-metadata-token-ttl-seconds: 300') && curl --silent -H "X-aws-ec2-metadata-token: ${!TOKEN}" 'http://169.254.169.254/latest/meta-data/public-ipv4') - - mkdir -p /root/.idea/clusters/${ClusterName}/${AWS::Region} - - echo " - --- - aws_partition: ${AWS::Partition} - aws_region: ${AWS::Region} - aws_account_id: '${AWS::AccountId}' - aws_dns_suffix: ${AWS::URLSuffix} - cluster_name: ${ClusterName} - administrator_email: ${AdministratorEmail} - vpc_cidr_block: ${VpcCidr} - ssh_key_pair_name: ${SSHKeyPair} - cluster_access: client-ip - client_ip: - - ${ClientIp} - - ${!INSTANCE_PUBLIC_IP}/32 - alb_public: true - use_vpc_endpoints: false - directory_service_provider: openldap - kms_key_type: aws-managed - enabled_modules: - - metrics - - scheduler - - virtual-desktop-controller - - bastion-host - metrics_provider: cloudwatch - base_os: ${BaseOS} - instance_type: m5.large - volume_size: '200' - " > /root/.idea/clusters/${ClusterName}/${AWS::Region}/values.yml - - yum install -y docker - systemctl enable docker.service - systemctl start docker.service - - docker pull ${!IDEA_ECR_REPO}:${!IDEA_REVISION} - - docker run --rm -i \ - -v /root/.idea/clusters:/root/.idea/clusters \ - -e AWS_DEFAULT_REGION=${AWS::Region} \ - -e IDEA_ADMIN_AWS_CREDENTIAL_PROVIDER=Ec2InstanceMetadata \ - ${!IDEA_ECR_REPO}:${!IDEA_REVISION} \ - idea-admin quick-setup \ - --values-file /root/.idea/clusters/${ClusterName}/${AWS::Region}/values.yml \ - --force - - /opt/aws/bin/cfn-signal -e "$?" --stack "${AWS::StackName}" --resource InstallerEc2 --region "${AWS::Region}" - - InstallerEc2SecurityGroup: - Type: 'AWS::EC2::SecurityGroup' - Metadata: - cfn_nag: - rules_to_suppress: - - id: W5 - reason: "Allow all IP egress from the installer EC2 instance" - - id: W29 - reason: "Allow all TCP ports egress from the installer EC2 instance" - Properties: - GroupDescription: Enable SSH access via port 22 from ClientIP - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 22 - ToPort: 22 - Description: SSH Access from Client IP - CidrIp: !Ref ClientIp - SecurityGroupEgress: - - FromPort: 0 - ToPort: 65535 - Description: All egress TCP traffic - CidrIp: 0.0.0.0/0 diff --git a/idea-admin-windows.ps1 b/idea-admin-windows.ps1 index e80e8ba..23fef0e 100755 --- a/idea-admin-windows.ps1 +++ b/idea-admin-windows.ps1 @@ -38,7 +38,7 @@ function Verify-Command($type,$message,$command) { $IDEADevMode = if ($Env:IDEA_DEV_MODE) {$Env:IDEA_DEV_MODE} else {""} $VirtualEnv = if ($Env:VIRTUAL_ENV) {$Env:VIRTUAL_ENV} else {""} $ScriptDir = $PSScriptRoot -$IDEARevision = if ($Env:IDEA_REVISION) {$Env:IDEA_REVISION} else {"v3.1.1"} +$IDEARevision = if ($Env:IDEA_REVISION) {$Env:IDEA_REVISION} else {"v3.1.2"} $IDEADockerRepo = "public.ecr.aws/g8j8s8q8" $DocumentationError = "https://ide-on-aws.com" $AWSProfile = if ($Env:AWS_PROFILE) {$Env:AWS_PROFILE} else {"default"} diff --git a/idea-admin.sh b/idea-admin.sh index 62fe4a1..ed1d9c9 100755 --- a/idea-admin.sh +++ b/idea-admin.sh @@ -28,7 +28,7 @@ # * IDEA_DEV_MODE - Set to "true" if you are working with IDEA sources SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -IDEA_REVISION=${IDEA_REVISION:-"v3.1.1"} +IDEA_REVISION=${IDEA_REVISION:-"v3.1.2"} IDEA_DOCKER_REPO=${IDEA_DOCKER_REPO:-"public.ecr.aws/g8j8s8q8/idea-administrator"} IDEA_ECR_CREDS_RESET=${IDEA_ECR_CREDS_RESET:-"true"} IDEA_ADMIN_AWS_CREDENTIAL_PROVIDER=${IDEA_ADMIN_AWS_CREDENTIAL_PROVIDER:=""} @@ -117,9 +117,9 @@ if [[ $? -ne 0 ]]; then fi # Launch installer -${DOCKER_BIN} run --rm -it -v ${HOME}/.idea/clusters:/root/.idea/clusters \ - -e IDEA_ADMIN_AWS_CREDENTIAL_PROVIDER=${IDEA_ADMIN_AWS_CREDENTIAL_PROVIDER} \ - -e IDEA_ADMIN_ENABLE_CDK_NAG_SCAN=${IDEA_ADMIN_ENABLE_CDK_NAG_SCAN} \ - -v ~/.aws:/root/.aws ${IDEA_DOCKER_REPO}:${IDEA_REVISION} \ - idea-admin ${@} +${DOCKER_BIN} run --rm -it -v "${HOME}/.idea/clusters:/root/.idea/clusters" \ + -e IDEA_ADMIN_AWS_CREDENTIAL_PROVIDER="${IDEA_ADMIN_AWS_CREDENTIAL_PROVIDER}" \ + -e IDEA_ADMIN_ENABLE_CDK_NAG_SCAN="${IDEA_ADMIN_ENABLE_CDK_NAG_SCAN}" \ + -v ~/.aws:/root/.aws "${IDEA_DOCKER_REPO}:${IDEA_REVISION}" \ + idea-admin "${@}" diff --git a/requirements/idea-cluster-manager.in b/requirements/idea-cluster-manager.in index ffb48d4..a7e2b97 100644 --- a/requirements/idea-cluster-manager.in +++ b/requirements/idea-cluster-manager.in @@ -1,5 +1,5 @@ -r idea-sdk.in supervisor -sanic=22.3.2 +sanic==22.3.2 python-ldap ldappool diff --git a/source/idea/idea-administrator/resources/config/templates/analytics/settings.yml b/source/idea/idea-administrator/resources/config/templates/analytics/settings.yml index e174f00..fe9483f 100644 --- a/source/idea/idea-administrator/resources/config/templates/analytics/settings.yml +++ b/source/idea/idea-administrator/resources/config/templates/analytics/settings.yml @@ -14,6 +14,7 @@ opensearch: slow_index_log_enabled: true # Log Amazon OpenSearch Service audit logs to this log group slow_search_log_enabled: true # Specify if slow search logging should be set up. {%- endif %} + kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # Specify your own CMK to encrypt OpenSearch domain. If set to ~ encryption will be managed by the default AWS key default_number_of_shards: 2 default_number_of_replicas: 1 @@ -25,3 +26,4 @@ opensearch: kinesis: shard_count: 2 stream_mode: PROVISIONED + kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # Specify your own CMK to encrypt Kinesis stream. If set to ~ encryption will be managed by the default AWS key diff --git a/source/idea/idea-administrator/resources/config/templates/cluster/settings.yml b/source/idea/idea-administrator/resources/config/templates/cluster/settings.yml index fb183e4..f2ab5b2 100644 --- a/source/idea/idea-administrator/resources/config/templates/cluster/settings.yml +++ b/source/idea/idea-administrator/resources/config/templates/cluster/settings.yml @@ -88,6 +88,10 @@ network: # https_proxy expected format is http://: https_proxy: "" no_proxy: "127.0.0.1,169.254.169.254,localhost,{{ aws_region }}.local,{{ aws_region }}.vpce.{{ aws_dns_suffix }},s3.{{ aws_region }}.{{ aws_dns_suffix }},s3.dualstack.{{ aws_region }}.{{ aws_dns_suffix }},dynamodb.{{ aws_region }}.{{ aws_dns_suffix }},{{ aws_region }}.es.{{ aws_dns_suffix }},sqs.{{ aws_region }}.{{ aws_dns_suffix }},ec2.{{ aws_region }}.{{ aws_dns_suffix }},secretsmanager.{{ aws_region }}.{{ aws_dns_suffix }},sns.{{ aws_region }}.{{ aws_dns_suffix }},cloudformation.{{ aws_region }}.{{ aws_dns_suffix }},elasticloadbalancing.{{ aws_region }}.{{ aws_dns_suffix }},monitoring.{{ aws_region }}.{{ aws_dns_suffix }},logs.{{ aws_region }}.{{ aws_dns_suffix }},ssm.{{ aws_region }}.{{ aws_dns_suffix }},application-autoscaling.{{ aws_region }}.{{ aws_dns_suffix }},events.{{ aws_region }}.{{ aws_dns_suffix }},kinesis.{{ aws_region }}.{{ aws_dns_suffix }},{{ aws_region }}.elb.{{ aws_dns_suffix }},autoscaling.{{ aws_region }}.{{ aws_dns_suffix }}" + {%- else %} + # https_proxy expected format is http://: + https_proxy: "" + no_proxy: "127.0.0.1,169.254.169.254,localhost,{{ aws_region }}.local,{{ aws_region }}.elb.{{ aws_dns_suffix }},{{ aws_region }}.es.{{ aws_dns_suffix }}" {%- endif %} # AWS Key Management Service @@ -97,22 +101,27 @@ kms: # Configure cluster-wide AWS Secrets Manager settings below secretsmanager: - kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # Specify your own CMK to encrypt your Secret manager. If set to ~ encryption will be managed by the default AWS key + kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # Specify your own CMK to encrypt Secrets Manager secrets. If set to ~ encryption will be managed by the default AWS key # Configure cluster-wide SQS settings below +# The customer managed key for Amazon SQS queues must have a policy statement that grants Amazon SNS service-principal access +# Consult the documentation at: https://docs.aws.amazon.com/sns/latest/dg/sns-enable-encryption-for-topic-sqs-queue-subscriptions.html sqs: kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # Specify your own CMK to encrypt SQS queues. If set to ~ encryption will be managed by the default AWS key # Configure cluster-wide SNS settings below sns: - kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # Specify your own CMK to encrypt SNS topic. If set to ~ encryption will be managed by the default AWS key + kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # Specify your own CMK to encrypt SNS topics. If set to ~ encryption will be managed by the default AWS key # Configure cluster-wide DynamoDB settings below. dynamodb: - # this configuration is not supported and used at the moment. - # customizations are required to enable DDB encryption at rest. + # Note: Dynamodb .vdc.dcv-broker.* tables are encrypted with DynamoDB service key kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # Specify your own CMK to encrypt DynamoDB tables. If set to ~ encryption will be managed by the default AWS key +# Configure cluster-wide EBS settings below +ebs: + kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # Specify your own CMK to encrypt EBS volumes. If set to ~ encryption will be managed by the default AWS key + solution: # Enable to disable IDEA Anonymous Metric Collection. # Refer to def build_metrics() on source/idea/idea-scheduler/src/ideascheduler/app/provisioning/job_provisioner/cloudformation_stack_builder.py for a list of metric being sent @@ -178,7 +187,7 @@ backups: # existing backup vault is not supported. backup_vault: - # Specify your own CMK to encrypt your Secret manager. If set to ~ encryption will be managed by the default AWS key + # Specify your own CMK to encrypt backup vault. If set to ~ encryption will be managed by the default AWS key kms_key_id: {{ kms_key_id if kms_key_id else '~' }} # The removal policy to apply to the vault. diff --git a/source/idea/idea-administrator/resources/config/templates/directoryservice/_templates/activedirectory.yml b/source/idea/idea-administrator/resources/config/templates/directoryservice/_templates/activedirectory.yml index 2879fe6..daf85a3 100644 --- a/source/idea/idea-administrator/resources/config/templates/directoryservice/_templates/activedirectory.yml +++ b/source/idea/idea-administrator/resources/config/templates/directoryservice/_templates/activedirectory.yml @@ -1,13 +1,13 @@ # Begin: Microsoft AD Settings (activedirectory) # Below is a template configuration to connect to your existing "On-Prem" or "Self-Managed" AD on AWS -# customize below configuration based on your environment requirements - below configuration will not work out of the box. +# customize the configuration based on your environment requirements - The configuration below will not work out of the box. # # # When using the 'activedirectory' Directory Service back-end - READ-ONLY access is activated in IDEA # for User and Group management. All user and group management activities take place in Active Directory directly. # READ-WRITE is still required for the creation of Computer objects. -# SSO is expected to be configured and linked to the same Active Directory back-end. +# SSO is required to be configured and linked to the same Active Directory back-end. # # The NetBIOS name for your domain @@ -96,19 +96,16 @@ clusteradmin: # -# Provide a mapping of the base IDEA groups to Active Directory DN +# Provide a mapping of the base IDEA groups to Active Directory groups # -# If just the name of the group is supplied, e.g. "cluster-manager-administrators-module-group" is provided, the qualified path will be computed as below: +# If just the name of the group is supplied, e.g. "default-project-group" is provided, the qualified path will be computed as below: # cn=, -# otherwise it is treated as a full DN -# e.g. cn=cluster-manager-administrators-module-group,OU=Org123,DC=domain,DC=local - +# otherwise it will be searched as the distinguishedName (must start with CN=) +# e.g. cn=default-project-group,OU=Org123,DC=domain,DC=local group_mapping: # idea-required-group is a special mapping - # When users SSO for the first time - they do not have cache entries in DDB. # The users must be part of this AD group to be considered eligible for the IDEA cluster. - # This can be specified to a specific group. # The IDEA / AD Administrator is expected to create this group and maintain the proper membership # of the users. # e.g. CN=IDEAUsers,OU=AADDC Users,DC=idea-admin,DC=cloud diff --git a/source/idea/idea-administrator/resources/config/templates/global-settings/settings.yml b/source/idea/idea-administrator/resources/config/templates/global-settings/settings.yml index 620c142..7f188ef 100644 --- a/source/idea/idea-administrator/resources/config/templates/global-settings/settings.yml +++ b/source/idea/idea-administrator/resources/config/templates/global-settings/settings.yml @@ -318,9 +318,9 @@ package_config: {%- if 'scheduler' in enabled_modules %} efa: - version: "1.22.0" - url: "https://efa-installer.amazonaws.com/aws-efa-installer-1.22.0.tar.gz" - checksum: "4c1b4a822f26e43c942623e3d0e6fb3816f15d6cbacd9bba286c9648b0cefe274cd79167d37c938bcdb49a96629b14a2" + version: "1.22.1" + url: "https://efa-installer.amazonaws.com/aws-efa-installer-1.22.1.tar.gz" + checksum: "081eed2a4e4be6cc44e3426916e7440bbd15734ace21a1013cb212aaf2a10f1c9cb7a1b380fa41ab7106b1a302712939" checksum_method: sha384 openpbs: version: "22.05.11" @@ -345,304 +345,304 @@ package_config: x86_64: {%- if 'windows' in supported_base_os %} windows: - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-server-x64-Release-2022.1-13300.msi - sha256sum: a0581180d56612eccfa4af6e4958d608cd0076a0d2e186b7e45cfe1c6ab49b61 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-server-x64-Release-2023.0-14852.msi + sha256sum: 877edd544e04a2b18b180baa766fd5c200872bce8ae44092162e908033a38000 {%- endif %} linux: {%- if 'amazonlinux2' in supported_base_os or 'rhel7' in supported_base_os or 'centos7' in supported_base_os %} al2_rhel_centos7: - version: 2022.1-13300-el7-x86_64 - tgz: nice-dcv-2022.1-13300-el7-x86_64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-el7-x86_64.tgz - sha256sum: fe782c3ff6a1fd9291f7dcca0eadeec7cce47f1ae13d2da97fbed714fe00cf4d + version: 2023.0-14852-el7-x86_64 + tgz: nice-dcv-2023.0-14852-el7-x86_64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-el7-x86_64.tgz + sha256sum: ab662e5bb79f06c46182f83b582538f2dffe5faa1d3da0251404fb96e8310ffe {%- endif %} {%- if 'rhel8' in supported_base_os or 'centos8' in supported_base_os or 'rocky8' in supported_base_os %} rhel_centos_rocky8: - version: 2022.1-13300-el8-x86_64 - tgz: nice-dcv-2022.1-13300-el8-x86_64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-el8-x86_64.tgz - sha256sum: d047aa01166e6b8315807bc0138680cfa4938325b83e638e7c717d7481b848e8 + version: 2023.0-14852-el8-x86_64 + tgz: nice-dcv-2023.0-14852-el8-x86_64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-el8-x86_64.tgz + sha256sum: 357436715d0304f10a518b423dc73dd12bd9ea935dd9f79faf41c24ec542b449 {%- endif %} {%- if 'suse12' in supported_base_os %} suse12: - version: 2022.1-13300-sles12-x86_64 - tgz: nice-dcv-2022.1-13300-sles12-x86_64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-sles12-x86_64.tgz - sha256sum: a7139e108db9d74ace1ebfccf665b0cdbb487c946dee5f4dda000f14c189ec4f + version: 2023.0-14852-sles12-x86_64 + tgz: nice-dcv-2023.0-14852-sles12-x86_64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-sles12-x86_64.tgz + sha256sum: 26592817b6d049080290d2ede2ce342aa35a59af1af8d7b3d2c791195cbf0a9a {%- endif %} {%- if 'suse15' in supported_base_os %} suse15: - version: 2022.1-13300-sles15-x86_64 - tgz: nice-dcv-2022.1-13300-sles15-x86_64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-sles15-x86_64.tgz - sha256sum: 7e8d9bc22e674014fe442207889be80efcb144bdff44e6b5c5a1391871fa565e + version: 2023.0-14852-sles15-x86_64 + tgz: nice-dcv-2023.0-14852-sles15-x86_64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-sles15-x86_64.tgz + sha256sum: 558e859d7e6a6aa65ac5a0d71193092c26a3faeb6b09a6ac48192d51af77e5a3 {%- endif %} ubuntu: {%- if 'ubuntu1804' in supported_base_os %} ubuntu1804: - version: 2022.1-13300-ubuntu1804-x86_64 - tgz: nice-dcv-2022.1-13300-ubuntu1804-x86_64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-ubuntu1804-x86_64.tgz - sha256sum: db15078bacfb01c0583b1edd541031f31085f07beeca66fc0131225da2925def + version: 2023.0-14852-ubuntu1804-x86_64 + tgz: nice-dcv-2023.0-14852-ubuntu1804-x86_64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-ubuntu1804-x86_64.tgz + sha256sum: 25488fb09585fc86381e1f4b20c7fe232708d027918518d429bcf79fe149a399 {%- endif %} {%- if 'ubuntu2004' in supported_base_os %} ubuntu2004: - version: 2022.1-13300-ubuntu2004-x86_64 - tgz: nice-dcv-2022.1-13300-ubuntu2004-x86_64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-ubuntu2004-x86_64.tgz - sha256sum: 2e8c4a58645f3f91987b2b1086de5d46cf1c686c34046d57ee0e6f1e526a3031 + version: 2023.0-14852-ubuntu2004-x86_64 + tgz: nice-dcv-2023.0-14852-ubuntu2004-x86_64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-ubuntu2004-x86_64.tgz + sha256sum: 7bc3547a172530f7a2d7086943d85c8c54d9afa7386024eb745faddd951688c5 {%- endif %} {%- if 'ubuntu2204' in supported_base_os %} ubuntu2204: - version: 2022.1-13300-ubuntu2204-x86_64 - tgz: nice-dcv-2022.1-13300-ubuntu2204-x86_64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-ubuntu2204-x86_64.tgz - sha256sum: 3e431255b30b2a69d145a09d18b308ed3f5fa7eb5a879ba241fb183c45795d40 + version: 2023.0-14852-ubuntu2204-x86_64 + tgz: nice-dcv-2023.0-14852-ubuntu2204-x86_64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-ubuntu2204-x86_64.tgz + sha256sum: 975b37eec1b260b6459400e38ad99c9c7b8bc6b4b0e095da7ed38d7653144fd8 {%- endif %} aarch64: linux: {%- if 'amazonlinux2' in supported_base_os or 'rhel7' in supported_base_os or 'centos7' in supported_base_os %} al2_rhel_centos7: - version: 2022.1-13300-el7-aarch64 - tgz: nice-dcv-2022.1-13300-el7-aarch64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-el7-aarch64.tgz - sha256sum: 62bea89c151c3ad840a9ffd17271227b6a51909e5529a4ff3ec401b37fde1667 + version: 2023.0-14852-el7-aarch64 + tgz: nice-dcv-2023.0-14852-el7-aarch64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-el7-aarch64.tgz + sha256sum: 1c7965898ad963b90047bf05bd23f2b6fd9d6e074d11354687a8a908f11257c3 {%- endif %} {%- if 'rhel8' in supported_base_os or 'centos8' in supported_base_os or 'rocky8' in supported_base_os %} rhel_centos_rocky8: - version: 2022.1-13300-el8-aarch64 - tgz: nice-dcv-2022.1-13300-el8-aarch64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-el8-aarch64.tgz - sha256sum: 9adaae8c52d594008dffc06361aa3848d2e5b37833445b007f15a79306fb672a + version: 2023.0-14852-el8-aarch64 + tgz: nice-dcv-2023.0-14852-el8-aarch64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-el8-aarch64.tgz + sha256sum: 57bb821bd07c6f3aab134caa4c05616ab11b6445562e181d22099d603f917964 {%- endif %} ubuntu: {%- if 'ubuntu1804' in supported_base_os %} ubuntu1804: - version: 2022.1-13300-ubuntu1804-aarch64 - tgz: nice-dcv-2022.1-13300-ubuntu1804-aarch64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-ubuntu1804-aarch64.tgz - sha256sum: cf0d5259254bed4f9777cc64b151e3faa1b4e2adffdf2be5082e64c744ba5b3b + version: 2023.0-14852-ubuntu1804-aarch64.tgz + tgz: nice-dcv-2023.0-14852-ubuntu1804-aarch64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-ubuntu1804-aarch64.tgz + sha256sum: 07d624beb69cfe5a0edc5d22507905ce1ed9a59595cc3e661fde94927de55a5f {%- endif %} {%- if 'ubuntu2204' in supported_base_os %} ubuntu2204: - version: 2022.1-13300-ubuntu2204-aarch64 - tgz: nice-dcv-2022.1-13300-ubuntu2204-aarch64.tgz - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-2022.1-13300-ubuntu2204-x86_64.tgz - sha256sum: 9aedd8b969c18c473c9b7d31e3d960f2d79968342f9ffdaef63d82cc96767088 + version: 2023.0-14852-ubuntu2204-aarch64 + tgz: nice-dcv-2023.0-14852-ubuntu2204-aarch64.tgz + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Servers/nice-dcv-2023.0-14852-ubuntu2204-aarch64.tgz + sha256sum: 9a703bea9a9bf5eeef9c691f69c255049b567acae39e1f16d3b29875685b2544 {%- endif %} agent: x86_64: {%- if 'windows' in supported_base_os %} windows: - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Servers/nice-dcv-server-x64-Release-2022.1-13300.msi - sha256sum: e043eda4ed5421692b60e31ac83e23ce2bc3d3098165b0d50344361c6bb0f808 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent-x64-Release-2023.0-642.msi + sha256sum: 7b70ebb79c5edf33d42c53973047f30d0ed85fbd9a6c1d72686b05805c29eb3f {%- endif %} linux: {%- if 'amazonlinux2' in supported_base_os or 'rhel7' in supported_base_os or 'centos7' in supported_base_os %} al2_rhel_centos7: - version: 2022.1.592-1.el7.x86_64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent-2022.1.592-1.el7.x86_64.rpm - sha256sum: 7825a389900fd89143f8c0deeff0bfb0336bbf59092249211503bbd7d2d12754 + version: 2023.0.642-1.el7.x86_64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent-2023.0.642-1.el7.x86_64.rpm + sha256sum: e7ce42827b462ee9119db54d7bbf4826c58ee81156b2362d9c81f500f434ce40 {%- endif %} {%- if 'rhel8' in supported_base_os or 'centos8' in supported_base_os or 'rocky8' in supported_base_os %} rhel_centos_rocky8: - version: 2022.1.592-1.el8.x86_64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent-2022.1.592-1.el8.x86_64.rpm - sha256sum: b45558cfb7034f5f2cbef9f87cb2db7fc118c80a8c80da55fe8e73be320e4581 + version: 2023.0.642-1.el8.x86_64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent-2023.0.642-1.el8.x86_64.rpm + sha256sum: d1ac5a48a878d2a0a753202a19220fae379fe7121be54bad0008b4478c67e22e {%- endif %} {%- if 'suse12' in supported_base_os %} suse12: - version: 2022.1.592-1.sles12.x86_64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent-2022.1.592-1.sles12.x86_64.rpm - sha256sum: a9fbf7e9e23a7b72f5f5f9e7c14504560f4aea3df13d5e3a93254c6efce2d4a7 + version: 2023.0.642-1.sles12.x86_64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent-2023.0.642-1.sles12.x86_64.rpm + sha256sum: 23d3acb7d9e023f3e85e2f6a01258710fab2d8c7567d63f3c47a15b11b324982 {%- endif %} {%- if 'suse15' in supported_base_os %} suse15: - version: 2022.1.592-1.sles15.x86_64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent-2022.1.592-1.sles15.x86_64.rpm - sha256sum: 93b2575b92b25b8c01e3a979ee005df19d752b326acd10293ee460b40946175b + version: 2023.0.642-1.sles15.x86_64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent-2023.0.642-1.sles15.x86_64.rpm + sha256sum: d41aa3ec0f6910ebdc505a4ddc91113621e44605824e90a7086fb128e8b0f3b8 {%- endif %} ubuntu: {%- if 'ubuntu1804' in supported_base_os %} ubuntu1804: - version: 2022.1.592-1_amd64.ubuntu1804 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent_2022.1.592-1_amd64.ubuntu1804.deb - sha256sum: bdce81b7541f90f86a8e4dbda2f013de344b90a6818c3ce1a285a1fc5ebb7d71 + version: 2023.0.642-1_amd64.ubuntu1804 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent_2023.0.642-1_amd64.ubuntu1804.deb + sha256sum: b03f1e80585ae598681d87ef75ede068f64928653458780d7ccb26ab15127e4b {%- endif %} {%- if 'ubuntu2004' in supported_base_os %} ubuntu2004: - version: 2022.1.592-1_amd64.ubuntu2004 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent_2022.1.592-1_amd64.ubuntu2004.deb - sha256sum: 37f354106f136453ea64b21ccec2f20855913dbd547abd9503424693243f3851 + version: 2023.0.642-1_amd64.ubuntu2004 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent_2023.0.642-1_amd64.ubuntu2004.deb + sha256sum: 9133b41c29445e2181077cf1823eeae8b984da815e74b3406b2a50dcd924b010 {%- endif %} {%- if 'ubuntu2204' in supported_base_os %} ubuntu2204: - version: 2022.1.592-1_amd64.ubuntu2204 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent_2022.1.592-1_amd64.ubuntu2204.deb - sha256sum: 44a389dc9fd9813831d605370d002c29c4d8ff1a3671d3b1cb4327965ea3198c + version: 2023.0.642-1_amd64.ubuntu2204 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent_2023.0.642-1_amd64.ubuntu2204.deb + sha256sum: f2577536d2b0e8bddf71cd257168e8d8ceb0814f4f5e02d4015776cdc8144fa1 {%- endif %} aarch64: linux: {%- if 'amazonlinux2' in supported_base_os or 'rhel7' in supported_base_os or 'centos7' in supported_base_os %} al2_rhel_centos7: - version: 2022.1.592-1.el7.aarch64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent-2022.1.592-1.el7.aarch64.rpm - sha256sum: 81bba492f340ce0f9930d7a7c698f07eca57e9274ea6167da5d6c6103527465b + version: 2023.0.642-1.el7.aarch64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent-2023.0.642-1.el7.aarch64.rpm + sha256sum: 875f73cbc0f7cc308e74334862c89e449736648a6e4c5747984c6c131a17b069 {%- endif %} {%- if 'rhel8' in supported_base_os or 'centos8' in supported_base_os or 'rocky8' in supported_base_os %} rhel_centos_rocky8: - version: 2022.1.592-1.el8.aarch64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent-2022.1.592-1.el8.aarch64.rpm - sha256sum: b70531548f4d648399df202529b20154d0cf62b58eb63ec53985faf9f0c1de42 + version: 2023.0.642-1.el8.aarch64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent-2023.0.642-1.el8.aarch64.rpm + sha256sum: 4fbab9b6d0658ce5c3d1087dd2feb4f9ec1b84bca102527b1a755c452de19501 {%- endif %} ubuntu: {%- if 'ubuntu1804' in supported_base_os %} ubuntu1804: - version: 2022.1.592-1_arm64.ubuntu1804 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent_2022.1.592-1_arm64.ubuntu1804.deb - sha256sum: 92017952fb02a773f1c411202900f69ffed945c4778635103cb13aaf5ebacdf0 + version: 2023.0.642-1_arm64.ubuntu1804 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent_2023.0.642-1_arm64.ubuntu1804.deb + sha256sum: 80c69e60d13590fe2a8a70c6b8026b04d183b1cac9ebae570cfd70c654e8c104 {%- endif %} {%- if 'ubuntu2204' in supported_base_os %} ubuntu2204: - version: 2022.1.592-1_arm64.ubuntu2204 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerAgents/nice-dcv-session-manager-agent_2022.1.592-1_arm64.ubuntu2204.deb - sha256sum: 2ee0829620f30855d76029b5de0b222bff262325c28f0b5b1e67cad3995d78b7 + version: 2023.0.642-1_arm64.ubuntu2204 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerAgents/nice-dcv-session-manager-agent_2023.0.642-1_arm64.ubuntu2204.deb + sha256sum: b19bc62c189a6a0b8439536e5640b5cefe4984b3f2ffeb0a383e96035844a7e9 {%- endif %} connection_gateway: x86_64: linux: {%- if 'amazonlinux2' in supported_base_os or 'rhel7' in supported_base_os or 'centos7' in supported_base_os %} al2_rhel_centos7: - version: 2022.1.377-1.el7.x86_64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway-2022.1.377-1.el7.x86_64.rpm - sha256sum: 3cdd671482fd58670dc037118709fb7810e8dcfbe040799a8f991ffebd5dafa4 + version: 2023.0.531-1.el7.x86_64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway-2023.0.531-1.el7.x86_64.rpm + sha256sum: 7abc061b94807510c8284849fd2545b170dc8d47954f1afa9f38966f47ce15ae {%- endif %} {%- if 'rhel8' in supported_base_os or 'centos8' in supported_base_os or 'rocky8' in supported_base_os %} rhel_centos_rocky8: - version: 2022.1.377-1.el8.x86_64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway-2022.1.377-1.el8.x86_64.rpm - sha256sum: 09de6b4debab90c14ba09c24ee5fa59b2e40b30d56ab4c09fcf93efef16883ef + version: 2023.0.531-1.el8.x86_64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway-2023.0.531-1.el8.x86_64.rpm + sha256sum: 67a71866996f1bd32b22d9cadb695ca357d165270e2d4373e5d4c9e2a56cb974 {%- endif %} ubuntu: {%- if 'ubuntu1804' in supported_base_os %} ubuntu1804: - version: 2022.1.377-1_amd64.ubuntu1804 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway_2022.1.377-1_amd64.ubuntu1804.deb - sha256sum: 806f9bf6c4d367168796a4ad307a39c6386c983f6c3f8e2ee31339a7a3a5a71f + version: 2023.0.531-1_amd64.ubuntu1804 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway_2023.0.531-1_amd64.ubuntu1804.deb + sha256sum: b710ecb96e350ea3f8d53926c60f8001441c1f4b0db4f476ca282c720d6a265e {%- endif %} {%- if 'ubuntu2004' in supported_base_os %} ubuntu2004: - version: 2022.1.377-1_amd64.ubuntu2004 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway_2022.1.377-1_amd64.ubuntu2004.deb - sha256sum: d5c0ecc0b70ff51d0670c682c1a298300cf18a8d47268853b8d3532ed6f28115 + version: 2023.0.531-1_amd64.ubuntu2004 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway_2023.0.531-1_amd64.ubuntu2004.deb + sha256sum: 87d8e3269f3bcfc648f80f7117e8d486ee7f6605449dc8c73f0a394a1c1035be {%- endif %} {%- if 'ubuntu2204' in supported_base_os %} ubuntu2204: - version: 2022.1.377-1_amd64.ubuntu2204 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway_2022.1.377-1_amd64.ubuntu2204.deb - sha256sum: d52ba3712df30b0452bb9c3618439cdc22d7527bb0fccc9ae445169c31a09c80 + version: 2023.0.531-1_amd64.ubuntu2204 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway_2023.0.531-1_amd64.ubuntu2204.deb + sha256sum: b5c48779cec33a9fe26829ca0f28aa15702151b8ebc42d354f95254db5dedcb3 {%- endif %} aarch64: linux: {%- if 'amazonlinux2' in supported_base_os or 'rhel7' in supported_base_os or 'centos7' in supported_base_os %} al2_rhel_centos7: - version: 2022.1.377-1.el7.aarch64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway-2022.1.377-1.el7.aarch64.rpm - sha256sum: dab04b1b20e91e664dd51d81190c6030e7104d8455de9f72bad946c591f57126 + version: 2023.0.531-1.el7.aarch64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway-2023.0.531-1.el7.aarch64.rpm + sha256sum: 77d29efa59e08d669058037a2a8aba6b0d51822b8771e2abdb42a0c9f425685f {%- endif %} {%- if 'rhel8' in supported_base_os or 'centos8' in supported_base_os or 'rocky8' in supported_base_os %} rhel_centos_rocky8: - version: 2022.1.377-1.el8.aarch64 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway-2022.1.377-1.el8.aarch64.rpm - sha256sum: 2babf08b13587ab0542185d5d9056dfe974b1926f7daea035900e2ed0d0399eb + version: 2023.0.531-1.el8.aarch64 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway-2023.0.531-1.el8.aarch64.rpm + sha256sum: b55f344eff9ed07e3d706da24f369f7e93d2e106edc4b3589569c06308571664 {%- endif %} ubuntu: {%- if 'ubuntu1804' in supported_base_os %} ubuntu1804: - version: 2022.1.377-1_arm64.ubuntu1804 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway_2022.1.377-1_arm64.ubuntu1804.deb - sha256sum: 04fb476168a4a9affa02e3a70512206b44171409b280a63bdbdaa4fa4b2fb8bc + version: 2023.0.531-1_arm64.ubuntu1804 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway_2023.0.531-1_arm64.ubuntu1804.deb + sha256sum: 1e7936fc95e9d4e7afb66ed044e1030ac51bc6bdb6ae2023e7389f3aa0890681 {%- endif %} {%- if 'ubuntu2004' in supported_base_os %} ubuntu2004: - version: 2022.1.377-1_arm64.ubuntu2004 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway_2022.1.377-1_arm64.ubuntu2004.deb - sha256sum: c21090d5c0fbd988655695d55f2bc43b19556b3f4910915f23edabf8ac5829db + version: 2023.0-531-1_arm64.ubuntu2004 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway_2023.0.531-1_arm64.ubuntu2004.deb + sha256sum: 2f940ed9f345370f1b46754d6ff3fd3348dad4030d162dff0d34cb56110c4d8a {%- endif %} {%- if 'ubuntu2204' in supported_base_os %} ubuntu2204: - version: 2022.1.377-1_arm64.ubuntu2204 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Gateway/nice-dcv-connection-gateway_2022.1.377-1_arm64.ubuntu2204.deb - sha256sum: c8004c51f026b90a2df8b560b381c0a725be531ff721e89182105376c35d568a + version: 2023.0-531-1_arm64.ubuntu2204 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Gateway/nice-dcv-connection-gateway_2023.0.531-1_arm64.ubuntu2204.deb + sha256sum: 727c7ff0882edafa061bf18e9d7840c55cadc3ca1f716618bff7f70e4cf0919d {%- endif %} broker: linux: {%- if 'amazonlinux2' in supported_base_os or 'rhel7' in supported_base_os or 'centos7' in supported_base_os %} al2_rhel_centos7: - version: 2022.1.355-1.el7.noarch - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerBrokers/nice-dcv-session-manager-broker-2022.1.355-1.el7.noarch.rpm - sha256sum: db3bad6cf7b295b3f2b63ab8ebb4dd3e41d3caaaceee1443213fc6472512d5a9 + version: 2023.0.392-1.el7.noarch + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerBrokers/nice-dcv-session-manager-broker-2023.0.392-1.el7.noarch.rpm + sha256sum: fc0f29de9d2c9d3799a6b2235c01ba48159fede6bb5e7500cb8cf121a27471be {%- endif %} {%- if 'rhel8' in supported_base_os or 'centos8' in supported_base_os or 'rocky8' in supported_base_os %} rhel_centos_rocky8: - version: 2022.1.355-1.el8.noarch - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerBrokers/nice-dcv-session-manager-broker-2022.1.355-1.el8.noarch.rpm - sha256sum: 20fedb637e2a8e689a34a7b8c6af33c5fc026bf6a221d48afbd49c2e0dc8d8b9 + version: 2023.0.392-1.el8.noarch + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerBrokers/nice-dcv-session-manager-broker-2023.0.392-1.el8.noarch.rpm + sha256sum: 5fc81cbc697f944c67127b529c620789f555fdc2585b07040a77c72ac0927f95 {%- endif %} ubuntu: {%- if 'ubuntu1804' in supported_base_os %} ubuntu1804: - version: 2022.1.355-1_all.ubuntu1804 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerBrokers/nice-dcv-session-manager-broker_2022.1.355-1_all.ubuntu1804.deb - sha256sum: 4c8ef05ee5f0abafc77d22db4d46e87180bf9c1a449e788bc8719a9a6e26821b + version: 2023.0.392-1_all.ubuntu1804 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerBrokers/nice-dcv-session-manager-broker_2023.0.392-1_all.ubuntu1804.deb + sha256sum: 662fb5d6da94898c30c565612a5fa11662757d431c02397704a7089ef9fe0e08 {%- endif %} {%- if 'ubuntu2004' in supported_base_os %} ubuntu2004: - version: 2022.1.355-1_all.ubuntu2004 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerBrokers/nice-dcv-session-manager-broker_2022.1.355-1_all.ubuntu2004.deb - sha256sum: 64ade6670e609148a89dd807e6d15650d6113efc41b6f37fbac8e0761dfb78a4 + version: 2023.0.392-1_all.ubuntu2004 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerBrokers/nice-dcv-session-manager-broker_2023.0.392-1_all.ubuntu2004.deb + sha256sum: 2511816811712a3a1354c9cee4c0305dce3d396fa470341f30fe9168264312d4 {%- endif %} {%- if 'ubuntu2204' in supported_base_os %} ubuntu2204: - version: 2022.1.355-1_all.ubuntu2204 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/SessionManagerBrokers/nice-dcv-session-manager-broker_2022.1.355-1_all.ubuntu2204.deb - sha256sum: 4dda3777a205ee5ad67de9d87585914682c3b8cf9bccd21da9ad75f0ecda7be6 + version: 2023.0.392-1_all.ubuntu2204 + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/SessionManagerBrokers/nice-dcv-session-manager-broker_2023.0.392-1_all.ubuntu2204.deb + sha256sum: 3c058d4849625c553eb0939c4b4bbcb9de4a5473ca0692eea058491aa2221f2a {%- endif %} clients: windows: msi: label: MSI - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-client-Release-2022.1-8261.msi + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-client-Release-2023.0-8655.msi zip: label: ZIP - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-client-Release-portable-2022.1-8261.zip + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-client-Release-portable-2023.0-8655.zip macos: m1: label: M1 Chip - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer-2022.1.4279.arm64.dmg + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-viewer-2023.0.5388.arm64.dmg intel: label: Intel Chip - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer-2022.1.4279.x86_64.dmg + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-viewer-2023.0.5388.x86_64.dmg linux: rhel_centos7: label: RHEL 7 | Cent OS 7 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer-2022.1.4251-1.el7.x86_64.rpm + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-viewer-2023.0.5388-1.el7.x86_64.rpm rhel_centos_rocky8: label: RHEL 8 | Cent OS 8 | Rocky Linux 8 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer-2022.1.4251-1.el8.x86_64.rpm + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-viewer-2023.0.5388-1.el8.x86_64.rpm suse15: label: SUSE Enterprise Linux 15 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer-2022.1.4251-1.sles15.x86_64.rpm + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-viewer-2023.0.5388-1.sles15.x86_64.rpm ubuntu: ubuntu1804: label: Ubuntu 18.04 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer_2022.1.4251-1_amd64.ubuntu1804.deb + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-viewer_2023.0.5388-1_amd64.ubuntu1804.deb ubuntu2004: label: Ubuntu 20.04 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer_2022.1.4251-1_amd64.ubuntu2004.deb + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-viewer_2023.0.5388-1_amd64.ubuntu2004.deb ubuntu2204: label: Ubuntu 22.04 - url: https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer_2022.1.4251-1_amd64.ubuntu2204.deb + url: https://d1uj6qtbmh3dt5.cloudfront.net/2023.0/Clients/nice-dcv-viewer_2023.0.5388-1_amd64.ubuntu2204.deb {%- endif %} gpu_settings: diff --git a/source/idea/idea-administrator/resources/config/templates/virtual-desktop-controller/settings.yml b/source/idea/idea-administrator/resources/config/templates/virtual-desktop-controller/settings.yml index 7ea353b..918e793 100644 --- a/source/idea/idea-administrator/resources/config/templates/virtual-desktop-controller/settings.yml +++ b/source/idea/idea-administrator/resources/config/templates/virtual-desktop-controller/settings.yml @@ -11,6 +11,15 @@ server: enable_metrics: false graceful_shutdown_timeout: 10 api_context_path: /{{ module_id }} + # Additional USB Remotization AllowList defined in list format + # Default is an empty list / no additional devices considered for USB remotization + # See docs: + # Admin Guide: https://docs.aws.amazon.com/dcv/latest/adminguide/manage-usb-remote.html + # User Guide: https://docs.aws.amazon.com/dcv/latest/userguide/using-usb.html + usb_remotization: [] +# Example DCV USB Filter string: +# - "Q-LightLampHIDDevice,3,0,0,1240,59196,0,0" + controller: autoscaling: @@ -153,6 +162,25 @@ dcv_session: - m6g deny: [] # Supports both instance families and types. E.g. specify t3 for family and t3.large for instance type quic_support: {{ dcv_session_quic_support | lower }} + + # + # By default, the eVDI subnets match the cluster private subnets. + # Here you can specify eVDI-specific subnets as an alternative to using the cluster.network.private_subnets + # + network: + private_subnets: + {{ utils.to_yaml(private_subnet_ids) | indent(6) }} + + # Supported eVDI randomize_subnets settings: + # True - Randomize the subnets (dcv_session.network.private_subnets or cluster.network.private_subnets) for deployment + # False - (default) Use the subnets (dcv_session.network.private_subnets or cluster.network.private_subnets) in the order specified in the configuration. + randomize_subnets: False + + # Retry methods when encountering an eVDI deployment error + # True - (default) Immediately retry the next subnet (ordered or random from subnet_deployment_method) + # False - error the request on first error. This may be desired to avoid cross-AZ charges. + subnet_autoretry: True + # instance metadata access method metadata_http_tokens: "required" # supported values are "required" for IMDSv2 or "optional" for IMDSv1 notifications: diff --git a/source/idea/idea-administrator/resources/installer_policies/idea-admin-create-policy.yml b/source/idea/idea-administrator/resources/installer_policies/idea-admin-create-policy.yml index 30cd3da..401c995 100644 --- a/source/idea/idea-administrator/resources/installer_policies/idea-admin-create-policy.yml +++ b/source/idea/idea-administrator/resources/installer_policies/idea-admin-create-policy.yml @@ -73,6 +73,7 @@ Statement: - elasticfilesystem:CreateMountTarget - elasticfilesystem:DescribeFileSystems - elasticfilesystem:DescribeMountTargets + - elasticfilesystem:PutFileSystemPolicy - elasticfilesystem:PutLifecycleConfiguration - elasticloadbalancing:AddTags - elasticloadbalancing:CreateListener @@ -115,6 +116,7 @@ Statement: - lambda:GetFunction - lambda:InvokeFunction - logs:CreateLogGroup + - logs:CreateLogStream - logs:DescribeLogGroups - logs:PutRetentionPolicy - route53resolver:AssociateResolverEndpointIpAddress diff --git a/source/idea/idea-administrator/resources/lambda_functions/idea_ec2_state_event_transformation_lambda/handler.py b/source/idea/idea-administrator/resources/lambda_functions/idea_ec2_state_event_transformation_lambda/handler.py index 617f5b7..30b34f4 100644 --- a/source/idea/idea-administrator/resources/lambda_functions/idea_ec2_state_event_transformation_lambda/handler.py +++ b/source/idea/idea-administrator/resources/lambda_functions/idea_ec2_state_event_transformation_lambda/handler.py @@ -18,6 +18,7 @@ import os import json import logging +import re logger = logging.getLogger() logger.setLevel(logging.INFO) @@ -35,6 +36,7 @@ def handler(event, _): cluster_name_tag_key = os.environ.get('IDEA_CLUSTER_NAME_TAG_KEY') cluster_name_tag_value = os.environ.get('IDEA_CLUSTER_NAME_TAG_VALUE') + idea_tag_prefix = os.environ.get('IDEA_TAG_PREFIX') instance_id = event['detail']['instance-id'] state = event['detail']['state'] @@ -44,11 +46,12 @@ def handler(event, _): message_attributes = {} for tags in ec2instance.tags: - event['detail']['tags'][tags["Key"]] = tags["Value"] - message_attributes[tags["Key"].replace(':', '_')] = { - 'DataType': 'String', - 'StringValue': tags["Value"] - } + if tags["Key"].startswith(idea_tag_prefix): + event['detail']['tags'][tags["Key"]] = tags["Value"] + message_attributes[re.sub(r"[^a-zA-Z0-9_\-\.]+","_",tags["Key"]).strip()] = { + 'DataType': 'String', + 'StringValue': tags["Value"] + } if tags["Key"] == cluster_name_tag_key and tags["Value"] == cluster_name_tag_value: cluster_match = True diff --git a/source/idea/idea-administrator/resources/policies/_templates/custom-kms-key.yml b/source/idea/idea-administrator/resources/policies/_templates/custom-kms-key.yml index 9d0efba..9449931 100644 --- a/source/idea/idea-administrator/resources/policies/_templates/custom-kms-key.yml +++ b/source/idea/idea-administrator/resources/policies/_templates/custom-kms-key.yml @@ -4,6 +4,14 @@ - kms:Decrypt - kms:GenerateDataKey Resource: - - '{{ context.arns.kms_key_arn }}' + {{ context.utils.to_yaml(context.arns.kms_key_arn) | indent(6) }} + Effect: Allow + {%- endif %} + {%- if context.config.get_string('cluster.dynamodb.kms_key_id') %} + - Action: + - kms:DescribeKey + - kms:CreateGrant + Resource: + - '{{ context.arns.kms_dynamodb_key_arn }}' Effect: Allow {%- endif %} diff --git a/source/idea/idea-administrator/resources/policies/analytics-sink-lambda.yml b/source/idea/idea-administrator/resources/policies/analytics-sink-lambda.yml index 1e3b59b..4b42e0a 100644 --- a/source/idea/idea-administrator/resources/policies/analytics-sink-lambda.yml +++ b/source/idea/idea-administrator/resources/policies/analytics-sink-lambda.yml @@ -12,6 +12,15 @@ Statement: - {{ context.arns.get_kinesis_arn() }} Effect: Allow + {%- if context.config.get_string('analytics.kinesis.kms_key_id') %} + - Action: + - kms:GenerateDataKey + - kms:Decrypt + Resource: + - '{{ context.arns.kms_kinesis_key_arn }}' + Effect: Allow + {%- endif %} + - Action: - logs:CreateLogGroup - logs:CreateLogStream @@ -25,6 +34,15 @@ Statement: - es:ESHttpPut Resource: '*' + {%- if context.config.get_string('analytics.opensearch.kms_key_id') %} + - Action: + - kms:GenerateDataKey + - kms:Decrypt + Resource: + - '{{ context.arns.kms_opensearch_key_arn }}' + Effect: Allow + {%- endif %} + - Effect: Allow Action: - ec2:CreateNetworkInterface diff --git a/source/idea/idea-administrator/resources/policies/controller-ssm-command-pass-role.yml b/source/idea/idea-administrator/resources/policies/controller-ssm-command-pass-role.yml index c52113e..83196f8 100644 --- a/source/idea/idea-administrator/resources/policies/controller-ssm-command-pass-role.yml +++ b/source/idea/idea-administrator/resources/policies/controller-ssm-command-pass-role.yml @@ -27,3 +27,12 @@ Statement: Resource: - '{{ context.arns.get_log_group_arn(context.config.get_module_id("virtual-desktop-controller") + "/dcv-session/*") }}' Effect: Allow + + {%- if context.config.get_string('cluster.sns.kms_key_id') %} + - Action: + - kms:GenerateDataKey + - kms:Decrypt + Resource: + - '{{ context.arns.kms_sns_key_arn }}' + Effect: Allow + {%- endif %} diff --git a/source/idea/idea-administrator/resources/policies/custom-resource-self-signed-certificate.yml b/source/idea/idea-administrator/resources/policies/custom-resource-self-signed-certificate.yml index 553a174..719e02a 100644 --- a/source/idea/idea-administrator/resources/policies/custom-resource-self-signed-certificate.yml +++ b/source/idea/idea-administrator/resources/policies/custom-resource-self-signed-certificate.yml @@ -27,7 +27,7 @@ Statement: - kms:GenerateDataKey - kms:Decrypt Resource: - - '{{ context.arns.kms_key_arn }}' + - '{{ context.arns.kms_secretsmanager_key_arn }}' Effect: Allow {%- endif %} @@ -39,4 +39,4 @@ Statement: - secretsmanager:TagResource Resource: '*' Effect: Allow - Sid: SecretManagerPermissions + Sid: SecretsManagerPermissions diff --git a/source/idea/idea-administrator/resources/policies/custom-resource-update-cluster-settings.yml b/source/idea/idea-administrator/resources/policies/custom-resource-update-cluster-settings.yml index 234fd7f..66fe7f1 100644 --- a/source/idea/idea-administrator/resources/policies/custom-resource-update-cluster-settings.yml +++ b/source/idea/idea-administrator/resources/policies/custom-resource-update-cluster-settings.yml @@ -15,3 +15,12 @@ Statement: Resource: {{ context.utils.to_yaml(context.arns.cluster_config_ddb_arn) | indent(6) }} Effect: Allow + + {%- if context.config.get_string('cluster.dynamodb.kms_key_id') %} + - Action: + - kms:GenerateDataKey + - kms:Decrypt + Resource: + - '{{ context.arns.kms_dynamodb_key_arn }}' + Effect: Allow + {%- endif %} diff --git a/source/idea/idea-administrator/resources/policies/ec2state-event-transformer.yml b/source/idea/idea-administrator/resources/policies/ec2state-event-transformer.yml index 56c0e5c..7f3b20c 100644 --- a/source/idea/idea-administrator/resources/policies/ec2state-event-transformer.yml +++ b/source/idea/idea-administrator/resources/policies/ec2state-event-transformer.yml @@ -1,20 +1,29 @@ Version: '2012-10-17' Statement: -- Action: - - logs:CreateLogGroup - Resource: "{{ context.arns.get_lambda_log_group_arn() }}" - Effect: Allow - Sid: CloudWatchLogsPermissions -- Action: - - logs:CreateLogStream - - logs:PutLogEvents - Resource: "{{ context.arns.lambda_log_stream_arn }}" - Effect: Allow - Sid: CloudWatchLogStreamPermissions -- Action: - - ec2:DescribeInstances - Resource: "*" - Effect: Allow -- Effect: Allow - Action: sns:Publish - Resource: "{{ context.arns.get_sns_arn('*ec2-state-change*') }}" + - Action: + - logs:CreateLogGroup + Resource: "{{ context.arns.get_lambda_log_group_arn() }}" + Effect: Allow + Sid: CloudWatchLogsPermissions + - Action: + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "{{ context.arns.lambda_log_stream_arn }}" + Effect: Allow + Sid: CloudWatchLogStreamPermissions + - Action: + - ec2:DescribeInstances + Resource: "*" + Effect: Allow + - Effect: Allow + Action: sns:Publish + Resource: "{{ context.arns.get_sns_arn('*ec2-state-change*') }}" + + {%- if context.config.get_string('cluster.sns.kms_key_id') %} + - Action: + - kms:GenerateDataKey + - kms:Decrypt + Resource: + - '{{ context.arns.kms_sns_key_arn }}' + Effect: Allow + {%- endif %} diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/.delete_cluster.py.swp b/source/idea/idea-administrator/src/ideaadministrator/app/.delete_cluster.py.swp new file mode 100644 index 0000000..80e30d9 Binary files /dev/null and b/source/idea/idea-administrator/src/ideaadministrator/app/.delete_cluster.py.swp differ diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/analytics.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/analytics.py index 4fea0b9..42f2295 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/analytics.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/analytics.py @@ -23,7 +23,8 @@ from aws_cdk import ( aws_ec2 as ec2, aws_iam as iam, - aws_opensearchservice as opensearch + aws_opensearchservice as opensearch, + aws_kms as kms ) @@ -49,6 +50,7 @@ def __init__( ebs: Optional[opensearch.EbsOptions] = None, enable_version_upgrade: Optional[bool] = None, encryption_at_rest: Optional[opensearch.EncryptionAtRestOptions] = None, + kms_key_arn: Optional[kms.IKey] = None, enforce_https: Optional[bool] = None, fine_grained_access_control: Optional[opensearch.AdvancedSecurityOptions] = None, logging: Optional[opensearch.LoggingOptions] = None, @@ -88,7 +90,8 @@ def __init__( if encryption_at_rest is None: encryption_at_rest = opensearch.EncryptionAtRestOptions( - enabled=True + enabled=True, + kms_key=kms_key_arn ) if ebs is None: diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/common.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/common.py index 0992d6e..e86e15a 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/common.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/common.py @@ -598,7 +598,18 @@ def __init__(self, context: AdministratorContext, name: str, scope: constructs.C class KinesisStream(SocaBaseConstruct, kinesis.Stream): def __init__(self, context: AdministratorContext, name: str, scope: constructs.Construct, stream_name: str, stream_mode: kinesis.StreamMode, shard_count: Optional[int]): + self.context = context + kms_key_id = self.context.config().get_string('analytics.kinesis.kms_key_id') + + if kms_key_id is not None: + kms_key_arn = self.get_kms_key_arn(kms_key_id) + kinesis_encryption_key = kms.Key.from_key_arn(scope=scope, id=f'kinesis-kms-key', key_arn=kms_key_arn) + else: + kinesis_encryption_key = kms.Alias.from_alias_name(scope=scope, id=f'kinesis-kms-key-default', alias_name='alias/aws/kinesis') + super().__init__(context, name, scope, stream_name=f'{context.cluster_name()}-{stream_name}', stream_mode=stream_mode, + encryption=kinesis.StreamEncryption.KMS, + encryption_key=kinesis_encryption_key, shard_count=shard_count) diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/existing_resources.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/existing_resources.py index 6c5fdcb..734f44d 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/existing_resources.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/constructs/existing_resources.py @@ -102,7 +102,7 @@ def get_private_subnets(self) -> List[ec2.ISubnet]: private_subnet_ids = self.get_private_subnet_ids() if Utils.is_empty(private_subnet_ids): self._private_subnets = [] - return self._public_subnets + return self._private_subnets result = [] if self.vpc.private_subnets is not None: @@ -113,7 +113,6 @@ def get_private_subnets(self) -> List[ec2.ISubnet]: for subnet in self.vpc.isolated_subnets: if subnet.subnet_id in private_subnet_ids: result.append(subnet) - # sort based on index in private_subnets[] configuration result.sort(key=lambda x: private_subnet_ids.index(x.subnet_id)) diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/.cluster_stack.py.swp b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/.cluster_stack.py.swp new file mode 100644 index 0000000..d397ddc Binary files /dev/null and b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/.cluster_stack.py.swp differ diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/analytics_stack.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/analytics_stack.py index d208cf3..44475dc 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/analytics_stack.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/analytics_stack.py @@ -40,6 +40,7 @@ aws_elasticloadbalancingv2 as elbv2, aws_kinesis as kinesis, aws_lambda as lambda_, + aws_kms as kms, aws_lambda_event_sources as lambda_event_sources, aws_logs as logs ) @@ -99,8 +100,17 @@ def __init__(self, scope: constructs.Construct, IdeaNagSuppression(rule_id='AwsSolutions-IAM5', reason='CDK L2 construct does not support custom LogGroup permissions'), IdeaNagSuppression(rule_id='AwsSolutions-IAM4', reason='Usage is required for Service Linked Role'), IdeaNagSuppression(rule_id='AwsSolutions-L1', reason='CDK L2 construct does not offer options to customize the Lambda runtime'), + IdeaNagSuppression(rule_id='AwsSolutions-KDS3', reason='Kinesis Data Stream is encrypted with customer-managed KMS key') ] ) + data_nodes = self.context.config().get_int('analytics.opensearch.data_nodes', required=True) + if data_nodes == 1: + self.add_nag_suppression( + construct=self.stack, + suppressions=[ + IdeaNagSuppression(rule_id='AwsSolutions-OS7', reason='OpenSearch domain has 1 data node disabling Zone Awareness') + ] + ) self.build_analytics_input_stream() self.build_cluster_settings() @@ -205,6 +215,10 @@ def build_opensearch(self): app_log_removal_policy = self.context.config().get_string('analytics.opensearch.logging.app_log_removal_policy', default='DESTROY') search_log_removal_policy = self.context.config().get_string('analytics.opensearch.logging.search_log_removal_policy', default='DESTROY') slow_index_log_removal_policy = self.context.config().get_string('analytics.opensearch.logging.slow_index_log_removal_policy', default='DESTROY') + kms_key_id = self.context.config().get_string('analytics.opensearch.kms_key_id') + kms_key_arn = None + if kms_key_id is not None: + kms_key_arn = kms.Key.from_key_arn(self.stack, 'opensearch-kms-key', self.get_kms_key_arn(key_id=kms_key_id)) self.opensearch = OpenSearch( context=self.context, @@ -217,6 +231,7 @@ def build_opensearch(self): ebs_volume_size=ebs_volume_size, removal_policy=cdk.RemovalPolicy(removal_policy), node_to_node_encryption=node_to_node_encryption, + kms_key_arn=kms_key_arn, create_service_linked_role=create_service_linked_role, logging=opensearch.LoggingOptions( slow_search_log_enabled=self.context.config().get_bool('analytics.opensearch.logging.slow_search_log_enabled', required=True), diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/bastion_host_stack.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/bastion_host_stack.py index 904ef10..9ab83c1 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/bastion_host_stack.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/bastion_host_stack.py @@ -29,7 +29,8 @@ import aws_cdk as cdk from aws_cdk import ( aws_ec2 as ec2, - aws_route53 as route53 + aws_route53 as route53, + aws_kms as kms ) import constructs @@ -118,16 +119,21 @@ def build_ec2_instance(self): enable_detailed_monitoring = self.context.config().get_bool('bastion-host.ec2.enable_detailed_monitoring', default=False) enable_termination_protection = self.context.config().get_bool('bastion-host.ec2.enable_termination_protection', default=False) metadata_http_tokens = self.context.config().get_string('bastion-host.ec2.metadata_http_tokens', required=True) - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, 'no_proxy': no_proxy } + kms_key_id = self.context.config().get_string('cluster.ebs.kms_key_id', required=False, default=None) + if kms_key_id is not None: + kms_key_arn = self.get_kms_key_arn(kms_key_id) + ebs_kms_key = kms.Key.from_key_arn(scope=self.stack, id=f'ebs-kms-key', key_arn=kms_key_arn) + else: + ebs_kms_key = kms.Alias.from_alias_name(scope=self.stack, id=f'ebs-kms-key-default', alias_name='alias/aws/ebs') instance_profile_name = self.bastion_host_instance_profile.instance_profile_name security_group = self.cluster.get_security_group(constants.MODULE_BASTION_HOST) @@ -160,6 +166,8 @@ def build_ec2_instance(self): block_devices=[ec2.BlockDevice( device_name=block_device_name, volume=ec2.BlockDeviceVolume(ebs_device=ec2.EbsDeviceProps( + encrypted=True, + kms_key=ebs_kms_key, volume_size=volume_size, volume_type=ec2.EbsDeviceVolumeType.GP3 ) diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/cluster_manager_stack.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/cluster_manager_stack.py index 1462f81..bc57f6f 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/cluster_manager_stack.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/cluster_manager_stack.py @@ -33,7 +33,8 @@ aws_cognito as cognito, aws_sqs as sqs, aws_elasticloadbalancingv2 as elbv2, - aws_autoscaling as asg + aws_autoscaling as asg, + aws_kms as kms ) import constructs @@ -247,16 +248,21 @@ def build_auto_scaling_group(self): rolling_update_min_instances_in_service = self.context.config().get_int('cluster-manager.ec2.autoscaling.rolling_update_policy.min_instances_in_service', default=1) rolling_update_pause_time_minutes = self.context.config().get_int('cluster-manager.ec2.autoscaling.rolling_update_policy.pause_time_minutes', default=15) metadata_http_tokens = self.context.config().get_string('cluster-manager.ec2.autoscaling.metadata_http_tokens', required=True) - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, 'no_proxy': no_proxy } + kms_key_id = self.context.config().get_string('cluster.ebs.kms_key_id', required=False, default=None) + if kms_key_id is not None: + kms_key_arn = self.get_kms_key_arn(kms_key_id) + ebs_kms_key = kms.Key.from_key_arn(scope=self.stack, id=f'ebs-kms-key', key_arn=kms_key_arn) + else: + ebs_kms_key = kms.Alias.from_alias_name(scope=self.stack, id=f'ebs-kms-key-default', alias_name='alias/aws/ebs') if is_public: vpc_subnets = ec2.SubnetSelection( @@ -291,6 +297,8 @@ def build_auto_scaling_group(self): block_devices=[ec2.BlockDevice( device_name=block_device_name, volume=ec2.BlockDeviceVolume(ebs_device=ec2.EbsDeviceProps( + encrypted=True, + kms_key=ebs_kms_key, volume_size=volume_size, volume_type=ec2.EbsDeviceVolumeType.GP3 )) diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/cluster_stack.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/cluster_stack.py index cce0066..6ce6dba 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/cluster_stack.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/cluster_stack.py @@ -135,7 +135,9 @@ def __init__(self, scope: constructs.Construct, self.oauth_credentials_lambda: Optional[LambdaFunction] = None self.solution_metrics_lambda: Optional[LambdaFunction] = None + self.solution_metrics_lambda_policy: Optional[Policy] = None self.cluster_settings_lambda: Optional[LambdaFunction] = None + self.cluster_settings_lambda_policy: Optional[Policy] = None self.ec2_events_sns_topic: Optional[SNSTopic] = None @@ -266,7 +268,7 @@ def build_backups(self): backup_vault_kms_key_id = self.context.config().get_string('cluster.backups.backup_vault.kms_key_id', default=None) backup_vault_encryption_key = None if Utils.is_not_empty(backup_vault_kms_key_id): - backup_vault_encryption_key = kms.Key.from_lookup(self.stack, 'backup-vault-kms-key', self.get_kms_key_arn(key_id=backup_vault_kms_key_id)) + backup_vault_encryption_key = kms.Key.from_key_arn(self.stack, 'backup-vault-kms-key', self.get_kms_key_arn(key_id=backup_vault_kms_key_id)) backup_vault = backup.BackupVault( self.stack, 'backup-vault', backup_vault_name=f'{self.cluster_name}-{self.module_id}-backup-vault', @@ -565,12 +567,12 @@ def build_cluster_settings_lambda(self): description=f'Role for cluster-settings lambda function for Cluster: {self.cluster_name}', assumed_by=['lambda']) - cluster_settings_lambda_role.attach_inline_policy(Policy( + self.cluster_settings_lambda_policy = Policy( context=self.context, name=f'{lambda_name}-policy', scope=self.stack, policy_template_name='custom-resource-update-cluster-settings.yml' - )) + ) self.cluster_settings_lambda = LambdaFunction( context=self.context, @@ -585,6 +587,8 @@ def build_cluster_settings_lambda(self): role=cluster_settings_lambda_role, log_retention_role=self.roles[app_constants.LOG_RETENTION_ROLE_NAME] ) + cluster_settings_lambda_role.attach_inline_policy(self.cluster_settings_lambda_policy) + self.cluster_settings_lambda.node.add_dependency(self.cluster_settings_lambda_policy) self.cluster_settings_lambda.node.add_dependency(cluster_settings_lambda_role) def build_solution_metrics_lambda(self): @@ -597,12 +601,12 @@ def build_solution_metrics_lambda(self): description=f'Role for solution-metrics metrics Lambda function for Cluster: {self.cluster_name}', assumed_by=['lambda']) - solution_metrics_lambda_role.attach_inline_policy(Policy( + self.solution_metrics_lambda_policy = Policy( context=self.context, name=f'{lambda_name}-policy', scope=self.stack, policy_template_name='solution-metrics-lambda-function.yml' - )) + ) self.solution_metrics_lambda = LambdaFunction( context=self.context, @@ -617,6 +621,9 @@ def build_solution_metrics_lambda(self): role=solution_metrics_lambda_role, log_retention_role=self.roles[app_constants.LOG_RETENTION_ROLE_NAME] ) + solution_metrics_lambda_role.attach_inline_policy(self.solution_metrics_lambda_policy) + self.solution_metrics_lambda.node.add_dependency(self.solution_metrics_lambda_policy) + self.solution_metrics_lambda.node.add_dependency(solution_metrics_lambda_role) def build_self_signed_certificates_lambda(self): """ @@ -788,7 +795,8 @@ def build_ec2_notification_module(self): environment={ 'IDEA_EC2_STATE_SNS_TOPIC_ARN': self.ec2_events_sns_topic.topic_arn, 'IDEA_CLUSTER_NAME_TAG_KEY': constants.IDEA_TAG_CLUSTER_NAME, - 'IDEA_CLUSTER_NAME_TAG_VALUE': self.context.cluster_name() + 'IDEA_CLUSTER_NAME_TAG_VALUE': self.context.cluster_name(), + 'IDEA_TAG_PREFIX': constants.IDEA_TAG_PREFIX }, timeout_seconds=180, role=ec2_state_event_transformation_lambda_role, @@ -1116,6 +1124,9 @@ def build_cluster_settings(self): cluster_settings['load_balancers.external_alb.certificates.certificate_secret_arn'] = self.external_certificate.get_att_string('certificate_secret_arn') cluster_settings['load_balancers.external_alb.certificates.private_key_secret_arn'] = self.external_certificate.get_att_string('private_key_secret_arn') cluster_settings['load_balancers.external_alb.certificates.acm_certificate_arn'] = self.external_certificate.get_att_string('acm_certificate_arn') + else: + cluster_settings['load_balancers.external_alb.certificates.provided'] = self.context.config().get_string('cluster.load_balancers.external_alb.certificates.provided', required=True) + cluster_settings['load_balancers.external_alb.certificates.acm_certificate_arn'] = self.context.config().get_string('cluster.load_balancers.external_alb.certificates.acm_certificate_arn', required=True) cluster_settings['load_balancers.internal_alb.certificates.certificate_secret_arn'] = self.internal_certificate.get_att_string('certificate_secret_arn') cluster_settings['load_balancers.internal_alb.certificates.private_key_secret_arn'] = self.internal_certificate.get_att_string('private_key_secret_arn') diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/directoryservice_stack.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/directoryservice_stack.py index 28aaadd..6fb1e68 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/directoryservice_stack.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/directoryservice_stack.py @@ -34,7 +34,8 @@ from aws_cdk import ( aws_ec2 as ec2, aws_route53 as route53, - aws_sqs as sqs + aws_sqs as sqs, + aws_kms as kms ) import constructs @@ -235,16 +236,21 @@ def build_ec2_instance(self): enable_detailed_monitoring = self.context.config().get_bool('directoryservice.ec2.enable_detailed_monitoring', default=False) enable_termination_protection = self.context.config().get_bool('directoryservice.ec2.enable_termination_protection', default=False) metadata_http_tokens = self.context.config().get_string('directoryservice.ec2.metadata_http_tokens', required=True) - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, 'no_proxy': no_proxy } + kms_key_id = self.context.config().get_string('cluster.ebs.kms_key_id', required=False, default=None) + if kms_key_id is not None: + kms_key_arn = self.get_kms_key_arn(kms_key_id) + ebs_kms_key = kms.Key.from_key_arn(scope=self.stack, id=f'ebs-kms-key', key_arn=kms_key_arn) + else: + ebs_kms_key = kms.Alias.from_alias_name(scope=self.stack, id=f'ebs-kms-key-default', alias_name='alias/aws/ebs') if is_public and len(self.cluster.public_subnets) > 0: subnet_ids = self.cluster.existing_vpc.get_public_subnet_ids() @@ -288,6 +294,8 @@ def build_ec2_instance(self): block_devices=[ec2.BlockDevice( device_name=block_device_name, volume=ec2.BlockDeviceVolume(ebs_device=ec2.EbsDeviceProps( + encrypted=True, + kms_key=ebs_kms_key, volume_size=volume_size, volume_type=ec2.EbsDeviceVolumeType.GP3 ) @@ -363,11 +371,14 @@ def build_route53_record_set(self): ) def build_ad_automation_sqs_queue(self): + kms_key_id = self.context.config().get_string('cluster.sqs.kms_key_id') + self.ad_automation_sqs_queue = SQSQueue( self.context, 'ad-automation-sqs-queue', self.stack, queue_name=f'{self.cluster_name}-{self.module_id}-ad-automation.fifo', fifo=True, content_based_deduplication=True, + encryption_master_key=kms_key_id, dead_letter_queue=sqs.DeadLetterQueue( max_receive_count=30, queue=SQSQueue( @@ -375,6 +386,7 @@ def build_ad_automation_sqs_queue(self): queue_name=f'{self.cluster_name}-{self.module_id}-ad-automation-dlq.fifo', fifo=True, content_based_deduplication=True, + encryption_master_key=kms_key_id, is_dead_letter_queue=True ) ) diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/identity_provider_stack.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/identity_provider_stack.py index caf702c..a2e164c 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/identity_provider_stack.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/identity_provider_stack.py @@ -58,7 +58,7 @@ class IdentityProviderStack(IdeaBaseStack): * External ALB Endpoint * Internal ALB Endpoint * Multi-AZ RDS (Aurora) - * SecretManager Secrets + * SecretsManager Secrets * CDK Custom Resource as a shim for down stream modules to register OAuth2 clients, Resource Servers """ diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/scheduler_stack.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/scheduler_stack.py index d68db87..22c986d 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/scheduler_stack.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/scheduler_stack.py @@ -39,7 +39,8 @@ aws_cognito as cognito, aws_sqs as sqs, aws_route53 as route53, - aws_elasticloadbalancingv2 as elbv2 + aws_elasticloadbalancingv2 as elbv2, + aws_kms as kms ) @@ -293,16 +294,21 @@ def build_ec2_instance(self): enable_detailed_monitoring = self.context.config().get_bool('scheduler.ec2.enable_detailed_monitoring', default=False) enable_termination_protection = self.context.config().get_bool('scheduler.ec2.enable_termination_protection', default=False) metadata_http_tokens = self.context.config().get_string('scheduler.ec2.metadata_http_tokens', required=True) - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, 'no_proxy': no_proxy } + kms_key_id = self.context.config().get_string('cluster.ebs.kms_key_id', required=False, default=None) + if kms_key_id is not None: + kms_key_arn = self.get_kms_key_arn(kms_key_id) + ebs_kms_key = kms.Key.from_key_arn(scope=self.stack, id=f'ebs-kms-key', key_arn=kms_key_arn) + else: + ebs_kms_key = kms.Alias.from_alias_name(scope=self.stack, id=f'ebs-kms-key-default', alias_name='alias/aws/ebs') if is_public and len(self.cluster.public_subnets) > 0: subnet_ids = self.cluster.existing_vpc.get_public_subnet_ids() @@ -332,6 +338,8 @@ def build_ec2_instance(self): block_devices=[ec2.BlockDevice( device_name=block_device_name, volume=ec2.BlockDeviceVolume(ebs_device=ec2.EbsDeviceProps( + encrypted=True, + kms_key=ebs_kms_key, volume_size=volume_size, volume_type=ec2.EbsDeviceVolumeType.GP3 ) diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/virtual_desktop_controller_stack.py b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/virtual_desktop_controller_stack.py index 1fcd091..7f2afa8 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/virtual_desktop_controller_stack.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/cdk/stacks/virtual_desktop_controller_stack.py @@ -50,7 +50,8 @@ aws_events_targets as events_targets, aws_s3 as s3, aws_backup as backup, - aws_iam as iam + aws_iam as iam, + aws_kms as kms ) from aws_cdk.aws_events import Schedule @@ -535,11 +536,10 @@ def build_dcv_broker(self): # autoscaling group dcv_broker_package_uri = self.stack.node.try_get_context('dcv_broker_bootstrap_package_uri') - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, @@ -639,11 +639,10 @@ def build_virtual_desktop_controller(self): component_jinja='virtual-desktop-controller.yml' ) - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, @@ -779,6 +778,13 @@ def _build_auto_scaling_group(self, component_name: str, security_group: Securit enable_detailed_monitoring = self.context.config().get_bool(f'virtual-desktop-controller.{self.CONFIG_MAPPING[component_name]}.autoscaling.enable_detailed_monitoring', default=False) metadata_http_tokens = self.context.config().get_string(f'virtual-desktop-controller.{self.CONFIG_MAPPING[component_name]}.autoscaling.metadata_http_tokens', required=True) + kms_key_id = self.context.config().get_string('cluster.ebs.kms_key_id', required=False, default=None) + if kms_key_id is not None: + kms_key_arn = self.get_kms_key_arn(kms_key_id) + ebs_kms_key = kms.Key.from_key_arn(scope=self.stack, id=f'{component_name}-ebs-kms-key', key_arn=kms_key_arn) + else: + ebs_kms_key = kms.Alias.from_alias_name(scope=self.stack, id=f'{component_name}-ebs-kms-key-default', alias_name='alias/aws/ebs') + launch_template = ec2.LaunchTemplate( self.stack, f'{component_name}-lt', instance_type=ec2.InstanceType(self.context.config().get_string(f'virtual-desktop-controller.{self.CONFIG_MAPPING[component_name]}.autoscaling.instance_type', required=True)), @@ -791,6 +797,8 @@ def _build_auto_scaling_group(self, component_name: str, security_group: Securit block_devices=[ec2.BlockDevice( device_name=block_device_name, volume=ec2.BlockDeviceVolume(ebs_device=ec2.EbsDeviceProps( + encrypted=True, + kms_key=ebs_kms_key, volume_size=self.context.config().get_int(f'virtual-desktop-controller.{self.CONFIG_MAPPING[component_name]}.autoscaling.volume_size', default=200), volume_type=ec2.EbsDeviceVolumeType.GP3 )) @@ -865,11 +873,10 @@ def _build_dcv_connection_gateway_instance_infrastructure(self): if Utils.is_empty(dcv_connection_gateway_bootstrap_package_uri): dcv_connection_gateway_bootstrap_package_uri = 'not-provided' - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, @@ -1087,6 +1094,11 @@ def build_cluster_settings(self): if not self.context.config().get_bool('virtual-desktop-controller.dcv_connection_gateway.certificate.provided', default=False): cluster_settings['dcv_connection_gateway.certificate.certificate_secret_arn'] = self.dcv_connection_gateway_self_signed_cert.get_att_string('certificate_secret_arn') cluster_settings['dcv_connection_gateway.certificate.private_key_secret_arn'] = self.dcv_connection_gateway_self_signed_cert.get_att_string('private_key_secret_arn') + else: + cluster_settings['dcv_connection_gateway.certificate.provided'] = self.context.config().get_string('virtual-desktop-controller.dcv_connection_gateway.certificate.provided', required=True) + cluster_settings['dcv_connection_gateway.certificate.certificate_secret_arn'] = self.context.config().get_string('virtual-desktop-controller.dcv_connection_gateway.certificate.certificate_secret_arn', required=True) + cluster_settings['dcv_connection_gateway.certificate.private_key_secret_arn'] = self.context.config().get_string('virtual-desktop-controller.dcv_connection_gateway.certificate.private_key_secret_arn', required=True) + cluster_settings['dcv_connection_gateway.certificate.custom_dns_name'] = self.context.config().get_string('virtual-desktop-controller.dcv_connection_gateway.certificate.custom_dns_name', required=True) if self.backup_plan is not None: cluster_settings['vdi_host_backup.backup_plan.arn'] = self.backup_plan.get_backup_plan_arn() diff --git a/source/idea/idea-administrator/src/ideaadministrator/app/delete_cluster.py b/source/idea/idea-administrator/src/ideaadministrator/app/delete_cluster.py index eaea1cd..a145c75 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app/delete_cluster.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app/delete_cluster.py @@ -240,7 +240,7 @@ def invoke_app_app_module_clean_up(self): time.sleep(10) def find_cloud_formation_stacks(self): - self.context.info('Searching for CloudFormation stacks to be terminated ...') + self.context.info(f'Searching for CloudFormation stacks to be terminated (matching {constants.IDEA_TAG_CLUSTER_NAME} of {self.cluster_name})...') stacks_to_delete = [] cluster_stacks = [] identity_provider_stacks = [] @@ -771,50 +771,35 @@ def delete_backup_vault_recovery_points(self): total_recovery_points = 0 total_deleted = 0 - next_token = None - while True: - - if next_token is None: - list_recovery_points_by_backup_vault_result = self.context.aws().backup().list_recovery_points_by_backup_vault( - BackupVaultName=backup_vault_name - ) - else: - list_recovery_points_by_backup_vault_result = self.context.aws().backup().list_recovery_points_by_backup_vault( - BackupVaultName=backup_vault_name, - NextToken=next_token - ) - - next_token = Utils.get_value_as_string('NextToken', list_recovery_points_by_backup_vault_result) - - recovery_points = Utils.get_value_as_list('RecoveryPoints', list_recovery_points_by_backup_vault_result, []) + bu_paginator = self.context.aws().backup().get_paginator('list_recovery_points_by_backup_vault') + bu_iterator = bu_paginator.paginate(BackupVaultName=backup_vault_name) + for _page in bu_iterator: + recovery_points = Utils.get_value_as_list('RecoveryPoints', _page, []) total_recovery_points += len(recovery_points) - for recovery_point in recovery_points: - recovery_point_arn = Utils.get_value_as_string('RecoveryPointArn', recovery_point) - - # can be one of: 'COMPLETED'|'PARTIAL'|'DELETING'|'EXPIRED' - # if status is not COMPLETED/EXPIRED, do not attempt to delete, but wait for deletion or backup completion. - recovery_point_status = Utils.get_value_as_string('Status', recovery_point) - if recovery_point_status not in ('COMPLETED', 'EXPIRED'): - continue - - self.context.info(f'deleting recovery point: {recovery_point_arn} ...') - self.context.aws().backup().delete_recovery_point( - BackupVaultName=backup_vault_name, - RecoveryPointArn=recovery_point_arn - ) - total_deleted += 1 - time.sleep(.1) + _rp_delete_start = Utils.current_time_ms() + self.context.info(f"Deleting {len(recovery_points)} recovery points from AWS Backup...") + for recovery_point in recovery_points: + recovery_point_arn = Utils.get_value_as_string('RecoveryPointArn', recovery_point) + + # can be one of: 'COMPLETED'|'PARTIAL'|'DELETING'|'EXPIRED' + # if status is not COMPLETED/EXPIRED, do not attempt to delete, but wait for deletion or backup completion. + recovery_point_status = Utils.get_value_as_string('Status', recovery_point) + if recovery_point_status not in ('COMPLETED', 'EXPIRED'): + continue + + self.context.info(f'deleting recovery point: {recovery_point_arn} ...') + self.context.aws().backup().delete_recovery_point( + BackupVaultName=backup_vault_name, + RecoveryPointArn=recovery_point_arn + ) + total_deleted += 1 + time.sleep(.1) - if next_token is None: - if total_recovery_points == total_deleted: - self.context.info(f'deleted {total_recovery_points} recovery points.') - break - else: - total_pending = total_recovery_points - total_deleted - with self.context.spinner(f'waiting for {total_pending} recovery points to be deleted or ready for deleting ...'): - time.sleep(10) + _rp_delete_end = Utils.current_time_ms() + _run_time_ms = int((_rp_delete_end - _rp_delete_start) / 1_000) + self.context.info(f'deleted {total_recovery_points} recovery points in {_run_time_ms} seconds.') except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] != 'ResourceNotFoundException': diff --git a/source/idea/idea-administrator/src/ideaadministrator/app_main.py b/source/idea/idea-administrator/src/ideaadministrator/app_main.py index fab8144..4d81521 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/app_main.py +++ b/source/idea/idea-administrator/src/ideaadministrator/app_main.py @@ -395,10 +395,13 @@ def read_local_config_entries(): else: break + local_config_dynamodb_kms_key_id = local_config.get_string(f'{cluster_module_id}.dynamodb.kms_key_id', required=False, default=None) + cluster_config_db = ClusterConfigDB( cluster_name=config_generator.get_cluster_name(), aws_region=config_generator.get_aws_region(), aws_profile=config_generator.get_aws_profile(), + dynamodb_kms_key_id=local_config_dynamodb_kms_key_id, create_database=True ) if config_dir: diff --git a/source/idea/idea-administrator/src/ideaadministrator/integration_tests/test_context.py b/source/idea/idea-administrator/src/ideaadministrator/integration_tests/test_context.py index 8614dbc..6a190f4 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/integration_tests/test_context.py +++ b/source/idea/idea-administrator/src/ideaadministrator/integration_tests/test_context.py @@ -138,19 +138,24 @@ def initialize_admin_auth(self): username=self.admin_username, password=self.admin_password ), result_as=InitiateAuthResult) + admin_auth = result.auth + if Utils.is_empty(admin_auth.access_token): + raise exceptions.general_exception('access_token not found') + if Utils.is_empty(admin_auth.refresh_token): + raise exceptions.general_exception('refresh_token not found') else: self.idea_context.info('Renewing Admin Authentication Access Token ...') result = self._admin_http_client.invoke_alt('Auth.InitiateAuth', InitiateAuthRequest( auth_flow='REFRESH_TOKEN_AUTH', username=self.admin_username, - password=self.admin_auth.refresh_token + refresh_token=self.admin_auth.refresh_token ), result_as=InitiateAuthResult) - admin_auth = result.auth - if Utils.is_empty(admin_auth.access_token): - raise exceptions.general_exception('access_token not found') - if Utils.is_empty(admin_auth.refresh_token): - raise exceptions.general_exception('refresh_token not found') + admin_auth = result.auth + if Utils.is_empty(admin_auth.access_token): + raise exceptions.general_exception('access_token not found') + # set refresh token from previous auth + admin_auth.refresh_token = self.admin_auth.refresh_token self.admin_auth = admin_auth self.admin_auth_expires_on = arrow.get().shift(seconds=admin_auth.expires_in) @@ -163,19 +168,22 @@ def initialize_non_admin_auth(self): username=self.non_admin_username, password=self.non_admin_password ), result_as=InitiateAuthResult) + non_admin_auth = result.auth + if Utils.is_empty(non_admin_auth.access_token): + raise exceptions.general_exception('access_token not found') + if Utils.is_empty(non_admin_auth.refresh_token): + raise exceptions.general_exception('refresh_token not found') else: self.idea_context.info('Renewing Non-Admin Authentication Access Token ...') result = self._admin_http_client.invoke_alt('Auth.InitiateAuth', InitiateAuthRequest( auth_flow='REFRESH_TOKEN_AUTH', username=self.non_admin_username, - password=self.non_admin_auth.refresh_token + refresh_token=self.non_admin_auth.refresh_token ), result_as=InitiateAuthResult) - non_admin_auth = result.auth - if Utils.is_empty(non_admin_auth.access_token): - raise exceptions.general_exception('access_token not found') - if Utils.is_empty(non_admin_auth.refresh_token): - raise exceptions.general_exception('refresh_token not found') + non_admin_auth = result.auth + if Utils.is_empty(non_admin_auth.access_token): + raise exceptions.general_exception('access_token not found') self.non_admin_auth = non_admin_auth self.non_admin_auth_expires_on = arrow.get().shift(seconds=non_admin_auth.expires_in) @@ -183,7 +191,7 @@ def initialize_non_admin_auth(self): def get_admin_access_token(self) -> str: if self.admin_auth is None: raise exceptions.general_exception('admin authentication not initialized') - if self.admin_auth_expires_on > arrow.get().shift(minutes=-5): + if self.admin_auth_expires_on > arrow.get().shift(minutes=15): return self.admin_auth.access_token self.initialize_admin_auth() return self.admin_auth.access_token @@ -191,7 +199,7 @@ def get_admin_access_token(self) -> str: def get_non_admin_access_token(self): if self.non_admin_auth is None: raise exceptions.general_exception('non admin authentication not initialized') - if self.non_admin_auth_expires_on > arrow.get().shift(minutes=-5): + if self.non_admin_auth_expires_on > arrow.get().shift(minutes=15): return self.non_admin_auth.access_token self.initialize_non_admin_auth() return self.non_admin_auth.access_token diff --git a/source/idea/idea-administrator/src/ideaadministrator/integration_tests/virtual_desktop_controller_tests.py b/source/idea/idea-administrator/src/ideaadministrator/integration_tests/virtual_desktop_controller_tests.py index 6ecec48..ae0aaf2 100644 --- a/source/idea/idea-administrator/src/ideaadministrator/integration_tests/virtual_desktop_controller_tests.py +++ b/source/idea/idea-administrator/src/ideaadministrator/integration_tests/virtual_desktop_controller_tests.py @@ -40,6 +40,7 @@ def test_admin_batch_session_workflow(context: TestContext): batch_sessions = create_batch_sessions(context, sessions) # 3. Execute Tests for session in batch_sessions: + admin_access_token = context.get_admin_access_token() session_workflow = SessionWorkflow(context, session, testcase_id, context.admin_username, admin_access_token, 'VirtualDesktopAdmin.GetSessionInfo') session_workflow.test_session_workflow() diff --git a/source/idea/idea-administrator/src/ideaadministrator_meta/__init__.py b/source/idea/idea-administrator/src/ideaadministrator_meta/__init__.py index fc85735..bf20e14 100644 --- a/source/idea/idea-administrator/src/ideaadministrator_meta/__init__.py +++ b/source/idea/idea-administrator/src/ideaadministrator_meta/__init__.py @@ -12,4 +12,4 @@ # pkg config for soca-admin. no dependencies. __name__ = 'idea-administrator' -__version__ = '3.1.1' +__version__ = '3.1.2' diff --git a/source/idea/idea-bootstrap/compute-node/_templates/configure_openpbs_compute_node.jinja2 b/source/idea/idea-bootstrap/compute-node/_templates/configure_openpbs_compute_node.jinja2 index cc8a52b..e41b181 100644 --- a/source/idea/idea-bootstrap/compute-node/_templates/configure_openpbs_compute_node.jinja2 +++ b/source/idea/idea-bootstrap/compute-node/_templates/configure_openpbs_compute_node.jinja2 @@ -1,12 +1,15 @@ # Begin: Configure OpenPBS Compute Node {% include '_templates/linux/openpbs.jinja2' %} +INSTANCE_HOSTNAME=$(hostname) + echo -e "PBS_SERVER={{ context.config.get_string('scheduler.private_dns_name', required=True).split('.')[0] }} PBS_START_SERVER=0 PBS_START_SCHED=0 PBS_START_COMM=0 PBS_START_MOM=1 PBS_EXEC=/opt/pbs +PBS_LEAF_NAME=$INSTANCE_HOSTNAME PBS_HOME=/var/spool/pbs PBS_CORE_LIMIT=unlimited PBS_SCP=/usr/bin/scp diff --git a/source/idea/idea-bootstrap/dcv-connection-gateway/install_app.sh.jinja2 b/source/idea/idea-bootstrap/dcv-connection-gateway/install_app.sh.jinja2 index df4f1bc..230c57b 100644 --- a/source/idea/idea-bootstrap/dcv-connection-gateway/install_app.sh.jinja2 +++ b/source/idea/idea-bootstrap/dcv-connection-gateway/install_app.sh.jinja2 @@ -168,8 +168,8 @@ function configure_dcv_connection_gateway() { level = \"trace\" [gateway] -quic-listen-endpoints = [\"0.0.0.0:8443\"] -web-listen-endpoints = [\"0.0.0.0:8443\", \"[::]:8445\"] +quic-listen-endpoints = [\"0.0.0.0:8443\", \"[::]:8443\"] +web-listen-endpoints = [\"0.0.0.0:8443\", \"[::]:8443\"] cert-file = \"/etc/dcv-connection-gateway/certs/default_cert.pem\" cert-key-file = \"/etc/dcv-connection-gateway/certs/default_key_pkcs8.pem\" diff --git a/source/idea/idea-bootstrap/virtual-desktop-host-linux/configure_dcv_host.sh.jinja2 b/source/idea/idea-bootstrap/virtual-desktop-host-linux/configure_dcv_host.sh.jinja2 index de2ea4f..d9a3e78 100644 --- a/source/idea/idea-bootstrap/virtual-desktop-host-linux/configure_dcv_host.sh.jinja2 +++ b/source/idea/idea-bootstrap/virtual-desktop-host-linux/configure_dcv_host.sh.jinja2 @@ -157,6 +157,14 @@ rotation = 'daily' echo -e "idea_session_id=\"${IDEA_SESSION_ID}\"" > /etc/dcv-session-manager-agent/tags/idea_tags.toml } +function configure_usb_remotization() { + echo "Searching for USB Remotization configurations..." + {%- for usb_info in context.config.get_list('vdc.server.usb_remotization', default=[]) %} + echo -en "{{ usb_info }}\n" >>/etc/dcv/usb-devices.conf + {%- endfor %} + +} + function restart_x_server() { echo "# restart x server ..." sudo systemctl isolate multi-user.target @@ -305,7 +313,7 @@ install_microphone_redirect download_broker_certificate configure_dcv_host configure_dcv_agent - +configure_usb_remotization configure_gl machine=$(uname -m) #x86_64 or aarch64 if [[ $machine == "x86_64" ]]; then diff --git a/source/idea/idea-bootstrap/virtual-desktop-host-linux/setup.sh.jinja2 b/source/idea/idea-bootstrap/virtual-desktop-host-linux/setup.sh.jinja2 index b7a52d3..def786a 100644 --- a/source/idea/idea-bootstrap/virtual-desktop-host-linux/setup.sh.jinja2 +++ b/source/idea/idea-bootstrap/virtual-desktop-host-linux/setup.sh.jinja2 @@ -28,6 +28,8 @@ IDEA_CLUSTER_S3_BUCKET={{ context.cluster_s3_bucket }} IDEA_CLUSTER_NAME={{ context.cluster_name }} IDEA_CLUSTER_HOME={{ context.cluster_home_dir }} IDEA_APP_DEPLOY_DIR={{ context.app_deploy_dir }} +IDEA_SESSION_ID="{{ context.vars.idea_session_id }}" +IDEA_SESSION_OWNER="{{ context.vars.session_owner }}" BOOTSTRAP_DIR=/root/bootstrap" > /etc/environment {% if context.https_proxy != '' %} diff --git a/source/idea/idea-bootstrap/virtual-desktop-host-windows/ConfigureDCVHost.ps1.jinja2 b/source/idea/idea-bootstrap/virtual-desktop-host-windows/ConfigureDCVHost.ps1.jinja2 index f1bff81..9c11342 100644 --- a/source/idea/idea-bootstrap/virtual-desktop-host-windows/ConfigureDCVHost.ps1.jinja2 +++ b/source/idea/idea-bootstrap/virtual-desktop-host-windows/ConfigureDCVHost.ps1.jinja2 @@ -121,6 +121,12 @@ rotation = 'daily' # DCVSessionManagerAgentService is usually disabled on the system. Need to enable it. Set-Service -Name DcvSessionManagerAgentService -StartupType Automatic + # Determine if we have any additional USB remotization configurations + $UsbDevicesConfFile = "C:\Program Files\NICE\DCV\Server\conf\usb-devices.conf" + + {%- for usb_info in context.config.get_list('vdc.server.usb_remotization', default=[]) %} + Add-Content $UsbDevicesConfFile -Value "{{ usb_info }}" + {%- endfor %} {%- if context.config.get_string('directoryservice.provider', required=True) in ['aws_managed_activedirectory', 'activedirectory'] %} # if ActiveDirectory, add the user to local Administrators group. # todo: need to have a broader discussion with the group on this. this will work well as long as no shared storage is mounted automatically. diff --git a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/account_tasks.py b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/account_tasks.py index 860e7e3..9a0c242 100644 --- a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/account_tasks.py +++ b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/account_tasks.py @@ -136,8 +136,7 @@ def invoke(self, payload: Dict): readonly = self.context.ldap_client.is_readonly() if readonly: - self.logger.info(f'sync user: Read-only Directory service - sync {username} NOOP ...') - self.logger.info('sync user: returning ...') + self.logger.info(f'sync user: Read-only Directory service - sync {username} NOOP ... returning') return if enabled: diff --git a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/accounts_service.py b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/accounts_service.py index 502347f..f031a78 100644 --- a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/accounts_service.py +++ b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/accounts_service.py @@ -594,7 +594,23 @@ def create_user(self, user: User, email_verified: bool = False) -> User: password = user.password if email_verified: if Utils.is_empty(password): - raise exceptions.invalid_params('user.password is required') + raise exceptions.invalid_params('Password is required') + + user_pool_password_policy = self.user_pool.describe_password_policy() + # Validate password compliance versus Cognito user pool password policy + # Cognito: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-policies.html + if len(password) < user_pool_password_policy.minimum_length: + raise exceptions.invalid_params(f'Password should be greater than {user_pool_password_policy.minimum_length} characters') + elif len(password) > 256: + raise exceptions.invalid_params(f'Password can be up to 256 characters') + elif user_pool_password_policy.require_numbers and re.search('[0-9]', password) is None: + raise exceptions.invalid_params('Password should include at least 1 number') + elif user_pool_password_policy.require_uppercase and re.search('[A-Z]', password) is None: + raise exceptions.invalid_params('Password should include at least 1 uppercase letter') + elif user_pool_password_policy.require_lowercase and re.search('[a-z]', password) is None: + raise exceptions.invalid_params('Password should include at least 1 lowercase letter') + elif user_pool_password_policy.require_symbols and re.search('[\^\$\*\.\[\]{}\(\)\?"!@#%&\/\\,><\':;\|_~`=\+\-]', password) is None: + raise exceptions.invalid_params('Password should include at least 1 of these special characters: ^ $ * . [ ] { } ( ) ? " ! @ # % & / \ , > < \' : ; | _ ~ ` = + -') else: self.logger.debug('create_user() - setting password to random value') password = Utils.generate_password(8, 2, 2, 2, 2) diff --git a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/cognito_user_pool.py b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/cognito_user_pool.py index 9b63921..218e0ac 100644 --- a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/cognito_user_pool.py +++ b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/cognito_user_pool.py @@ -8,6 +8,7 @@ # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # and limitations under the License. +import logging from ideadatamodel import ( exceptions, @@ -19,7 +20,8 @@ RespondToAuthChallengeRequest, RespondToAuthChallengeResult, AuthResult, - CognitoUser + CognitoUser, + CognitoUserPoolPasswordPolicy ) from ideasdk.utils import Utils from ideasdk.context import SocaContext @@ -129,6 +131,8 @@ def admin_get_user(self, username: str) -> Optional[CognitoUser]: if user is not None: return user + _api_query_start = Utils.current_time_ms() + try: result = self._context.aws().cognito_idp().admin_get_user( UserPoolId=self.user_pool_id, @@ -140,6 +144,10 @@ def admin_get_user(self, username: str) -> Optional[CognitoUser]: else: raise e + _api_query_end = Utils.current_time_ms() + if self._logger.isEnabledFor(logging.DEBUG): + self._logger.debug(f"Cognito-API query: {_api_query_end - _api_query_start}ms") + user = CognitoUser(**result) self._context.cache().short_term().set(cache_key, user) return user @@ -528,3 +536,28 @@ def change_password(self, username: str, access_token: str, old_password: str, n ProposedPassword=new_password ) self.password_updated(username) + + def describe_password_policy(self) -> CognitoUserPoolPasswordPolicy: + try: + describe_result = self._context.aws().cognito_idp().describe_user_pool( + UserPoolId=self.user_pool_id + ) + except botocore.exceptions.ClientError as e: + raise e + user_pool = Utils.get_value_as_dict('UserPool', describe_result) + policies = Utils.get_value_as_dict('Policies', user_pool) + password_policy = Utils.get_value_as_dict('PasswordPolicy', policies) + minimum_length = Utils.get_value_as_int('MinimumLength', password_policy, 8) + require_uppercase = Utils.get_value_as_bool('RequireUppercase', password_policy, True) + require_lowercase = Utils.get_value_as_bool('RequireLowercase', password_policy, True) + require_numbers = Utils.get_value_as_bool('RequireNumbers', password_policy, True) + require_symbols = Utils.get_value_as_bool('RequireSymbols', password_policy, True) + temporary_password_validity_days = Utils.get_value_as_int('TemporaryPasswordValidityDays', password_policy, 7) + return CognitoUserPoolPasswordPolicy( + minimum_length=minimum_length, + require_uppercase=require_uppercase, + require_lowercase=require_lowercase, + require_numbers=require_numbers, + require_symbols=require_symbols, + temporary_password_validity_days=temporary_password_validity_days + ) diff --git a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/db/user_dao.py b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/db/user_dao.py index 2cb9635..1489374 100644 --- a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/db/user_dao.py +++ b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/db/user_dao.py @@ -8,6 +8,7 @@ # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # and limitations under the License. +import logging from ideasdk.utils import Utils from ideadatamodel import ListUsersRequest, ListUsersResult, SocaPaginator, User @@ -57,7 +58,7 @@ def initialize(self): self.table = self.context.aws().dynamodb_table().Table(self.get_table_name()) def convert_from_db(self, user: Dict) -> User: - user = User( + user_entry = User( **{ 'username': Utils.get_value_as_string('username', user), 'email': Utils.get_value_as_string('email', user), @@ -75,12 +76,12 @@ def convert_from_db(self, user: Dict) -> User: 'updated_on': Utils.get_value_as_int('updated_on', user) } ) - cognito_user = self.user_pool.admin_get_user(user.username) - if cognito_user is not None: - user.status = cognito_user.UserStatus - else: - user.status = 'DELETED' - return user + + # The Cognito status lookup was removed for performance considerations + # over multiple/looped API calls for bulk users. This takes place on + # singleton lookups/get_user. + + return user_entry @staticmethod def convert_to_db(user: User) -> Dict: @@ -132,11 +133,15 @@ def create_user(self, user: Dict) -> Dict: def get_user(self, username: str) -> Optional[Dict]: username = AuthUtils.sanitize_username(username) + _lu_start = Utils.current_time_ms() result = self.table.get_item( Key={ 'username': username } ) + _lu_stop = Utils.current_time_ms() + if self.logger.isEnabledFor(logging.DEBUG): + self.logger.debug(f"user_lookup: {result} - {_lu_stop - _lu_start}ms") return Utils.get_value_as_dict('Item', result) @@ -208,13 +213,25 @@ def list_users(self, request: ListUsersRequest) -> ListUsersResult: if scan_filter is not None: scan_request['ScanFilter'] = scan_filter + _scan_start = Utils.current_time_ms() scan_result = self.table.scan(**scan_request) + _scan_end = Utils.current_time_ms() db_users = Utils.get_value_as_list('Items', scan_result, []) + + if self.logger.isEnabledFor(logging.DEBUG): + self.logger.debug(f"DDB Table scan took {_scan_end - _scan_start}ms for {len(db_users)} users") + users = [] + + _idp_start = Utils.current_time_ms() for db_user in db_users: user = self.convert_from_db(db_user) users.append(user) + _idp_end = Utils.current_time_ms() + + if self.logger.isEnabledFor(logging.DEBUG): + self.logger.debug(f"User status took {_idp_end - _idp_start}ms for {len(users)} users") response_cursor = None last_evaluated_key = Utils.get_any_value('LastEvaluatedKey', scan_result) diff --git a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/ldapclient/abstract_ldap_client.py b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/ldapclient/abstract_ldap_client.py index f16a806..c84e896 100644 --- a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/ldapclient/abstract_ldap_client.py +++ b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/accounts/ldapclient/abstract_ldap_client.py @@ -28,6 +28,7 @@ from ldappool import ConnectionManager # noqa from abc import abstractmethod import base64 +import logging class LdapClientOptions(SocaBaseModel): @@ -85,6 +86,28 @@ def __init__(self, context: SocaContext, options: LdapClientOptions, logger=None self.refresh_root_username_password() # initialize pooled connection manager for LDAP to conserve resources + # Set any LDAP options that may be needed + self.logger.debug(f"Setting LDAP options..") + default_ldap_options = [ + { 'name': 'Referrals' , 'code': ldap.OPT_REFERRALS, 'value': ldap.OPT_OFF }, + { 'name': 'Protocol Version', 'code': ldap.OPT_PROTOCOL_VERSION, 'value': ldap.VERSION3 } + ] + for option in default_ldap_options: + self.logger.debug(f"Setting default option: {option.get('name')}({option.get('code')}) -> {option.get('value')}") + ldap.set_option(option.get('code'), option.get('value')) + self.logger.debug(f"Confirming default option: {option.get('name')}({option.get('code')}) -> {ldap.get_option(option.get('code'))}") + + configured_ldap_options = self.context.config().get_list('directoryservice.ldap_options', default=[]) + if configured_ldap_options: + self.logger.debug(f"Obtained LDAP options from directoryservice.ldap_options: {configured_ldap_options}") + for option in configured_ldap_options: + opt_d = dict(eval(option)) + if isinstance(opt_d, dict): + self.logger.debug(f"Setting configured option: {opt_d.get('name')}({opt_d.get('code')}) -> {opt_d.get('value')}") + ldap.set_option(opt_d.get('code'), opt_d.get('value')) + self.logger.debug(f"Confirming configured option: {opt_d.get('name')}({opt_d.get('code')}) -> {ldap.get_option(opt_d.get('code'))}") + + self.logger.debug(f"Starting LDAP connection pool to {self.ldap_uri}") self.connection_manager = ConnectionManager( uri=self.ldap_uri, size=Utils.get_as_int(options.connection_pool_size, DEFAULT_LDAP_CONNECTION_POOL_SIZE), @@ -93,7 +116,6 @@ def __init__(self, context: SocaContext, options: LdapClientOptions, logger=None timeout=Utils.get_as_float(options.connection_timeout, DEFAULT_LDAP_CONNECTION_TIMEOUT), use_pool=Utils.get_as_bool(options.connection_pool_enabled, DEFAULT_LDAP_ENABLE_CONNECTION_POOL) ) - # ldap wrapper methods def add_s(self, dn, modlist): trace_message = f'ldapadd -x -D "{self.ldap_root_bind}" -H {self.ldap_uri} "{dn}"' @@ -380,10 +402,15 @@ def get_ldap_root_connection(self) -> LDAPObject: """ returns an LDAP connection object bound to ROOT user from the connection pool """ - return self.connection_manager.connection( + res = self.connection_manager.connection( bind=self.ldap_root_bind, passwd=self.ldap_root_password ) + if self.logger.isEnabledFor(logging.DEBUG): + cm_info = str(self.connection_manager) + self.logger.debug(f"LDAP CM returning conn ({res}), CM now:\n{cm_info}") + + return res @staticmethod def convert_ldap_group(ldap_group: Dict) -> Optional[Dict]: @@ -474,7 +501,11 @@ def build_group_dn(self, group_name: str) -> str: def build_group_filterstr(self, group_name: str = None, username: str = None) -> str: filterstr = self.ldap_group_filterstr if group_name is not None: - filterstr = f'{filterstr}(cn={group_name})' + # Check if we have a fully qualified + if group_name.upper().startswith('CN='): + filterstr = f'{filterstr}(distinguishedName={group_name})' + else: + filterstr = f'{filterstr}(cn={group_name})' if username is not None: filterstr = f'{filterstr}(memberUid={username})' return f'(&{filterstr})' @@ -546,6 +577,7 @@ def convert_ldap_user(self, ldap_user: Dict) -> Optional[Dict]: return result def get_group(self, group_name: str) -> Optional[Dict]: + self.logger.debug(f"abstract_ldap-get_group(): Attempting to lookup {group_name} . Base: {self.ldap_group_base}") result = self.search_s( base=self.ldap_group_base, filterstr=self.build_group_filterstr(group_name) diff --git a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/api/api_invoker.py b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/api/api_invoker.py index 2804a01..670ae92 100644 --- a/source/idea/idea-cluster-manager/src/ideaclustermanager/app/api/api_invoker.py +++ b/source/idea/idea-cluster-manager/src/ideaclustermanager/app/api/api_invoker.py @@ -18,6 +18,8 @@ CreateUserRequest, InitiateAuthRequest, InitiateAuthResult, + ListUsersResult, + ListGroupsResult, RespondToAuthChallengeRequest, RespondToAuthChallengeResult, ChangePasswordRequest, @@ -54,6 +56,11 @@ def __init__(self, context: ideaclustermanager.AppContext): self.auth_api = AuthAPI(context) self.accounts_api = AccountsAPI(context) self.email_templates_api = EmailTemplatesAPI(context) + self.max_listings_for_logging = 10 + self.auto_truncate_responses = { + "Accounts.ListUsers": ListUsersResult, + "Accounts.ListGroups": ListGroupsResult + } def get_token_service(self) -> Optional[TokenService]: return self._context.token_service @@ -108,6 +115,11 @@ def get_response_logging_payload(self, context: ApiInvocationContext) -> Optiona return None namespace = context.namespace + + # Namespaces that are subject to listing truncating + # to keep the logs usable in larger environments. + + if namespace == 'Auth.InitiateAuth': response = context.get_response(deep_copy=True) payload = context.get_response_payload_as(InitiateAuthResult) @@ -147,6 +159,18 @@ def get_response_logging_payload(self, context: ApiInvocationContext) -> Optiona payload.lines = ['****'] response['payload'] = Utils.to_dict(payload) return response + elif namespace in self.auto_truncate_responses: + request = context.get_response(deep_copy=True) + payload = context.get_response_payload_as(self.auto_truncate_responses.get(namespace)) + payload_listing_count = Utils.get_as_int(len(Utils.get_as_list(payload.listing, default=[])), default=0) + + if payload_listing_count > self.max_listings_for_logging: + # This would normally be a log_debug() but the API response log line comes out as INFO + # We want to make sure we do not confuse the admin log-user by showing a truncated list + context.log_info(f"Truncated {payload_listing_count - self.max_listings_for_logging} entries from payload logging for {namespace} ({payload_listing_count} non-truncated listings)") + payload.listing = payload.listing[0:self.max_listings_for_logging] + request['payload'] = Utils.to_dict(payload) + return request def invoke(self, context: ApiInvocationContext): namespace = context.namespace diff --git a/source/idea/idea-cluster-manager/src/ideaclustermanager/cli/groups.py b/source/idea/idea-cluster-manager/src/ideaclustermanager/cli/groups.py index b98a8bc..5ce4f63 100644 --- a/source/idea/idea-cluster-manager/src/ideaclustermanager/cli/groups.py +++ b/source/idea/idea-cluster-manager/src/ideaclustermanager/cli/groups.py @@ -26,9 +26,13 @@ CreateGroupRequest, CreateGroupResult, DeleteGroupRequest, - DeleteGroupResult - + DeleteGroupResult, + AddUserToGroupRequest, + AddUserToGroupResult, + RemoveUserFromGroupRequest, + RemoveUserFromGroupResult ) + from ideaclustermanager.cli import build_cli_context from ideasdk.utils import Utils from ideaclustermanager.app.accounts.auth_utils import AuthUtils @@ -216,3 +220,51 @@ def delete_group(groupname: str): ) except exceptions.SocaException as e: context.error(e.message) + +@groups.command(context_settings=constants.CLICK_SETTINGS) +@click.option('--username', required=True, multiple=True, help='Username for operation. Can be specified multiple times (-u user1 -u user2)') +@click.option('--groupname', required=True, help='Groupname to add username(s)') +def add_user_to_group(username: list[str], groupname: str): + """ + Add username(s) to a group. + """ + context = ClusterManagerUtils.get_soca_cli_context_cluster_manager() + result = None + try: + result = context.unix_socket_client.invoke_alt( + namespace='Accounts.AddUserToGroup', + payload=AddUserToGroupRequest( + usernames=username, + group_name=groupname + ), + result_as=AddUserToGroupResult + ) + except exceptions.SocaException as e: + context.error(e.message) + + # Show the groupname in summary? + print(result) + +@groups.command(context_settings=constants.CLICK_SETTINGS) +@click.option('--username', required=True, multiple=True, help='Username for operation. Can be specified multiple times (-u user1 -u user2)') +@click.option('--groupname', required=True, help='Groupname to remove username(s)') +def remove_user_from_group(username: list[str], groupname: str): + """ + Remove username(s) from a group. + """ + context = ClusterManagerUtils.get_soca_cli_context_cluster_manager() + result = None + try: + result = context.unix_socket_client.invoke_alt( + namespace='Accounts.RemoveUserFromGroup', + payload=RemoveUserFromGroupRequest( + usernames=username, + group_name=groupname + ), + result_as=RemoveUserFromGroupResult + ) + except exceptions.SocaException as e: + context.error(e.message) + + # Show the groupname in summary + print(result) diff --git a/source/idea/idea-cluster-manager/src/ideaclustermanager_meta/__init__.py b/source/idea/idea-cluster-manager/src/ideaclustermanager_meta/__init__.py index 8b5e148..d419177 100644 --- a/source/idea/idea-cluster-manager/src/ideaclustermanager_meta/__init__.py +++ b/source/idea/idea-cluster-manager/src/ideaclustermanager_meta/__init__.py @@ -10,4 +10,4 @@ # and limitations under the License. __name__ = 'idea-cluster-manager' -__version__ = '3.1.1' +__version__ = '3.1.2' diff --git a/source/idea/idea-cluster-manager/webapp/.env b/source/idea/idea-cluster-manager/webapp/.env index 1a4ea60..7abcaa9 100644 --- a/source/idea/idea-cluster-manager/webapp/.env +++ b/source/idea/idea-cluster-manager/webapp/.env @@ -1,4 +1,4 @@ REACT_APP_IDEA_HTTP_ENDPOINT="http://localhost:8080" REACT_APP_IDEA_ALB_ENDPOINT="http://localhost:8080" REACT_APP_IDEA_HTTP_API_SUFFIX="/api/v1" -REACT_APP_IDEA_RELEASE_VERSION="3.1.1" +REACT_APP_IDEA_RELEASE_VERSION="3.1.2" diff --git a/source/idea/idea-cluster-manager/webapp/package.json b/source/idea/idea-cluster-manager/webapp/package.json index ceca7d5..bf1d3a8 100644 --- a/source/idea/idea-cluster-manager/webapp/package.json +++ b/source/idea/idea-cluster-manager/webapp/package.json @@ -1,6 +1,6 @@ { "name": "web-portal", - "version": "3.1.1", + "version": "3.1.2", "private": true, "dependencies": { "@cloudscape-design/components": "^3.0.82", diff --git a/source/idea/idea-cluster-manager/webapp/src/common/authentication-context.ts b/source/idea/idea-cluster-manager/webapp/src/common/authentication-context.ts index e2ef7f8..2776543 100644 --- a/source/idea/idea-cluster-manager/webapp/src/common/authentication-context.ts +++ b/source/idea/idea-cluster-manager/webapp/src/common/authentication-context.ts @@ -34,7 +34,7 @@ const KEY_ID_TOKEN = 'id-token' const KEY_SSO_AUTH = 'sso-auth' const HEADER_CONTENT_TYPE_JSON = 'application/json;charset=UTF-8' -const NETWORK_TIMEOUT = 10000 +const NETWORK_TIMEOUT = 30000 /** * IDEA Authentication Context diff --git a/source/idea/idea-cluster-manager/webapp/src/common/utils.ts b/source/idea/idea-cluster-manager/webapp/src/common/utils.ts index cf4ee85..6609d76 100644 --- a/source/idea/idea-cluster-manager/webapp/src/common/utils.ts +++ b/source/idea/idea-cluster-manager/webapp/src/common/utils.ts @@ -430,7 +430,7 @@ class Utils { break case 'NVIDIA': options.push({ - title: 'NVIDA', + title: 'NVIDIA', value: 'NVIDIA' }) } diff --git a/source/idea/idea-cluster-manager/webapp/src/components/navbar/navbar.tsx b/source/idea/idea-cluster-manager/webapp/src/components/navbar/navbar.tsx index aca0f2f..edbafcd 100644 --- a/source/idea/idea-cluster-manager/webapp/src/components/navbar/navbar.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/components/navbar/navbar.tsx @@ -182,7 +182,7 @@ class IdeaNavbar extends Component { const getPasswordExpirationMessage = () => { const expiresIn = AppContext.get().auth().getPasswordExpiresInDays() - if(expiresIn < 0) { + if(expiresIn < 0 || expiresIn > 10) { return } let expiryText @@ -198,12 +198,13 @@ class IdeaNavbar extends Component { return expiryText } - let hasNotifications = true + let hasNotifications = false const getNotifications = () => { let passwordExpirationMessage = getPasswordExpirationMessage() let notifications: any = [] if(passwordExpirationMessage) { + hasNotifications = true notifications.push({ id: 'password-expiration', text: passwordExpirationMessage, diff --git a/source/idea/idea-cluster-manager/webapp/src/docs/my-virtual-desktop-sessions.md b/source/idea/idea-cluster-manager/webapp/src/docs/my-virtual-desktop-sessions.md index 41d95e0..f036a9f 100644 --- a/source/idea/idea-cluster-manager/webapp/src/docs/my-virtual-desktop-sessions.md +++ b/source/idea/idea-cluster-manager/webapp/src/docs/my-virtual-desktop-sessions.md @@ -7,7 +7,7 @@ Manage your Windows & Linux virtual desktops, powered by [AWS NICE DCV](https:// Create a new desktop -Click **"Launch new Virtual Desktop"** and follow the instructions to create your virtual desktop. +Click **"Launch New Virtual Desktop"** and follow the instructions to create your virtual desktop. diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/cluster-admin/cluster-status.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/cluster-admin/cluster-status.tsx index 2926a49..475ef2e 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/cluster-admin/cluster-status.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/cluster-admin/cluster-status.tsx @@ -367,7 +367,7 @@ class ClusterStatus extends Component { cell: (item: any) => { if (item.type === Constants.MODULE_TYPE_APP) { /* If the module is not deployed by admin-choice - don't alarm the admin with red status */ - if (item.status == 'not-deployed') { + if (item.status === 'not-deployed') { return Not Applicable } else { return diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/hpc/jobs.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/hpc/jobs.tsx index 197a211..ce12182 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/hpc/jobs.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/hpc/jobs.tsx @@ -332,7 +332,7 @@ class Jobs extends Component { - + @@ -342,7 +342,7 @@ class Jobs extends Component { - + ) }, @@ -353,7 +353,7 @@ class Jobs extends Component { - + @@ -363,8 +363,8 @@ class Jobs extends Component { - - + + diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/hpc/queues.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/hpc/queues.tsx index 122a289..2a55e96 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/hpc/queues.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/hpc/queues.tsx @@ -284,7 +284,7 @@ class Queues extends Component { - + {!Utils.asBoolean(selected().keep_forever) && } diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/hpc/submit-job.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/hpc/submit-job.tsx index 6ea5883..fb82ff6 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/hpc/submit-job.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/hpc/submit-job.tsx @@ -206,7 +206,7 @@ class SubmitJob extends Component { jobSubmissionParameters = { ...jobSubmissionParameters, input_file: inputFile, - job_name: inputFile.substring(inputFile.lastIndexOf('/') + 1, inputFile.length) + job_name: inputFile.substring(inputFile.lastIndexOf('/') + 1, inputFile.length).replaceAll(`.`, '_'), } } @@ -786,7 +786,7 @@ class SubmitJob extends Component { -

Estimated Total Cost

+

Estimated Total Cost Per Hour

{Utils.getFormattedAmount(estimated_bom_cost?.total)}

diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/hpc/update-queue-profile.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/hpc/update-queue-profile.tsx index 58402df..d4a9a76 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/hpc/update-queue-profile.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/hpc/update-queue-profile.tsx @@ -137,7 +137,7 @@ class HpcUpdateQueueProfile extends Component { - if(request.param === 'project_ids') { + if (request.param === 'project_ids') { return AppContext.get().client().projects().listProjects({ paginator: { page_size: 100 @@ -294,19 +294,19 @@ class HpcUpdateQueueProfile extends Component this.schedulerAdmin().updateQueueProfile(params) } + return invoke({ queue_profile: queueProfile }).then(_ => { diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/user-management/users.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/user-management/users.tsx index 6b0681a..37db89a 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/user-management/users.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/user-management/users.tsx @@ -90,25 +90,6 @@ export const USER_TABLE_COLUMN_DEFINITIONS: TableProps.ColumnDefinition[] } } }, - { - id: 'confirmation_status', - header: 'Confirmation Status', - cell: e => { - let status - if (e.status === 'CONFIRMED') { - status = 'Confirmed' - } else if (e.status === 'PENDING_CONFIRMATION') { - status = 'Pending Confirmation' - } else if (e.status === 'FORCE_CHANGE_PASSWORD') { - status = 'Force Change Password' - } else if (e.status === 'RESET_REQUIRED') { - status = 'Reset Required' - } else { - status = 'Unknown' - } - return status - } - }, { id: 'created_on', header: 'Created On', diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/forms/virtual-desktop-create-session-form.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/forms/virtual-desktop-create-session-form.tsx index acc2d9b..fe0256b 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/forms/virtual-desktop-create-session-form.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/forms/virtual-desktop-create-session-form.tsx @@ -447,7 +447,7 @@ class VirtualDesktopCreateSessionForm extends Component { if (event.param.name === 'base_os') { diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/forms/virtual-desktop-update-session-permissions-form.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/forms/virtual-desktop-update-session-permissions-form.tsx index 30ee7d1..a148d8c 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/forms/virtual-desktop-update-session-permissions-form.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/forms/virtual-desktop-update-session-permissions-form.tsx @@ -227,7 +227,7 @@ class PermissionRow extends Component { { name: 'actor', data_type: 'str', - param_type: 'select', + param_type: 'select_or_text', readonly: this.props.existing, default: this.getDefaultUserChoice(), validate: { diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/my-virtual-desktop-sessions.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/my-virtual-desktop-sessions.tsx index 8380847..a8aa9fb 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/my-virtual-desktop-sessions.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/my-virtual-desktop-sessions.tsx @@ -1001,7 +1001,7 @@ class MyVirtualDesktopSessions extends Component { this.showCreateSessionForm() }}> - Launch new Virtual Desktop + Launch New Virtual Desktop }> @@ -1095,7 +1095,7 @@ class MyVirtualDesktopSessions extends Component Click the button below to create a new virtual desktop.
- + } items={getSessions()} diff --git a/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/virtual-desktop-settings.tsx b/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/virtual-desktop-settings.tsx index 107ae7d..d1ee549 100644 --- a/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/virtual-desktop-settings.tsx +++ b/source/idea/idea-cluster-manager/webapp/src/pages/virtual-desktops/virtual-desktop-settings.tsx @@ -201,10 +201,17 @@ class VirtualDesktopSettings extends Component General}> - + + + + + + + + Info}>OpenAPI Specification}> diff --git a/source/idea/idea-data-model/src/ideadatamodel/aws/model.py b/source/idea/idea-data-model/src/ideadatamodel/aws/model.py index 2dc776b..4c4dfc6 100644 --- a/source/idea/idea-data-model/src/ideadatamodel/aws/model.py +++ b/source/idea/idea-data-model/src/ideadatamodel/aws/model.py @@ -26,7 +26,8 @@ 'EC2PrefixList', 'EC2PrefixListEntry', 'CognitoUser', - 'CognitoUserMFAOptions' + 'CognitoUserMFAOptions', + 'CognitoUserPoolPasswordPolicy' ) from ideadatamodel.aws import EC2Instance @@ -244,3 +245,11 @@ def uid(self) -> Optional[int]: def gid(self) -> Optional[int]: gid = self.get_user_attribute('custom:gid') return ModelUtils.get_as_int(gid, None) + +class CognitoUserPoolPasswordPolicy(SocaBaseModel): + minimum_length: Optional[int] + require_uppercase: Optional[bool] + require_lowercase: Optional[bool] + require_numbers: Optional[bool] + require_symbols: Optional[bool] + temporary_password_validity_days: Optional[int] diff --git a/source/idea/idea-data-model/src/ideadatamodel/constants.py b/source/idea/idea-data-model/src/ideadatamodel/constants.py index 9e9e659..ec8ee27 100644 --- a/source/idea/idea-data-model/src/ideadatamodel/constants.py +++ b/source/idea/idea-data-model/src/ideadatamodel/constants.py @@ -37,6 +37,30 @@ STORAGE_PROVIDER_FSX_WINDOWS_FILE_SERVER ] +# Volume Type strings +VOLUME_TYPE_GP2 = 'gp2' +VOLUME_TYPE_GP3 = 'gp3' +VOLUME_TYPE_IO1 = 'io1' +VOLUME_TYPE_IO2 = 'io2' +# +# +# EBS volume type defaults +# +DEFAULT_VOLUME_TYPE_SCRATCH = VOLUME_TYPE_IO1 +DEFAULT_VOLUME_TYPE_COMPUTE = VOLUME_TYPE_GP3 +DEFAULT_VOLUME_TYPE_VDI = VOLUME_TYPE_GP3 +# +# EBS volume encryption behavior +# +DEFAULT_VOLUME_ENCRYPTION_VDI = True +DEFAULT_VOLUME_ENCRYPTION_COMPUTE = True + + +# VDI networking options +DEFAULT_VDI_RANDOMIZE_SUBNETS = False +DEFAULT_VDI_SUBNET_AUTORETRY = True + + SCHEDULER_OPENPBS = 'openpbs' QUEUE_MODE_FIFO = 'fifo' @@ -70,35 +94,37 @@ AWS_TAG_EC2SPOT_FLEET_REQUEST_ID = 'aws:ec2spot:fleet-request-id' AWS_TAG_AUTOSCALING_GROUP_NAME = 'aws:autoscaling:groupName' -IDEA_TAG_NODE_TYPE = 'idea:NodeType' +IDEA_TAG_PREFIX = 'idea:' + +IDEA_TAG_NODE_TYPE = IDEA_TAG_PREFIX + 'NodeType' -IDEA_TAG_CLUSTER_NAME = 'idea:ClusterName' -IDEA_TAG_MODULE_ID = 'idea:ModuleId' -IDEA_TAG_MODULE_NAME = 'idea:ModuleName' -IDEA_TAG_MODULE_VERSION = 'idea:ModuleVersion' -IDEA_TAG_PROJECT = 'idea:Project' -IDEA_TAG_AMI_BUILDER = 'idea:AmiBuilder' +IDEA_TAG_CLUSTER_NAME = IDEA_TAG_PREFIX + 'ClusterName' +IDEA_TAG_MODULE_ID = IDEA_TAG_PREFIX + 'ModuleId' +IDEA_TAG_MODULE_NAME = IDEA_TAG_PREFIX + 'ModuleName' +IDEA_TAG_MODULE_VERSION = IDEA_TAG_PREFIX + 'ModuleVersion' +IDEA_TAG_PROJECT = IDEA_TAG_PREFIX + 'Project' +IDEA_TAG_AMI_BUILDER = IDEA_TAG_PREFIX + 'AmiBuilder' IDEA_TAG_NAME = 'Name' -IDEA_TAG_JOB_ID = 'idea:JobId' -IDEA_TAG_JOB_GROUP = 'idea:JobGroup' -IDEA_TAG_JOB_NAME = 'idea:JobName' -IDEA_TAG_JOB_OWNER = 'idea:JobOwner' -IDEA_TAG_JOB_QUEUE = 'idea:JobQueue' -IDEA_TAG_KEEP_FOREVER = 'idea:KeepForever' -IDEA_TAG_TERMINATE_WHEN_IDLE = 'idea:TerminateWhenIdle' -IDEA_TAG_QUEUE_TYPE = 'idea:QueueType' -IDEA_TAG_SCALING_MODE = 'idea:ScalingMode' -IDEA_TAG_CAPACITY_TYPE = 'idea:CapacityType' -IDEA_TAG_FSX = 'idea:FSx' -IDEA_TAG_COMPUTE_STACK = 'idea:StackId' -IDEA_TAG_CREATED_FROM = 'idea:CreatedFrom' -IDEA_TAG_CREATED_ON = 'idea:CreatedOn' -IDEA_TAG_BACKUP_PLAN = 'idea:BackupPlan' -IDEA_TAG_STACK_TYPE = 'idea:StackType' -IDEA_TAG_IDEA_SESSION_ID = 'idea:IDEASessionUUID' -IDEA_TAG_DCV_SESSION_ID = 'idea:DCVSessionUUID' +IDEA_TAG_JOB_ID = IDEA_TAG_PREFIX + 'JobId' +IDEA_TAG_JOB_GROUP = IDEA_TAG_PREFIX + 'JobGroup' +IDEA_TAG_JOB_NAME = IDEA_TAG_PREFIX + 'JobName' +IDEA_TAG_JOB_OWNER = IDEA_TAG_PREFIX + 'JobOwner' +IDEA_TAG_JOB_QUEUE = IDEA_TAG_PREFIX + 'JobQueue' +IDEA_TAG_KEEP_FOREVER = IDEA_TAG_PREFIX + 'KeepForever' +IDEA_TAG_TERMINATE_WHEN_IDLE = IDEA_TAG_PREFIX + 'TerminateWhenIdle' +IDEA_TAG_QUEUE_TYPE = IDEA_TAG_PREFIX + 'QueueType' +IDEA_TAG_SCALING_MODE = IDEA_TAG_PREFIX + 'ScalingMode' +IDEA_TAG_CAPACITY_TYPE = IDEA_TAG_PREFIX + 'CapacityType' +IDEA_TAG_FSX = IDEA_TAG_PREFIX + 'FSx' +IDEA_TAG_COMPUTE_STACK = IDEA_TAG_PREFIX + 'StackId' +IDEA_TAG_CREATED_FROM = IDEA_TAG_PREFIX + 'CreatedFrom' +IDEA_TAG_CREATED_ON = IDEA_TAG_PREFIX + 'CreatedOn' +IDEA_TAG_BACKUP_PLAN = IDEA_TAG_PREFIX + 'BackupPlan' +IDEA_TAG_STACK_TYPE = IDEA_TAG_PREFIX + 'StackType' +IDEA_TAG_IDEA_SESSION_ID = IDEA_TAG_PREFIX + 'IDEASessionUUID' +IDEA_TAG_DCV_SESSION_ID = IDEA_TAG_PREFIX + 'DCVSessionUUID' NODE_TYPE_COMPUTE = 'compute-node' NODE_TYPE_DCV_HOST = 'virtual-desktop-dcv-host' diff --git a/source/idea/idea-data-model/src/ideadatamodel_meta/__init__.py b/source/idea/idea-data-model/src/ideadatamodel_meta/__init__.py index b2adf5d..da3c352 100644 --- a/source/idea/idea-data-model/src/ideadatamodel_meta/__init__.py +++ b/source/idea/idea-data-model/src/ideadatamodel_meta/__init__.py @@ -10,4 +10,4 @@ # and limitations under the License. __name__ = 'idea-data-model' -__version__ = '3.1.1' +__version__ = '3.1.2' diff --git a/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/job_provisioner/cloudformation_stack_builder.py b/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/job_provisioner/cloudformation_stack_builder.py index fce235e..84d41f0 100644 --- a/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/job_provisioner/cloudformation_stack_builder.py +++ b/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/job_provisioner/cloudformation_stack_builder.py @@ -168,11 +168,10 @@ def build_user_data(self): Key=bootstrap_package_key ) - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, @@ -374,14 +373,19 @@ def build_launch_template(self) -> LaunchTemplate: user_data = self.build_user_data() launch_template_data.UserData = Base64(Sub(user_data)) + kms_key_id = self.context.config().get_string('cluster.ebs.kms_key_id', required=False, default=None) + if kms_key_id is None: + kms_key_id = 'alias/aws/ebs' + launch_template_data.BlockDeviceMappings = [ LaunchTemplateBlockDeviceMapping( DeviceName=Utils.get_ec2_block_device_name(base_os=self.job.params.base_os), Ebs=EBSBlockDevice( VolumeSize=self.job.params.root_storage_size.int_val(), - VolumeType='gp3', + VolumeType=constants.DEFAULT_VOLUME_TYPE_COMPUTE, DeleteOnTermination=not self.job.params.keep_ebs_volumes, - Encrypted=True + Encrypted=constants.DEFAULT_VOLUME_ENCRYPTION_COMPUTE, + KmsKeyId=kms_key_id ) ) ] @@ -393,10 +397,11 @@ def build_launch_template(self) -> LaunchTemplate: DeviceName='/dev/xvdbx', Ebs=EBSBlockDevice( VolumeSize=self.job.params.scratch_storage_size.int_val(), - VolumeType='io1' if iops > 0 else 'gp3', + VolumeType= Utils.get_as_string(constants.DEFAULT_VOLUME_TYPE_SCRATCH, default='io1') if iops > 0 else Utils.get_as_string(constants.DEFAULT_VOLUME_TYPE_COMPUTE, default='gp3'), Iops=iops if iops > 0 else Ref('AWS::NoValue'), DeleteOnTermination=not self.job.params.keep_ebs_volumes, - Encrypted=True + Encrypted=Utils.get_as_bool(constants.DEFAULT_VOLUME_ENCRYPTION_COMPUTE, default=True), + KmsKeyId=kms_key_id ) ) ) diff --git a/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/job_provisioning_queue/hpc_queue_profiles_service.py b/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/job_provisioning_queue/hpc_queue_profiles_service.py index ca57467..2af4d6d 100644 --- a/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/job_provisioning_queue/hpc_queue_profiles_service.py +++ b/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/job_provisioning_queue/hpc_queue_profiles_service.py @@ -102,7 +102,7 @@ def validate_and_sanitize_queue_profile(self, queue_profile: HpcQueueProfile): raise exceptions.invalid_params('queue_profile.name is required') if Utils.is_empty(queue_profile.queues): raise exceptions.invalid_params('queue_profile.queues is required') - if Utils.is_empty(queue_profile.scaling_mode): + if Utils.is_empty(queue_profile.scaling_mode) and not Utils.get_as_bool(queue_profile.keep_forever, False): raise exceptions.invalid_params('queue_profile.scaling_mode is required') if Utils.is_empty(queue_profile.queue_mode): raise exceptions.invalid_params('queue_profile.queue_mode is required') @@ -113,17 +113,15 @@ def validate_and_sanitize_queue_profile(self, queue_profile: HpcQueueProfile): raise exceptions.invalid_params('queue_profile.projects[] is required') terminate_when_idle = queue_profile.terminate_when_idle - if queue_profile.scaling_mode == SocaScalingMode.BATCH: + if queue_profile.scaling_mode is not None and queue_profile.scaling_mode == SocaScalingMode.BATCH: if terminate_when_idle is None or terminate_when_idle <= 0: raise exceptions.invalid_params('queue_profile.terminate_when_idle must be required and > 0 when scaling_mode == BATCH') - elif queue_profile.scaling_mode == SocaScalingMode.SINGLE_JOB: + elif queue_profile.scaling_mode is not None and queue_profile.scaling_mode == SocaScalingMode.SINGLE_JOB: if terminate_when_idle is not None and terminate_when_idle > 0: raise exceptions.invalid_params('queue_profile.terminate_when_idle must be 0 when scaling_mode == SINGLE_JOB') keep_forever = queue_profile.keep_forever if keep_forever is not None and keep_forever: - if terminate_when_idle is not None and terminate_when_idle > 0: - raise exceptions.invalid_params('queue_profile.terminate_when_idle must be 0 when keep_forever == True') if queue_profile.stack_uuid is None: queue_profile.stack_uuid = Utils.uuid() diff --git a/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/node_monitor/node_house_keeper.py b/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/node_monitor/node_house_keeper.py index f46928c..fbdee30 100644 --- a/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/node_monitor/node_house_keeper.py +++ b/source/idea/idea-scheduler/src/ideascheduler/app/provisioning/node_monitor/node_house_keeper.py @@ -234,7 +234,7 @@ def _can_terminate(self, instance: EC2Instance, node: SocaComputeNode) -> bool: if job is not None: return False - if instance.soca_keep_forever: + if instance.soca_keep_forever and instance.soca_terminate_when_idle == 0: return False terminate_when_idle = instance.soca_terminate_when_idle diff --git a/source/idea/idea-scheduler/src/ideascheduler/app/scheduler/job_param_builder.py b/source/idea/idea-scheduler/src/ideascheduler/app/scheduler/job_param_builder.py index 4e8c472..4ad3485 100644 --- a/source/idea/idea-scheduler/src/ideascheduler/app/scheduler/job_param_builder.py +++ b/source/idea/idea-scheduler/src/ideascheduler/app/scheduler/job_param_builder.py @@ -2769,8 +2769,8 @@ def _initialize(self): self.add_builder(EnableScratchParamBuilder(job_param=constants.JOB_PARAM_ENABLE_SCRATCH, context=self)) self.add_builder(EnableSystemMetricsParamBuilder(job_param=constants.JOB_PARAM_ENALE_SYSTEM_METRICS, context=self)) self.add_builder(EnableAnonymousMetricsParamBuilder(job_param=constants.JOB_PARAM_ENABLE_ANONYMOUS_METRICS, context=self)) - self.add_builder(TerminateWhenIdleOptionBuilder(job_param=constants.JOB_OPTION_TERMINATE_WHEN_IDLE, context=self)) self.add_builder(KeepForeverOptionBuilder(job_param=constants.JOB_OPTION_KEEP_FOREVER, context=self)) + self.add_builder(TerminateWhenIdleOptionBuilder(job_param=constants.JOB_OPTION_TERMINATE_WHEN_IDLE, context=self)) self.add_builder(TagsOptionBuilder(job_param=constants.JOB_OPTION_TAGS, context=self)) self.add_builder(LicensesParamBuilder(job_param=constants.JOB_PARAM_LICENSES, context=self)) self.add_builder(ComputeStackParamBuilder(job_param=constants.JOB_PARAM_COMPUTE_STACK, context=self)) diff --git a/source/idea/idea-scheduler/src/ideascheduler/app/scheduler_default_settings.py b/source/idea/idea-scheduler/src/ideascheduler/app/scheduler_default_settings.py index 5e078e7..f1417b6 100644 --- a/source/idea/idea-scheduler/src/ideascheduler/app/scheduler_default_settings.py +++ b/source/idea/idea-scheduler/src/ideascheduler/app/scheduler_default_settings.py @@ -252,7 +252,7 @@ def create_applications(self): }, { "title": "5 minutes", - "value": "120" + "value": "300" }, { "title": "10 minutes", diff --git a/source/idea/idea-scheduler/src/ideascheduler/cli/ami_builder.py b/source/idea/idea-scheduler/src/ideascheduler/cli/ami_builder.py index 1fb95f6..d97514d 100644 --- a/source/idea/idea-scheduler/src/ideascheduler/cli/ami_builder.py +++ b/source/idea/idea-scheduler/src/ideascheduler/cli/ami_builder.py @@ -247,11 +247,10 @@ def build_userdata(self, upload_to_s3: bool = True) -> str: Key=bootstrap_package_key ) - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, diff --git a/source/idea/idea-scheduler/src/ideascheduler_meta/__init__.py b/source/idea/idea-scheduler/src/ideascheduler_meta/__init__.py index c48432e..6ca9e2d 100644 --- a/source/idea/idea-scheduler/src/ideascheduler_meta/__init__.py +++ b/source/idea/idea-scheduler/src/ideascheduler_meta/__init__.py @@ -12,4 +12,4 @@ # pkgconfig for ideascheduler. no dependencies # noqa __name__ = 'idea-scheduler' -__version__ = '3.1.1' +__version__ = '3.1.2' diff --git a/source/idea/idea-sdk/src/ideasdk/aws/aws_util.py b/source/idea/idea-sdk/src/ideasdk/aws/aws_util.py index dca01a2..3a73245 100644 --- a/source/idea/idea-sdk/src/ideasdk/aws/aws_util.py +++ b/source/idea/idea-sdk/src/ideasdk/aws/aws_util.py @@ -978,6 +978,14 @@ def dynamodb_create_table(self, create_table_request: Dict, wait: bool = False, }) create_table_request['Tags'] = updated_tags + dynamodb_kms_key_id = self._context.config().get_string('cluster.dynamodb.kms_key_id') + if dynamodb_kms_key_id is not None: + create_table_request['SSESpecification'] = { + 'Enabled': True, + 'SSEType': 'KMS', + 'KMSMasterKeyId': dynamodb_kms_key_id + } + self.aws().dynamodb().create_table(**create_table_request) if wait or ttl: diff --git a/source/idea/idea-sdk/src/ideasdk/bootstrap/bootstrap_userdata_builder.py b/source/idea/idea-sdk/src/ideasdk/bootstrap/bootstrap_userdata_builder.py index e7be764..bf78eec 100644 --- a/source/idea/idea-sdk/src/ideasdk/bootstrap/bootstrap_userdata_builder.py +++ b/source/idea/idea-sdk/src/ideasdk/bootstrap/bootstrap_userdata_builder.py @@ -126,6 +126,7 @@ def _build_linux_userdata_substitution(self) -> str: if [[ "${!BASE_OS}" == "amazonlinux2" ]]; then yum remove -y awscli fi + cd /root/bootstrap local machine=$(uname -m) if [[ ${!machine} == "x86_64" ]]; then curl -s ${!AWSCLI_X86_64_URL} -o "awscliv2.zip" @@ -138,6 +139,7 @@ def _build_linux_userdata_substitution(self) -> str: fi unzip -q awscliv2.zip ./aws/install --bin-dir /bin --update + rm -rf aws awscliv2.zip } echo "#!/bin/bash @@ -219,6 +221,7 @@ def _build_linux_userdata_non_substitution(self) -> str: if [[ "${BASE_OS}" == "amazonlinux2" ]]; then yum remove -y awscli fi + cd /root/bootstrap local machine=$(uname -m) if [[ ${machine} == "x86_64" ]]; then curl -s ${AWSCLI_X86_64_URL} -o "awscliv2.zip" @@ -231,6 +234,7 @@ def _build_linux_userdata_non_substitution(self) -> str: fi unzip -q awscliv2.zip ./aws/install --bin-dir /bin --update + rm -rf aws awscliv2.zip } echo "#!/bin/bash diff --git a/source/idea/idea-sdk/src/ideasdk/config/cluster_config_db.py b/source/idea/idea-sdk/src/ideasdk/config/cluster_config_db.py index 925ebd3..290aae2 100644 --- a/source/idea/idea-sdk/src/ideasdk/config/cluster_config_db.py +++ b/source/idea/idea-sdk/src/ideasdk/config/cluster_config_db.py @@ -40,7 +40,7 @@ class ClusterConfigDB(DynamoDBStreamSubscriber): * implementation from AwsUtil.dynamodb_create_table() cannot be used in this class and tables must be created manually """ - def __init__(self, cluster_name: str, aws_region: str, aws_profile: Optional[str], create_database: bool = False, create_subscription: bool = False, cluster_config_subscriber: Optional[DynamoDBStreamSubscriber] = None, logger=None): + def __init__(self, cluster_name: str, aws_region: str, aws_profile: Optional[str], dynamodb_kms_key_id: Optional[str] = None, create_database: bool = False, create_subscription: bool = False, cluster_config_subscriber: Optional[DynamoDBStreamSubscriber] = None, logger=None): self.logger = logger @@ -58,6 +58,7 @@ def __init__(self, cluster_name: str, aws_region: str, aws_profile: Optional[str self.create_database = create_database self.create_subscription = create_subscription self.stream_subscriber = cluster_config_subscriber + self.dynamodb_kms_key_id = dynamodb_kms_key_id self.module_metadata_helper = ModuleMetadataHelper() self.aws = AwsClientProvider( @@ -142,6 +143,14 @@ def get_or_create_modules_table(self): except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] == 'ResourceNotFoundException': + SSESpecification = {} + if self.dynamodb_kms_key_id is not None: + self.log_info(f'detected cluster.dynamodb.kms_key_id is set to: {self.dynamodb_kms_key_id}') + SSESpecification = { + 'Enabled': True, + 'SSEType': 'KMS', + 'KMSMasterKeyId': self.dynamodb_kms_key_id + } if self.create_database: self.log_info(f'creating cluster config dynamodb table: {table_name}, region: {self.aws_region}') self.aws.dynamodb().create_table( @@ -159,6 +168,7 @@ def get_or_create_modules_table(self): } ], BillingMode='PAY_PER_REQUEST', + SSESpecification=SSESpecification, Tags=[ { 'Key': constants.IDEA_TAG_CLUSTER_NAME, @@ -194,6 +204,13 @@ def get_or_create_cluster_settings_table(self): except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] == 'ResourceNotFoundException': + SSESpecification = {} + if self.dynamodb_kms_key_id is not None: + SSESpecification = { + 'Enabled': True, + 'SSEType': 'KMS', + 'KMSMasterKeyId': self.dynamodb_kms_key_id + } if self.create_database: self.log_info(f'creating cluster config dynamodb table: {table_name}, region: {self.aws_region}') self.aws.dynamodb().create_table( @@ -215,6 +232,7 @@ def get_or_create_cluster_settings_table(self): 'StreamEnabled': True, 'StreamViewType': 'NEW_AND_OLD_IMAGES' }, + SSESpecification=SSESpecification, Tags=[ { 'Key': constants.IDEA_TAG_CLUSTER_NAME, diff --git a/source/idea/idea-sdk/src/ideasdk/context/arn_builder.py b/source/idea/idea-sdk/src/ideasdk/context/arn_builder.py index ae07094..5bda7ff 100644 --- a/source/idea/idea-sdk/src/ideasdk/context/arn_builder.py +++ b/source/idea/idea-sdk/src/ideasdk/context/arn_builder.py @@ -238,10 +238,80 @@ def get_route53_hostedzone_arn(self) -> str: return f'arn:{self.config.get_string("cluster.aws.partition", required=True)}:route53:::hostedzone/*' @property - def kms_key_arn(self) -> str: - return self.get_arn(service='kms', - resource=f'key/{self.config.get_string("cluster.secretsmanager.kms_key_id")}', - aws_region=self.config.get_string("cluster.aws.region")) + def kms_secretsmanager_key_arn(self) -> str: + return(self.get_arn(service='kms', + resource=f'key/{self.config.get_string("cluster.secretsmanager.kms_key_id")}', + aws_region=self.config.get_string("cluster.aws.region"))) + + @property + def kms_sqs_key_arn(self) -> str: + return(self.get_arn(service='kms', + resource=f'key/{self.config.get_string("cluster.sqs.kms_key_id")}', + aws_region=self.config.get_string("cluster.aws.region"))) + + @property + def kms_sns_key_arn(self) -> str: + return(self.get_arn(service='kms', + resource=f'key/{self.config.get_string("cluster.sns.kms_key_id")}', + aws_region=self.config.get_string("cluster.aws.region"))) + + @property + def kms_dynamodb_key_arn(self) -> str: + return(self.get_arn(service='kms', + resource=f'key/{self.config.get_string("cluster.dynamodb.kms_key_id")}', + aws_region=self.config.get_string("cluster.aws.region"))) + @property + def kms_ebs_key_arn(self) -> str: + return(self.get_arn(service='kms', + resource=f'key/{self.config.get_string("cluster.ebs.kms_key_id")}', + aws_region=self.config.get_string("cluster.aws.region"))) + + @property + def kms_backup_key_arn(self) -> str: + return(self.get_arn(service='kms', + resource=f'key/{self.config.get_string("cluster.backups.backup_vault.kms_key_id")}', + aws_region=self.config.get_string("cluster.aws.region"))) + + @property + def kms_opensearch_key_arn(self) -> str: + return(self.get_arn(service='kms', + resource=f'key/{self.config.get_string("analytics.opensearch.kms_key_id")}', + aws_region=self.config.get_string("cluster.aws.region"))) + + @property + def kms_kinesis_key_arn(self) -> str: + return(self.get_arn(service='kms', + resource=f'key/{self.config.get_string("analytics.kinesis.kms_key_id")}', + aws_region=self.config.get_string("cluster.aws.region"))) + + @property + def kms_key_arn(self) -> List[str]: + kms_key_arns = [] + service_kms_key_ids = { + 'secretsmanager': 'cluster.secretsmanager.kms_key_id', + 'sqs': 'cluster.sqs.kms_key_id', + 'sns': 'cluster.sns.kms_key_id', + 'dynamodb': 'cluster.dynamodb.kms_key_id', + 'ebs': 'cluster.ebs.kms_key_id', + 'backup': 'cluster.backups.backup_vault.kms_key_id', + 'opensearch': 'analytics.opensearch.kms_key_id', + 'kinesis': 'analytics.kinesis.kms_key_id' + } + service_kms_key_arns = { + 'secretsmanager': self.kms_secretsmanager_key_arn, + 'sqs': self.kms_sqs_key_arn, + 'sns': self.kms_sns_key_arn, + 'dynamodb': self.kms_dynamodb_key_arn, + 'ebs': self.kms_ebs_key_arn, + 'backup': self.kms_backup_key_arn, + 'opensearch': self.kms_opensearch_key_arn, + 'kinesis': self.kms_kinesis_key_arn + } + for service in service_kms_key_arns.keys(): + if self.config.get_string(service_kms_key_ids[service]) is not None: + kms_key_arns.append(service_kms_key_arns[service]) + + return kms_key_arns @property def user_pool_arn(self) -> str: diff --git a/source/idea/idea-sdk/src/ideasdk/context/bootstrap_context.py b/source/idea/idea-sdk/src/ideasdk/context/bootstrap_context.py index dccefb4..5fef7f3 100644 --- a/source/idea/idea-sdk/src/ideasdk/context/bootstrap_context.py +++ b/source/idea/idea-sdk/src/ideasdk/context/bootstrap_context.py @@ -78,16 +78,16 @@ def app_deploy_dir(self) -> str: @property def https_proxy(self) -> str: - use_vpc_endpoints = self.config.get_bool('cluster.network.use_vpc_endpoints', default=False) - if use_vpc_endpoints: - return self.config.get_string('cluster.network.https_proxy', required=False, default='') + https_proxy = self.config.get_string('cluster.network.https_proxy', required=False, default='') + if Utils.is_not_empty(https_proxy): + return https_proxy else: return '' @property def no_proxy(self) -> str: - use_vpc_endpoints = self.config.get_bool('cluster.network.use_vpc_endpoints', default=False) - if use_vpc_endpoints: + https_proxy = self.config.get_string('cluster.network.https_proxy', required=False, default='') + if Utils.is_not_empty(https_proxy): return self.config.get_string('cluster.network.no_proxy', required=False, default='') else: return '' @@ -97,7 +97,7 @@ def default_system_user(self) -> str: if self.base_os in ('amazonlinux2', 'rhel7'): return 'ec2-user' if self.base_os == 'centos7': - return 'centos7' + return 'centos' raise exceptions.general_exception(f'unknown system user name for base_os: {self.base_os}') def has_storage_provider(self, provider: str) -> bool: diff --git a/source/idea/idea-sdk/src/ideasdk/context/soca_context.py b/source/idea/idea-sdk/src/ideasdk/context/soca_context.py index 20f17df..3036522 100644 --- a/source/idea/idea-sdk/src/ideasdk/context/soca_context.py +++ b/source/idea/idea-sdk/src/ideasdk/context/soca_context.py @@ -289,7 +289,7 @@ def module_version(self) -> Optional[str]: return ideasdk.__version__ def cluster_timezone(self) -> str: - return self.config().get_string('cluster.cluster_timezone', 'America/Los_Angeles') + return self.config().get_string('cluster.timezone', default='America/Los_Angeles') def get_aws_solution_id(self) -> str: return constants.AWS_SOLUTION_ID diff --git a/source/idea/idea-sdk/src/ideasdk/filesystem/filesystem_helper.py b/source/idea/idea-sdk/src/ideasdk/filesystem/filesystem_helper.py index d3d430b..31995ce 100644 --- a/source/idea/idea-sdk/src/ideasdk/filesystem/filesystem_helper.py +++ b/source/idea/idea-sdk/src/ideasdk/filesystem/filesystem_helper.py @@ -8,6 +8,7 @@ # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # and limitations under the License. +import logging from ideadatamodel.filesystem import ( ListFilesRequest, @@ -32,7 +33,6 @@ import os import arrow -import stat import mimetypes import shutil import aiofiles @@ -134,46 +134,51 @@ def check_access(self, file: str, check_dir=False, check_read=True, check_write= def list_files(self, request: ListFilesRequest) -> ListFilesResult: cwd = request.cwd - user_home = self.get_user_home() + if Utils.is_empty(cwd): - cwd = user_home + cwd = self.get_user_home() + + self.logger.debug(f"list_files() for CWD ({cwd})") self.check_access(cwd, check_dir=True, check_read=True, check_write=False) - result = self.shell.invoke(['su', self.username, '-c', f'ls "{cwd}" -1']) - if result.returncode != 0: - raise exceptions.unauthorized_access() + # This is specifically _after_ the check_access so its timing doesnt impact this timing + if self.logger.isEnabledFor(logging.DEBUG): + listing_start = Utils.current_time_ms() - files = result.stdout.split(os.linesep) result = [] - for file in files: - if Utils.is_empty(file): - continue - file_path = os.path.join(cwd, file) - if os.path.islink(file_path): - # skip symlinks to avoid any security risks - continue + with os.scandir(cwd) as scandir: + for entry in scandir: - if cwd == '/': - if file in RESTRICTED_ROOT_FOLDERS: + if Utils.is_empty(entry): + self.logger.debug(f"Empty Entry found at cwd ({cwd}) Name: ({entry.name})") continue - file_stat = os.stat(file_path) - is_dir = stat.S_ISDIR(file_stat.st_mode) - is_hidden = file.startswith('.') - file_size = None - if not is_dir: - file_size = file_stat.st_size - mod_date = arrow.get(file_stat.st_mtime).datetime - result.append(FileData( - file_id=Utils.shake_256(f'{file}{is_dir}', 5), - name=file, - size=file_size, - mod_date=mod_date, - is_dir=is_dir, - is_hidden=is_hidden - )) + if entry.is_file(): + # Check for restricted files/dirs and do not list them + if cwd == '/' and entry.name in RESTRICTED_ROOT_FOLDERS: + # This is only logged at debug since simply browsing to a directory that has + # restricted folders isn't a sign of problems + self.logger.debug(f"Listing denied for RESTRICTED_ROOT_FOLDERS: cwd ({cwd}) Name: ({entry.name})") + continue + + is_hidden = entry.name.startswith('.') + file_size = None if entry.is_dir() else entry.stat().st_size + mod_date = arrow.get(entry.stat().st_mtime).datetime + + result.append(FileData( + file_id=Utils.shake_256(f'{entry.name}{entry.is_dir()}', 5), + name=entry.name, + size=file_size, + mod_date=mod_date, + is_dir=entry.is_dir(), + is_hidden=is_hidden + )) + + if self.logger.isEnabledFor(logging.DEBUG): + listing_end = Utils.current_time_ms() + self.logger.debug(f"Directory Listing Timing: CWD ({cwd}), Len: {len(result)}, Time taken: {listing_end - listing_start} ms") # noqa: listing_start is only set if logging.DEBUG return ListFilesResult( cwd=cwd, diff --git a/source/idea/idea-sdk/src/ideasdk/logging/soca_logging.py b/source/idea/idea-sdk/src/ideasdk/logging/soca_logging.py index 3cb0ddd..406a9ef 100644 --- a/source/idea/idea-sdk/src/ideasdk/logging/soca_logging.py +++ b/source/idea/idea-sdk/src/ideasdk/logging/soca_logging.py @@ -151,6 +151,7 @@ def _build_handler(self, handler_name, logger_name: Optional[str]): else: file_handler = logging.handlers.TimedRotatingFileHandler( filename=logfile, + encoding=constants.DEFAULT_ENCODING, when=handler_config['when'], interval=int(handler_config['interval']), backupCount=int(handler_config['backupCount']) @@ -268,6 +269,7 @@ def get_custom_file_logger(self, params: CustomFileLoggerParams, log_level=loggi else: file_handler = logging.handlers.TimedRotatingFileHandler( filename=logfile, + encoding=constants.DEFAULT_ENCODING, when=params.when, interval=params.interval, backupCount=params.backupCount diff --git a/source/idea/idea-sdk/src/ideasdk/server/sanic_config.py b/source/idea/idea-sdk/src/ideasdk/server/sanic_config.py index d4bb85d..c66d378 100644 --- a/source/idea/idea-sdk/src/ideasdk/server/sanic_config.py +++ b/source/idea/idea-sdk/src/ideasdk/server/sanic_config.py @@ -70,7 +70,7 @@ "FORWARDED_FOR_HEADER": "X-Forwarded-For", "FORWARDED_SECRET": None, "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec - not used, and is handled by SocaServer - "KEEP_ALIVE_TIMEOUT": 5, # 5 seconds + "KEEP_ALIVE_TIMEOUT": 15, # 15 seconds - give time for follow-up requests "KEEP_ALIVE": True, "MOTD": True, "MOTD_DISPLAY": {}, @@ -81,9 +81,9 @@ "REQUEST_BUFFER_SIZE": 65536, # 64 KiB "REQUEST_MAX_HEADER_SIZE": 8192, # 8 KiB, but cannot exceed 16384 "REQUEST_ID_HEADER": "X-Request-ID", - "REQUEST_MAX_SIZE": 100000000, # 100 megabytes - "REQUEST_TIMEOUT": 60, # 60 seconds - "RESPONSE_TIMEOUT": 60, # 60 seconds + "REQUEST_MAX_SIZE": 10_000_000_000, # 10 Gigabytes + "REQUEST_TIMEOUT": 600, # 10-min - large uploads over slow connections + "RESPONSE_TIMEOUT": 600, # 10-min - large uploads over slow connections "USE_UVLOOP": True, "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte "WEBSOCKET_PING_INTERVAL": 20, diff --git a/source/idea/idea-sdk/src/ideasdk/utils/utils.py b/source/idea/idea-sdk/src/ideasdk/utils/utils.py index 61a2e5b..b8cd55d 100644 --- a/source/idea/idea-sdk/src/ideasdk/utils/utils.py +++ b/source/idea/idea-sdk/src/ideasdk/utils/utils.py @@ -353,6 +353,7 @@ def generate_password(length=8, min_uppercase_chars=1, min_lowercase_chars=1, mi generator.minlchars = min_lowercase_chars generator.minnumbers = min_numbers generator.minschars = min_special_chars + generator.excludeschars = "$" return generator.generate() @staticmethod diff --git a/source/idea/idea-sdk/src/ideasdk_meta/__init__.py b/source/idea/idea-sdk/src/ideasdk_meta/__init__.py index f579894..2861ca5 100644 --- a/source/idea/idea-sdk/src/ideasdk_meta/__init__.py +++ b/source/idea/idea-sdk/src/ideasdk_meta/__init__.py @@ -12,4 +12,4 @@ # pkgconfig for soca-sdk. no dependencies # noqa __name__ = 'idea-sdk' -__version__ = '3.1.1' +__version__ = '3.1.2' diff --git a/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/api/virtual_desktop_api.py b/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/api/virtual_desktop_api.py index 11fbda5..975f4eb 100644 --- a/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/api/virtual_desktop_api.py +++ b/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/api/virtual_desktop_api.py @@ -502,11 +502,9 @@ def complete_create_session_request(self, session: VirtualDesktopSession, contex # self.default_instance_profile_arn = self.context.app_config.virtual_desktop_dcv_host_profile_arn # self.default_security_group = self.context.app_config.virtual_desktop_dcv_host_security_group_id - if Utils.is_empty(session.server.subnet_id): - session.server.subnet_id = random.choice(self.context.config().get_list('cluster.network.private_subnets')) if Utils.is_empty(session.server.key_pair_name): - session.server.key_pair_name = self.context.config().get_string('cluster.network.ssh_key_pair') + session.server.key_pair_name = self.context.config().get_string('cluster.network.ssh_key_pair', required=True) if Utils.is_empty(session.server.security_groups): session.server.security_groups = [] diff --git a/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/clients/dcvssmswaggerclient/configuration.py b/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/clients/dcvssmswaggerclient/configuration.py index 47f91b2..4e05ff1 100644 --- a/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/clients/dcvssmswaggerclient/configuration.py +++ b/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/clients/dcvssmswaggerclient/configuration.py @@ -127,7 +127,7 @@ def logger_file(self, value): if self.__logger_file: # If set logging file, # then add file handler and remove stream handler. - self.logger_file_handler = logging.FileHandler(self.__logger_file) + self.logger_file_handler = logging.FileHandler(self.__logger_file, encoding='utf-8') self.logger_file_handler.setFormatter(self.logger_formatter) for _, logger in six.iteritems(self.logger): logger.addHandler(self.logger_file_handler) diff --git a/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/virtual_desktop_controller_utils.py b/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/virtual_desktop_controller_utils.py index 7ad684d..35eebed 100644 --- a/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/virtual_desktop_controller_utils.py +++ b/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller/app/virtual_desktop_controller_utils.py @@ -9,6 +9,8 @@ # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # and limitations under the License. import os +import logging +import random from threading import RLock from typing import List, Dict, Optional @@ -117,11 +119,10 @@ def _build_userdata(self, session: VirtualDesktopSession): 'Setup-WindowsEC2Instance' ] - use_vpc_endpoints = self.context.config().get_bool('cluster.network.use_vpc_endpoints', default=False) https_proxy = self.context.config().get_string('cluster.network.https_proxy', required=False, default='') no_proxy = self.context.config().get_string('cluster.network.no_proxy', required=False, default='') proxy_config = {} - if use_vpc_endpoints and Utils.is_not_empty(https_proxy): + if Utils.is_not_empty(https_proxy): proxy_config = { 'http_proxy': https_proxy, 'https_proxy': https_proxy, @@ -172,50 +173,135 @@ def provision_dcv_host_for_session(self, session: VirtualDesktopSession) -> dict }) metadata_http_tokens = self.context.config().get_string('virtual-desktop-controller.dcv_session.metadata_http_tokens', required=True) - response = self.ec2_client.run_instances( - UserData=self._build_userdata(session), - ImageId=session.software_stack.ami_id, - InstanceType=session.server.instance_type, - TagSpecifications=[ - { - 'ResourceType': 'instance', - 'Tags': aws_tags - } - ], - MaxCount=1, - MinCount=1, - NetworkInterfaces=[ - { - 'DeviceIndex': 0, - 'AssociatePublicIpAddress': False, - 'SubnetId': session.server.subnet_id, - 'Groups': session.server.security_groups - } - ], - IamInstanceProfile={ - 'Arn': session.server.instance_profile_arn - }, - BlockDeviceMappings=[ - { - 'DeviceName': Utils.get_ec2_block_device_name(session.software_stack.base_os.value), - 'Ebs': { - 'DeleteOnTermination': True, - 'VolumeSize': session.server.root_volume_size.int_val(), - 'Encrypted': False if Utils.is_empty(session.hibernation_enabled) else session.hibernation_enabled, - 'VolumeType': 'gp3' - } - } - ], - KeyName=session.server.key_pair_name, - HibernationOptions={ - 'Configured': False if Utils.is_empty(session.hibernation_enabled) else session.hibernation_enabled - }, - MetadataOptions={ - 'HttpTokens': metadata_http_tokens, - 'HttpEndpoint': 'enabled' - } + + kms_key_id = self.context.config().get_string('cluster.ebs.kms_key_id', required=False, default=None) + if kms_key_id is None: + kms_key_id = 'alias/aws/ebs' + + # Handle subnet processing for eVDI hosts + + randomize_subnet_method = self.context.config().get_bool( + 'vdc.dcv_session.network.randomize_subnets', + default=Utils.get_as_bool(constants.DEFAULT_VDI_RANDOMIZE_SUBNETS, default=False) + ) + subnet_autoretry_method = self.context.config().get_bool( + 'vdc.dcv_session.network.subnet_autoretry', + default=Utils.get_as_bool(constants.DEFAULT_VDI_SUBNET_AUTORETRY, default=True) + ) + # Determine if we have a specific list of subnets configured for VDI + configured_vdi_subnets = self.context.config().get_list( + 'vdc.dcv_session.network.private_subnets', + default=[] ) - return Utils.to_dict(response) + # Required=True as these should always be available, and we want to error otherwise + cluster_private_subnets = self.context.config().get_list( + 'cluster.network.private_subnets', + required=True + ) + + _attempt_subnets = [] + # Use a subnet_id if specified + if Utils.is_not_empty(session.server.server_id): + # this comes in as a string from the API + self._logger.debug(f"Using strict requested subnet_id: {session.server.server_id}") + _attempt_subnets.append(session.server.server_id) + elif configured_vdi_subnets: + # A list from the config + self._logger.debug(f"Found configured VDI subnets: {', '.join(configured_vdi_subnets)}") + _attempt_subnets = configured_vdi_subnets + else: + # fallback to a list of cluster private_subnets + self._logger.debug(f"Fallback to cluster private_subnets: {', '.join(cluster_private_subnets)}") + _attempt_subnets = cluster_private_subnets + + # Shuffle the list if configured for random subnet + if randomize_subnet_method: + random.shuffle(_attempt_subnets) + self._logger.debug(f"Applying randomize to subnet list due to configuration.") + + # At this stage _attempt_subnets contains the subnets + # we want to attempt in the order that we prefer + # (ordered or pre-shuffled) + + self._logger.debug(f"Deployment Attempt Ready - Retry: {subnet_autoretry_method} Attempt_Subnets({len(_attempt_subnets)}): {', '.join(_attempt_subnets)}") + + _deployment_loop = 0 + _attempt_provision = True + + while _attempt_provision: + _deployment_loop += 1 + # We just .pop(0) since the list has been randomized already if it was requested + _subnet_to_try = _attempt_subnets.pop(0) + _remainig_subnet_count = len(_attempt_subnets) + + self._logger.info(f"Deployment attempt #{_deployment_loop} subnet_id: {_subnet_to_try} Remaining Subnets: {_remainig_subnet_count}") + if _remainig_subnet_count <= 0: + # this is our last attempt + self._logger.debug(f"Final deployment attempt (_remaining_subnet_count == 0)") + _attempt_provision = False + else: + _attempt_provision = True + + response = None + + try: + response = self.ec2_client.run_instances( + UserData=self._build_userdata(session), + ImageId=session.software_stack.ami_id, + InstanceType=session.server.instance_type, + TagSpecifications=[ + { + 'ResourceType': 'instance', + 'Tags': aws_tags + } + ], + MaxCount=1, + MinCount=1, + NetworkInterfaces=[ + { + 'DeviceIndex': 0, + 'AssociatePublicIpAddress': False, + 'SubnetId': _subnet_to_try, + 'Groups': session.server.security_groups + } + ], + IamInstanceProfile={ + 'Arn': session.server.instance_profile_arn + }, + BlockDeviceMappings=[ + { + 'DeviceName': Utils.get_ec2_block_device_name(session.software_stack.base_os.value), + 'Ebs': { + 'DeleteOnTermination': True, + 'VolumeSize': session.server.root_volume_size.int_val(), + 'Encrypted': Utils.get_as_bool(constants.DEFAULT_VOLUME_ENCRYPTION_VDI, default=True), + 'KmsKeyId': kms_key_id, + 'VolumeType': Utils.get_as_string(constants.DEFAULT_VOLUME_TYPE_VDI, default='gp3') + } + } + ], + KeyName=session.server.key_pair_name, + HibernationOptions={ + 'Configured': False if Utils.is_empty(session.hibernation_enabled) else session.hibernation_enabled + }, + MetadataOptions={ + 'HttpTokens': metadata_http_tokens, + 'HttpEndpoint': 'enabled' + } + ) + except Exception as err: + self._logger.warning(f"Encountered Deployment Exception: {err} / Response: {response}") + self._logger.debug(f"Remaining subnets {len(_attempt_subnets)}: {_attempt_subnets}") + if subnet_autoretry_method and _attempt_provision: + self._logger.debug(f"Continue with next attempt with remaining subnets..") + continue + else: + self._logger.warning(f"Exhausted all deployment attempts. Cannot continue with this request.") + break + + if response: + self._logger.debug(f"Returning response: {response}") + return Utils.to_dict(response) def _add_instance_data_to_cache(self): with self.instance_types_lock: @@ -296,6 +382,7 @@ def get_valid_instance_types(self, hibernation_support: bool, software_stack: Vi # We now have a list of all instance types (Cache has been updated IF it was empty). valid_instance_types = [] + valid_instance_types_names = [] allowed_instance_types = self.context.config().get_list('virtual-desktop-controller.dcv_session.instance_types.allow', default=[]) allowed_instance_type_names = set() @@ -315,32 +402,46 @@ def get_valid_instance_types(self, hibernation_support: bool, software_stack: Vi else: denied_instance_type_families.add(instance_type) + if self._logger.isEnabledFor(logging.DEBUG): + self._logger.debug(f"get_valid_instance_types() - Instance Allow/Deny Summary") + self._logger.debug(f"Allowed instance Families: {allowed_instance_type_families}") + self._logger.debug(f"Allowed instances: {allowed_instance_type_names}") + self._logger.debug(f"Denied instance Families: {denied_instance_type_families}") + self._logger.debug(f"Denied instances: {denied_instance_type_names}") + for instance_type_name in instance_types_names: instance_type_family = instance_type_name.split('.')[0] + self._logger.debug(f"Processing - Instance Name: {instance_type_name} Family: {instance_type_family}") if instance_type_name not in allowed_instance_type_names and instance_type_family not in allowed_instance_type_families: # instance type or instance family is not present in allow list + self._logger.debug(f"Found {instance_type_name} ({instance_type_family}) NOT in ALLOW config: ({allowed_instance_type_names} / {allowed_instance_type_families})") continue if instance_type_name in denied_instance_type_names or instance_type_family in denied_instance_type_families: # instance type or instance family is present in deny list + self._logger.debug(f"Found {instance_type_name} ({instance_type_family}) IN DENIED config: ({denied_instance_type_names} / {denied_instance_type_families})") continue instance_info = instance_info_data[instance_type_name] if Utils.is_not_empty(software_stack) and software_stack.min_ram > self.get_instance_ram(instance_type_name): # this instance doesn't have the minimum ram required to support the software stack. + self._logger.debug(f"Software stack ({software_stack}) restrictions on RAM ({software_stack.min_ram}): Instance {instance_type_name} lacks enough ({self.get_instance_ram(instance_type_name)}). Skipped.") continue - hibernation_supported = Utils.get_value_as_bool('HibernationSupported', instance_info, False) + hibernation_supported = Utils.get_value_as_bool('HibernationSupported', instance_info, default=False) if hibernation_support and not hibernation_supported: + self._logger.debug(f"Hibernation ({hibernation_support}) != Instance {instance_type_name} ({hibernation_supported}) Skipped.") continue supported_archs = Utils.get_value_as_list('SupportedArchitectures', Utils.get_value_as_dict('ProcessorInfo', instance_info, {}), []) if Utils.is_not_empty(software_stack) and software_stack.architecture.value not in supported_archs: # not desired architecture + self._logger.debug(f"Software Stack arch ({software_stack.architecture.value}) != Instance {instance_type_name} ({supported_archs}) Skipped.") continue supported_gpus = Utils.get_value_as_list('Gpus', Utils.get_value_as_dict('GpuInfo', instance_info, {}), []) + self._logger.debug(f"Instance {instance_type_name} GPU ({supported_gpus})") perform_gpu_check = False gpu_to_check_against = None if Utils.is_not_empty(gpu): @@ -361,6 +462,7 @@ def get_valid_instance_types(self, hibernation_support: bool, software_stack: Vi # we don't need GPU if len(supported_gpus) > 0: # this instance SHOULD NOT have GPU support, but it does. + self._logger.debug(f"Instance {instance_type_name} Should not have GPU ({supported_gpus}) but it does.") continue else: # we need GPU @@ -372,9 +474,14 @@ def get_valid_instance_types(self, hibernation_support: bool, software_stack: Vi if not gpu_found: # we needed a GPU, but we didn't find any + self._logger.debug(f"Instance {instance_type_name} - Needed a GPU but didn't find one.") continue + # All checks passed if we make it this far + self._logger.debug(f"Instance {instance_type_name} - Added as valid_instance_types") + valid_instance_types_names.append(instance_type_name) valid_instance_types.append(instance_info) + self._logger.debug(f"Returning valid_instance_types: {valid_instance_types_names}") return valid_instance_types def describe_image_id(self, ami_id: str) -> dict: diff --git a/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller_meta/__init__.py b/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller_meta/__init__.py index 9d52386..959cd0f 100644 --- a/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller_meta/__init__.py +++ b/source/idea/idea-virtual-desktop-controller/src/ideavirtualdesktopcontroller_meta/__init__.py @@ -10,4 +10,4 @@ # and limitations under the License. __name__ = 'idea-virtual-desktop-controller' -__version__ = '3.1.1' +__version__ = '3.1.2' diff --git a/tasks/.release.py.swp b/tasks/.release.py.swp new file mode 100644 index 0000000..55d3428 Binary files /dev/null and b/tasks/.release.py.swp differ diff --git a/tasks/release.py b/tasks/release.py index 902fee0..fafb39e 100644 --- a/tasks/release.py +++ b/tasks/release.py @@ -111,7 +111,6 @@ def ignore_callback(src: str, names: List[str]) -> List[str]: # copy all source artifacts to build/open-source targets = [ - 'deployment', 'tasks', 'source', 'requirements', diff --git a/tasks/tools/clean_tool.py b/tasks/tools/clean_tool.py index 4dcc145..6948bf2 100644 --- a/tasks/tools/clean_tool.py +++ b/tasks/tools/clean_tool.py @@ -45,6 +45,15 @@ def clean(self): def clean_non_project_items(): idea.console.print_header_block(f'clean non-project items') + # lambda assets clean-up (all directories that start with 'idea_') + if os.path.isdir(idea.props.project_build_dir): + for file in os.listdir(idea.props.project_build_dir): + if file.startswith('idea_'): + function_build_dir = os.path.join(idea.props.project_build_dir, file) + if os.path.isdir(function_build_dir): + idea.console.print(f'deleting {function_build_dir} ...') + shutil.rmtree(function_build_dir) + # open source clean-up opensource_build_dir = os.path.join(idea.props.project_build_dir, 'open-source') if os.path.isdir(opensource_build_dir):