tasks/tools/infra_ami_package_tool.py (125 lines of code) (raw):

# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # with the License. A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # # 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 tasks.idea as idea from ideasdk.utils import Jinja2Utils from invoke import Context import shutil import os import re import yaml from typing import List, Dict class InfraAmiPackageTool: def __init__(self, c: Context): self.c = c @property def bash_script_name(self) -> str: return 'install.sh' @property def output_archive_basename(self) -> str: return 'res-infra-dependencies' @property def requirements_file_name(self) -> str: return 'requirements.txt' @property def output_archive_name(self) -> str: return f'{self.output_archive_basename}.tar.gz' @property def output_dir(self) -> str: return os.path.join(idea.props.project_dist_dir, self.output_archive_basename) @property def common_dir(self) -> str: return os.path.join(self.output_dir, 'common') @property def all_depencencies_dir(self) -> str: return os.path.join(self.output_dir, 'all_dependencies') @property def package_names(self) -> List[str]: return [ "idea-cluster-manager", "idea-virtual-desktop-controller", "idea-sdk" ] def get_global_settings(self) -> Dict: idea.console.print('getting global settings ...') env = Jinja2Utils.env_using_file_system_loader(search_path=idea.props.global_settings_dir) template = env.get_template('settings.yml') global_settings = template.render(enabled_modules=['virtual-desktop-controller'], supported_base_os=['amazonlinux2', 'rhel8', 'rhel9']) return yaml.full_load(global_settings) def get_all_requirement_files(self) -> List[str]: idea.console.print('getting all requirements file ...') requirement_files = [] for package_name in self.package_names: requirements_file = os.path.join(idea.props.requirements_dir, f'{package_name}.txt') requirement_files.append(requirements_file) if not os.path.isfile(requirements_file): raise idea.exceptions.build_failed(f'project requirements file not found: {requirements_file}') return requirement_files def build_infra_python_requirements(self) -> None: idea.console.print('building infra python requirements ...') requirement_files = self.get_all_requirement_files() packages = dict() package_list = [] for requirement_file in requirement_files: with open(requirement_file, 'r') as f: for line in f: package = line.strip() if re.match('^\w', package): package_name, package_version = package.split('==') if package_name not in packages: packages[package_name] = package_version package_list.append(package) with open(os.path.join(self.all_depencencies_dir, self.requirements_file_name), 'w') as f: for package in package_list: f.write(package + '\n') def copy_common_infra_requirements(self) -> None: idea.console.print('copying comming script ...') common_bootstrap_dir = os.path.join(idea.props.bootstrap_dir, 'common') scripts = os.listdir(common_bootstrap_dir) for script in scripts: if script.endswith('.sh'): shutil.copy(os.path.join(common_bootstrap_dir, script), self.common_dir) def archive(self) -> None: idea.console.print('creating archive ...') shutil.make_archive(self.output_dir, 'gztar', self.output_dir) def build_bash_script(self) -> str: idea.console.print('building dependency installation bash script ...') global_settings = self.get_global_settings() bash_content = """#!/bin/bash # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # with the License. A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # # 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. # #Set up environment variables set -ex if [[ -f /etc/os-release ]]; then OS_RELEASE_ID=$(grep -E '^(ID)=' /etc/os-release | awk -F'"' '{print $2}') OS_RELEASE_VERSION_ID=$(grep -E '^(VERSION_ID)=' /etc/os-release | awk -F'"' '{print $2}') RES_BASE_OS=$(echo $OS_RELEASE_ID${OS_RELEASE_VERSION_ID%%.*}) elif [[ -f /usr/lib/os-release ]]; then OS_RELEASE_ID=$(grep -E '^(ID)=' /usr/lib/os-release | awk -F'"' '{print $2}') OS_RELEASE_VERSION_ID=$(grep -E '^(VERSION_ID)=' /usr/lib/os-release | awk -F'"' '{print $2}') RES_BASE_OS=$(echo $OS_RELEASE_ID${OS_RELEASE_VERSION_ID%%.*}) else echo "Base OS information on Linux instance cannot be found." exit 1 fi BOOTSTRAP_DIR=/root/bootstrap LOGS_DIR=$BOOTSTRAP_DIR/logs LOG_FILE=$LOGS_DIR/userdata.log SCRIPT_DIR=$(pwd) timestamp=$(date +%s) #Create required directories mkdir -p $LOGS_DIR #Create log file if [[ -f $LOG_FILE ]]; then mv $LOG_FILE "${LOG_FILE}.${timestamp}" fi exec > $LOG_FILE 2>&1 cd $BOOTSTRAP_DIR export PATH=$PATH:/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/opt/idea/python/latest/bin machine=$(uname -m) ###Common installs #AWS CLI AWS=$(command -v aws) if [[ `$AWS --version | awk -F'[/.]' '{print $2}'` != 2 ]]; then curl -s "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" which unzip > /dev/null 2>&1 if [[ "$?" != "0" ]]; then yum install -y unzip fi unzip -q awscliv2.zip ./aws/install --bin-dir /bin --update rm -rf aws awscliv2.zip fi if [[ $RES_BASE_OS =~ ^(amzn2)$ ]]; then #Amazon linux extras sudo yum install -y amazon-linux-extras fi #AWS SSM Agent: """ bash_content += f""" systemctl status amazon-ssm-agent if [[ "$?" != "0" ]]; then yum install -y "{global_settings['package_config']['aws_ssm']['x86_64']}" fi """ bash_content += """ #Jq yum install -y jq #EPEL Repo /bin/bash "${SCRIPT_DIR}/../common/epel_repo.sh" -o $RES_BASE_OS -s "${SCRIPT_DIR}" #System Packages """ linux_packages = global_settings['package_config']['linux_packages'] all_linux_packages = " ".join(linux_packages['application'] + linux_packages['system'] + linux_packages['openldap_client'] + linux_packages['openldap_server'] + linux_packages['sssd'] + linux_packages['putty'] ) bash_content += f""" ALL_PACKAGES=({all_linux_packages}) """ bash_content += """ yum install -y ${ALL_PACKAGES[*]} --skip-broken if [[ $RES_BASE_OS =~ ^(rhel8|rhel9)$ ]]; then dnf install -y openssh # Install Open LDAP Client. # RHEL 8 and 9 does not support OpenLDAP by default. # As a workaround, install it from the Symas OpenLDAP Repository wget -q https://repo.symas.com/configs/SOLDAP/${RES_BASE_OS}/release26.repo -O /etc/yum.repos.d/soldap-release26.repo dnf install -y symas-openldap-clients fi """ bash_content += """ #efs utils if [[ $RES_BASE_OS =~ ^(amzn2)$ ]]; then yum install -y amazon-efs-utils elif [[ $RES_BASE_OS =~ ^(rhel8|rhel9)$ ]]; then if [[ $RES_BASE_OS =~ ^(rhel8)$ ]]; then yum module install -y rust-toolset elif [[ $RES_BASE_OS =~ ^(rhel9)$ ]]; then dnf install -y rust-toolset fi sudo rm -f -r ./efs-utils git clone https://github.com/aws/efs-utils cd efs-utils make rpm yum -y install build/amazon-efs-utils*rpm cd .. fi """ bash_content += """ #CloudWatch Agent yum install -y amazon-cloudwatch-agent #NFS Utils and dependency items /bin/bash "${SCRIPT_DIR}/../common/nfs_utils.sh" -o $RES_BASE_OS -s "${SCRIPT_DIR}" #jq /bin/bash "${SCRIPT_DIR}/../common/jq.sh" -o $RES_BASE_OS -s "${SCRIPT_DIR}" """ bash_content += f""" #Python PYTHON_VERSION="{global_settings['package_config']['python']['version']}" PYTHON_URL="{global_settings['package_config']['python']['url']}" """ bash_content += """ PYTHON_TGZ=$(basename ${PYTHON_URL}) function install_python () { # - ALIAS_PREFIX: Will generate symlinks for python3 and pip3 for the alias: # eg. if ALIAS_PREFIX == 'idea', idea_python and idea_pip will be available for use. # - INSTALL_DIR: the location where python will be installed. ALIAS_PREFIX="idea" INSTALL_DIR="/opt/idea/python" PYTHON3_BIN="${INSTALL_DIR}/latest/bin/python3" CURRENT_VERSION="$(${PYTHON3_BIN} --version | awk {'print $NF'})" if [[ "${CURRENT_VERSION}" == "${PYTHON_VERSION}" ]]; then echo "Python already installed and at correct version." else echo "Python not detected, installing" TIMESTAMP=$(date +%s) TMP_DIR="/root/bootstrap/python_installer/${ALIAS_PREFIX}-${TIMESTAMP}" mkdir -p "${TMP_DIR}" pushd ${TMP_DIR} wget ${PYTHON_URL} tar xvf ${PYTHON_TGZ} pushd "Python-${PYTHON_VERSION}" PYTHON_DIR="${INSTALL_DIR}/${PYTHON_VERSION}" ./configure LDFLAGS="-L/usr/lib64/openssl" \\ CPPFLAGS="-I/usr/include/openssl" \\ -enable-loadable-sqlite-extensions \\ --prefix="${PYTHON_DIR}" NUM_PROCS=`nproc --all` MAKE_FLAGS="-j${NUM_PROCS}" make ${MAKE_FLAGS} make ${MAKE_FLAGS} install popd popd # create symlinks PYTHON_LATEST="${INSTALL_DIR}/latest" ln -sf "${PYTHON_DIR}" "${PYTHON_LATEST}" ln -sf "${PYTHON_LATEST}/bin/python3" "${PYTHON_LATEST}/bin/${ALIAS_PREFIX}_python" ln -sf "${PYTHON_LATEST}/bin/pip3" "${PYTHON_LATEST}/bin/${ALIAS_PREFIX}_pip" pip_command="${ALIAS_PREFIX}_pip" """ bash_content += f""" requirements_path="${{SCRIPT_DIR}}/{self.requirements_file_name}" """ bash_content += """ export PATH=$PATH:/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/opt/idea/python/latest/bin $pip_command install -r $requirements_path fi } install_python """ bash_content += f""" #DCV rpm --import {global_settings['package_config']['dcv']['gpg_key']} """ bash_content += f""" #DCV server if [[ $RES_BASE_OS == "amzn2" ]]; then DCV_SERVER_URL="{global_settings['package_config']['dcv']['host']['x86_64']['linux']['al2']['url']}" DCV_SERVER_SHA256_URL="{global_settings['package_config']['dcv']['host']['x86_64']['linux']['al2']['sha256sum']}" elif [[ $RES_BASE_OS == "rhel8" ]]; then DCV_SERVER_URL="{global_settings['package_config']['dcv']['host']['x86_64']['linux']['rhel_centos_rocky8']['url']}" DCV_SERVER_SHA256_URL="{global_settings['package_config']['dcv']['host']['x86_64']['linux']['rhel_centos_rocky8']['sha256sum']}" elif [[ $RES_BASE_OS == "rhel9" ]]; then DCV_SERVER_URL="{global_settings['package_config']['dcv']['host']['x86_64']['linux']['rhel_centos_rocky9']['url']}" DCV_SERVER_SHA256_URL="{global_settings['package_config']['dcv']['host']['x86_64']['linux']['rhel_centos_rocky9']['sha256sum']}" else echo "Base OS $RES_BASE_OS is not supported." exit 1 fi """ bash_content += """ wget $DCV_SERVER_URL DCV_SERVER_TGZ=$(basename $DCV_SERVER_URL) urlSha256Sum=$(wget -O - ${DCV_SERVER_SHA256_URL}) if [[ $(sha256sum ${DCV_SERVER_TGZ} | awk '{print $1}') != ${urlSha256Sum} ]]; then echo -e "FATAL ERROR: Checksum for DCV Server failed. File may be compromised." > /etc/motd exit 1 fi extractDir=$(echo ${DCV_SERVER_TGZ} | sed 's/\.tgz$//') mkdir -p ${extractDir} tar zxvf ${DCV_SERVER_TGZ} -C ${extractDir} --strip-components 1 pushd ${extractDir} rpm -ivh nice-dcv-web-viewer-*.${machine}.rpm popd rm -rf ${extractDir} rm -f $DCV_SERVER_TGZ || true """ bash_content += f""" #Gateway if [[ $RES_BASE_OS == "amzn2" ]]; then GATEWAY_URL="{global_settings['package_config']['dcv']['connection_gateway']['x86_64']['linux']['al2']['url']}" GATEWAY_SHA256_URL="{global_settings['package_config']['dcv']['connection_gateway']['x86_64']['linux']['al2']['sha256sum']}" elif [[ $RES_BASE_OS == "rhel8" ]]; then GATEWAY_URL="{global_settings['package_config']['dcv']['connection_gateway']['x86_64']['linux']['rhel_centos_rocky8']['url']}" GATEWAY_SHA256_URL="{global_settings['package_config']['dcv']['connection_gateway']['x86_64']['linux']['rhel_centos_rocky8']['sha256sum']}" elif [[ $RES_BASE_OS == "rhel9" ]]; then GATEWAY_URL="{global_settings['package_config']['dcv']['connection_gateway']['x86_64']['linux']['rhel_centos_rocky9']['url']}" GATEWAY_SHA256_URL="{global_settings['package_config']['dcv']['connection_gateway']['x86_64']['linux']['rhel_centos_rocky9']['sha256sum']}" else echo "Base OS $RES_BASE_OS is not supported." exit 1 fi """ bash_content += """ wget $GATEWAY_URL GATEWAY_FILE_NAME=$(basename $GATEWAY_URL) urlSha256Sum=$(wget -O - $GATEWAY_SHA256_URL) if [[ $(sha256sum $GATEWAY_FILE_NAME | awk '{print $1}') != ${urlSha256Sum} ]]; then echo -e "FATAL ERROR: Checksum for DCV Connection Gateway failed. File may be compromised." > /etc/motd exit 1 fi yum install -y $GATEWAY_FILE_NAME RM_RPM=$(echo $GATEWAY_FILE_NAME | sed 's/\.rpm/*rpm*/') rm -f $RM_RPM || true #Gateway - netcat yum install -y nc """ bash_content += f""" #Broker if [[ $RES_BASE_OS == "amzn2" ]]; then BROKER_URL="{global_settings['package_config']['dcv']['broker']['linux']['al2']['url']}" BROKER_SHA256_URL="{global_settings['package_config']['dcv']['broker']['linux']['al2']['sha256sum']}" elif [[ $RES_BASE_OS == "rhel8" ]]; then BROKER_URL="{global_settings['package_config']['dcv']['broker']['linux']['rhel_centos_rocky8']['url']}" BROKER_SHA256_URL="{global_settings['package_config']['dcv']['broker']['linux']['rhel_centos_rocky8']['sha256sum']}" elif [[ $RES_BASE_OS == "rhel9" ]]; then BROKER_URL="{global_settings['package_config']['dcv']['broker']['linux']['rhel_centos_rocky9']['url']}" BROKER_SHA256_URL="{global_settings['package_config']['dcv']['broker']['linux']['rhel_centos_rocky9']['sha256sum']}" else echo "Base OS $RES_BASE_OS is not supported." exit 1 fi """ bash_content += """ wget $BROKER_URL BROKER_FILE_NAME=$(basename $BROKER_URL) urlSha256Sum=$(wget -O - ${BROKER_SHA256_URL}) if [[ $(sha256sum ${BROKER_FILE_NAME} | awk '{print $1}') != ${urlSha256Sum} ]]; then echo -e "FATAL ERROR: Checksum for DCV Broker failed. File may be compromised." > /etc/motd exit 1 fi yum install -y $BROKER_FILE_NAME RM_RPM=$(echo $BROKER_FILE_NAME | sed 's/\.rpm/*rpm*/') rm -f $RM_RPM || true """ return bash_content def create_all_dependencies_script(self) -> None: bash_content = self.build_bash_script() with open(os.path.join(self.all_depencencies_dir, self.bash_script_name), 'w') as f: f.write(bash_content) def package(self): idea.console.print_header_block(f'package infra-ami-deps') shutil.rmtree(self.output_dir, ignore_errors=True) os.makedirs(self.output_dir, exist_ok=True) os.makedirs(self.common_dir, exist_ok=True) os.makedirs(self.all_depencencies_dir, exist_ok=True) self.build_infra_python_requirements() self.copy_common_infra_requirements() self.create_all_dependencies_script() self.archive()