From 832b3bf81a24937dbc1307629787d160e58de57f Mon Sep 17 00:00:00 2001 From: Greg Ochmanski Date: Thu, 31 Oct 2024 14:25:32 +0100 Subject: [PATCH 1/8] fix(terraform): fix main module to handle existing security groups feat(core): add Unicode support The main Terraform module has been updated to allow adding existing security groups without creating a default one. This fixes an issue where the module was creating a new security group even when one was already provided. Additionally, the core module has been enhanced to provide Unicode support, enabling expanded character set handling. Other minor fixes and cleanup. --- assets/packer/perforce/helix-core/p4_configure.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/packer/perforce/helix-core/p4_configure.sh b/assets/packer/perforce/helix-core/p4_configure.sh index e307dedc..7af6311b 100644 --- a/assets/packer/perforce/helix-core/p4_configure.sh +++ b/assets/packer/perforce/helix-core/p4_configure.sh @@ -126,10 +126,10 @@ prepare_site_tags() { set_unicode() { log_message "Setting unicode flag for p4d." log_message "sourcing p4_vars" - + # Capture the command output output=$(su - perforce -c "source /p4/common/bin/p4_vars && /p4/common/bin/p4d -xi" 2>&1) - + # Check if the output matches exactly what we expect if [ "$output" = "Server switched to Unicode mode." ]; then log_message "Successfully switched server to Unicode mode" From a290cd9ed878a36d5a174df7a722880a82890c7b Mon Sep 17 00:00:00 2001 From: Greg Ochmanski Date: Mon, 4 Nov 2024 19:38:41 +0100 Subject: [PATCH 2/8] feat(ansible): implement Helix Core setup playbook - Replace p4_configure.sh and p4_setup.sh with Ansible playbook - Eliminate need for Packer-built AMI and automates Amazon Linux 2023 - Add tasks for downloading and configuring Helix binaries - Implement platform-specific binary selection - Ensure correct placement of binaries in /hxdepots/sdp/helix_binaries - Add error handling and retries for binary downloads - Improve automation and consistency in Perforce Helix Core setup This playbook automates the Perforce Helix Core installation and configuration process, providing a more robust and maintainable solution compared to the previous shell scripts. It dynamically selects the appropriate binaries based on the target system's architecture, enhancing cross-platform compatibility. --- .../helix-core/p4_configure_playbook.yml | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml diff --git a/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml b/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml new file mode 100644 index 00000000..fd53434f --- /dev/null +++ b/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml @@ -0,0 +1,175 @@ +--- +- name: Install and Configure Perforce + hosts: all + become: yes + vars: + log_file: "/var/log/p4_setup.log" + sdp_root: "/hxdepots/sdp/helix_binaries" + sdp: "/hxdepots/sdp" + package: "policycoreutils-python-utils" + required_binaries: + - p4 + - p4broker + - p4d + - p4p + default_helix_version: "r23.1" + default_bin_list: "p4,p4d,p4p,p4broker" + perforce_ftp_base_url: "https://ftp.perforce.com/perforce" + stage_bin_dir: "/hxdepots/sdp/helix_binaries" + download_apis: false + + tasks: + - name: Ensure running as root + fail: + msg: "This playbook must be run as root" + when: ansible_user_id != "root" + + - name: Ensure perforce group exists + group: + name: perforce + state: present + + - name: Ensure perforce user exists + user: + name: perforce + group: perforce + home: /home/perforce + shell: /bin/bash + create_home: yes + + - name: Set up sudoers for perforce user + copy: + content: "perforce ALL=(ALL) NOPASSWD:ALL" + dest: /etc/sudoers.d/perforce + mode: '0400' + + - name: Create required directories + file: + path: "{{ item }}" + state: directory + owner: perforce + group: perforce + mode: '0755' + loop: + - /hxdepots + - /hxlogs + - /hxmetadata + + - name: Download SDP + get_url: + url: https://swarm.workshop.perforce.com/download/guest/perforce_software/sdp/downloads/sdp.Unix.tgz + dest: /hxdepots/sdp.Unix.tgz + + - name: Extract SDP + unarchive: + src: /hxdepots/sdp.Unix.tgz + dest: /hxdepots + remote_src: yes + + - name: Set permissions for SDP + file: + path: "{{ sdp }}" + state: directory + recurse: yes + mode: 'u+w' + + - name: Check for required binaries + stat: + path: "{{ sdp_root }}/{{ item }}" + register: binary_stats + loop: "{{ required_binaries }}" + + - name: Ensure the target directory exists + file: + path: "{{ stage_bin_dir }}" + state: directory + mode: '0755' + + - name: Set Helix version + set_fact: + helix_version: "{{ helix_version | default(default_helix_version) }}" + + - name: Set binary list + set_fact: + bin_list: "{{ bin_list | default(default_bin_list) }}" + + - name: Determine OS and architecture + set_fact: + os_name: "{{ ansible_system | lower }}" + os_arch: "{{ ansible_architecture }}" + + - name: Set platform for each binary + set_fact: + platform: >- + {%- if os_name == 'linux' -%} + linux26{{ 'x86_64' if os_arch == 'x86_64' else '' }} + {%- else -%} + {{ os_name }}{{ '64' if os_arch == 'x86_64' else '' }} + {%- endif -%} + + - name: Download binaries + get_url: + url: "{{ perforce_ftp_base_url }}/{{ helix_version }}/bin.{{ platform | trim }}/{{ item }}" + dest: "{{ stage_bin_dir }}/{{ item }}" + mode: '0755' + loop: "{{ bin_list.split(',') }}" + register: download_result + retries: 3 + delay: 5 + until: download_result is success + + + - name: Get binary versions + command: "{{ stage_bin_dir }}/{{ item }} -V" + register: version_info + changed_when: false + loop: "{{ bin_list.split(',') }}" + + - name: Display binary versions + debug: + msg: "Version of {{ item.item }}: {{ item.stdout_lines[0] }}" + loop: "{{ version_info.results }}" + + - name: Download APIs + block: + - name: Get API directory listing + uri: + url: "{{ perforce_ftp_base_url }}/{{ helix_version }}/bin.{{ platform }}/" + return_content: yes + register: api_dir_content + + - name: Extract API filenames + set_fact: + api_files: "{{ api_dir_content.content | regex_findall('href=\"(p4api-.*?.tgz)\"') }}" + + - name: Download API files + get_url: + url: "{{ perforce_ftp_base_url }}/{{ helix_version }}/bin.{{ platform }}/{{ item }}" + dest: "{{ stage_bin_dir }}/{{ item }}" + loop: "{{ api_files }}" + + - name: Display API versions + command: tar -tzf {{ stage_bin_dir }}/{{ item }} | head -1 + register: api_versions + changed_when: false + loop: "{{ api_files }}" + + - name: Show API versions + debug: + msg: "Version of {{ item.item }}: {{ item.stdout.split('/')[0] }}" + loop: "{{ api_versions.results }}" + when: download_apis | bool + + - name: Set ownership of SDP_Root + file: + path: "{{ sdp_root }}" + state: directory + recurse: yes + owner: perforce + group: perforce + + - name: Log completion + lineinfile: + path: "{{ log_file }}" + line: "{{ ansible_date_time.iso8601 }} - Perforce installation and configuration completed" + create: yes From 7665ea4d91419d16e9a14dea7c7e23e6defae7dc Mon Sep 17 00:00:00 2001 From: Greg Ochmanski Date: Thu, 7 Nov 2024 18:03:59 +0100 Subject: [PATCH 3/8] feat(perforce): add SSM document for Helix Core configuration - Create new SSM module for applying Ansible playbooks - Update p4_configure_playbook.yml with improved installation steps - Modify p4_configure.sh remove unecessary selinux commands This commit introduces an SSM document to streamline the deployment of Perforce Helix Core on EC2 instances using Ansible playbooks. The configuration process is now more automated and consistent across deployments. --- .../helix-core/p4_configure_playbook.yml | 2 + .../perforce/helix-core/ssm_module.tf | 143 ++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 assets/ansible-playbooks/perforce/helix-core/ssm_module.tf diff --git a/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml b/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml index fd53434f..e2f1771e 100644 --- a/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml +++ b/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml @@ -173,3 +173,5 @@ path: "{{ log_file }}" line: "{{ ansible_date_time.iso8601 }} - Perforce installation and configuration completed" create: yes + + diff --git a/assets/ansible-playbooks/perforce/helix-core/ssm_module.tf b/assets/ansible-playbooks/perforce/helix-core/ssm_module.tf new file mode 100644 index 00000000..730b3760 --- /dev/null +++ b/assets/ansible-playbooks/perforce/helix-core/ssm_module.tf @@ -0,0 +1,143 @@ +variable "s3_bucket_name" { + description = "S3 bucket where playbook is stored" + type = string + + +} + +variable "playbook_file_name" { + description = "The name of the playbook yaml on s3" + type = string +} + +resource "aws_ssm_document" "ansible_playbook" { + name = "Toolkit-AnsiblePlaybook" + document_type = "Command" + target_type = "/AWS::EC2::Instance" + content = jsonencode({ + schemaVersion = "2.2" + description = "Use this document to run Ansible Playbooks on Systems Manager managed instances. For more information, see https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-state-manager-ansible.html." + parameters = { + SourceType = { + description = "(Optional) Specify the source type." + type = "String" + allowedValues = ["GitHub", "S3"] + } + SourceInfo = { + description = "(Optional) Specify the information required to access the resource from the specified source type. If source type is GitHub, then you can specify any of the following: 'owner', 'repository', 'path', 'getOptions', 'tokenInfo'. Example GitHub parameters: {\"owner\":\"awslabs\",\"repository\":\"amazon-ssm\",\"path\":\"Compliance/InSpec/PortCheck\",\"getOptions\":\"branch:master\"}. If source type is S3, then you can specify 'path'. Important: If you specify S3, then the IAM instance profile on your managed instances must be configured with read access to Amazon S3." + type = "StringMap" + displayType = "textarea" + default = {} + } + InstallDependencies = { + type = "String" + description = "(Required) If set to True, Systems Manager installs Ansible and its dependencies, including Python, from the PyPI repo. If set to False, then verify that Ansible and its dependencies are installed on the target instances. If they aren't, the SSM document fails to run." + allowedValues = ["True", "False"] + default = "True" + } + PlaybookFile = { + type = "String" + description = "(Optional) The Playbook file to run (including relative path). If the main Playbook file is located in the ./automation directory, then specify automation/playbook.yml." + default = "hello-world-playbook.yml" + allowedPattern = "[(a-z_A-Z0-9\\-\\.)/]+(.yml|.yaml)$" + } + ExtraVariables = { + type = "String" + description = "(Optional) Additional variables to pass to Ansible at runtime. Enter key/value pairs separated by a space. For example: color=red flavor=cherry" + default = "SSM=True" + displayType = "textarea" + allowedPattern = "^$|^\\w+\\=(([^\\s|:();&]+)|('[^|:();&]+'))(\\s+\\w+\\=(([^\\s|:();&]+)|('[^|:();&]+')))*$" + } + Check = { + type = "String" + description = "(Optional) Use this parameter to run a check of the Ansible execution. The system doesn't make any changes to your systems. Instead, any module that supports check mode reports the changes it would make rather than making them. Modules that don't support check mode take no action and don't report changes that would be made." + allowedValues = ["True", "False"] + default = "False" + } + Verbose = { + type = "String" + description = "(Optional) Set the verbosity level for logging Playbook executions. Specify -v for low verbosity, -vv or –vvv for medium verbosity, and -vvvv for debug level." + allowedValues = ["-v", "-vv", "-vvv", "-vvvv"] + default = "-v" + } + TimeoutSeconds = { + type = "String" + description = "(Optional) The time in seconds for a command to be completed before it is considered to have failed." + default = "3600" + } + } + mainSteps = [ + { + action = "aws:downloadContent" + name = "downloadContent" + inputs = { + SourceType = "{{ SourceType }}" + SourceInfo = "{{ SourceInfo }}" + } + }, + { + action = "aws:runShellScript" + name = "runShellScript" + inputs = { + timeoutSeconds = "{{ TimeoutSeconds }}" + runCommand = [ + "#!/bin/bash", + "if [[ \"{{InstallDependencies}}\" == True ]] ; then", + " echo \"Installing and/or updating required tools: Ansible, wget, unzip ...\" >&2", + " if [ -f \"/etc/system-release\" ] ; then", + " if grep -q 'Amazon Linux release 2023' /etc/system-release ; then", + " sudo dnf install -y ansible wget unzip", + " elif grep -q 'Amazon Linux release 2' /etc/system-release ; then", + " sudo yum install -y ansible wget unzip", + " elif grep -q 'Red Hat Enterprise Linux' /etc/system-release ; then", + " sudo dnf install -y ansible wget unzip", + " else", + " echo \"Unsupported Amazon Linux or RHEL version. Please install Ansible, wget, and unzip manually.\" >&2", + " exit 1", + " fi", + " elif grep -qi ubuntu /etc/issue ; then", + " sudo apt-get update", + " sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ansible wget unzip", + " else", + " echo \"Unsupported operating system. Please install Ansible, wget, and unzip manually.\" >&2", + " exit 1", + " fi", + "fi", + "echo \"Running Ansible in $(pwd)\"", + "for zip in $(find -iname '*.zip'); do", + " unzip -o $zip", + "done", + "PlaybookFile=\"{{PlaybookFile}}\"", + "if [ ! -f \"$${PlaybookFile}\" ] ; then", + " echo \"The specified Playbook file doesn't exist in the downloaded bundle. Please review the relative path and file name.\" >&2", + " exit 2", + "fi", + "PYTHON_INTERPRETER=$(which python3)", + "if [[ \"{{Check}}\" == True ]] ; then", + " ansible-playbook -i \"localhost,\" --check -c local -e \"ansible_python_interpreter=$${PYTHON_INTERPRETER}\" -e \"{{ExtraVariables}}\" \"{{Verbose}}\" \"$${PlaybookFile}\"", + "else", + " ansible-playbook -i \"localhost,\" -c local -e \"ansible_python_interpreter=$${PYTHON_INTERPRETER}\" -e \"{{ExtraVariables}}\" \"{{Verbose}}\" \"$${PlaybookFile}\"", + "fi" + ] + } + } + ] + }) +} + +resource "aws_ssm_association" "toolkitdoc" { + name = aws_ssm_document.ansible_playbook.name + association_name = "Toolkit-AnsibleAssociation" + + targets { + key = "tag:Project" + values = ["ToolkitTest"] + } + parameters = { + SourceType = "S3" + SourceInfo = jsonencode({ + "path" = "https://s3.amazonaws.com/${var.s3_bucket_name}/${var.playbook_file_name}" + }) + PlaybookFile = "${var.playbook_file_name}" + } +} From 346e286e86092f978c37964c9f3ccfed21256091 Mon Sep 17 00:00:00 2001 From: Greg Ochmanski Date: Wed, 27 Nov 2024 11:37:00 +0100 Subject: [PATCH 4/8] feat(perforce): add dynamic infrastructure provisioning - Implement support for multiple server types (commit, replica, edge) - Add server_configuration variable for defining server instances - Introduce tagging system for Ansible playbook configuration - Update IAM, security, and DNS configurations - Add SSM integration for server management - Refactor main Terraform files for improved modularity BREAKING CHANGE: The module now requires a server_configuration input in Terraform to define Perforce server instances and their respective types and network environment. --- .../perforce/helix-core/SSM-Run-AMZL2023.json | 123 +++++++++++++++ .../perforce/helix-core/ssm.tf | 143 ++++++++++++++++++ modules/perforce/examples/complete/dns.tf | 12 +- modules/perforce/examples/complete/main.tf | 27 +++- .../perforce/examples/complete/security.tf | 19 ++- .../perforce/examples/complete/variables.tf | 4 + .../perforce/examples/complete/versions.tf | 2 +- .../helix-authentication-service/iam.tf | 21 ++- modules/perforce/helix-core/data.tf | 10 +- modules/perforce/helix-core/iam.tf | 14 +- modules/perforce/helix-core/locals.tf | 20 ++- modules/perforce/helix-core/main.tf | 97 ++++++++---- modules/perforce/helix-core/outputs.tf | 46 ++++-- modules/perforce/helix-core/ssm.tf | 143 ++++++++++++++++++ modules/perforce/helix-core/variables.tf | 52 +++++-- modules/perforce/helix-swarm/iam.tf | 23 ++- 16 files changed, 670 insertions(+), 86 deletions(-) create mode 100644 assets/ansible-playbooks/perforce/helix-core/SSM-Run-AMZL2023.json create mode 100644 assets/ansible-playbooks/perforce/helix-core/ssm.tf create mode 100644 modules/perforce/helix-core/ssm.tf diff --git a/assets/ansible-playbooks/perforce/helix-core/SSM-Run-AMZL2023.json b/assets/ansible-playbooks/perforce/helix-core/SSM-Run-AMZL2023.json new file mode 100644 index 00000000..19ab3787 --- /dev/null +++ b/assets/ansible-playbooks/perforce/helix-core/SSM-Run-AMZL2023.json @@ -0,0 +1,123 @@ +{ + "schemaVersion": "2.2", + "description": "Use this document to run Ansible Playbooks on Systems Manager managed instances. For more information, see https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-state-manager-ansible.html.", + "parameters": { + "SourceType": { + "description": "(Optional) Specify the source type.", + "type": "String", + "allowedValues": [ + "GitHub", + "S3" + ] + }, + "SourceInfo": { + "description": "(Optional) Specify the information required to access the resource from the specified source type. If source type is GitHub, then you can specify any of the following: 'owner', 'repository', 'path', 'getOptions', 'tokenInfo'. Example GitHub parameters: {\"owner\":\"awslabs\",\"repository\":\"amazon-ssm\",\"path\":\"Compliance/InSpec/PortCheck\",\"getOptions\":\"branch:master\"}. If source type is S3, then you can specify 'path'. Important: If you specify S3, then the IAM instance profile on your managed instances must be configured with read access to Amazon S3.", + "type": "StringMap", + "displayType": "textarea", + "default": {} + }, + "InstallDependencies": { + "type": "String", + "description": "(Required) If set to True, Systems Manager installs Ansible and its dependencies, including Python, from the PyPI repo. If set to False, then verify that Ansible and its dependencies are installed on the target instances. If they aren’t, the SSM document fails to run.", + "allowedValues": [ + "True", + "False" + ], + "default": "True" + }, + "PlaybookFile": { + "type": "String", + "description": "(Optional) The Playbook file to run (including relative path). If the main Playbook file is located in the ./automation directory, then specify automation/playbook.yml.", + "default": "hello-world-playbook.yml", + "allowedPattern": "[(a-z_A-Z0-9\\-\\.)/]+(.yml|.yaml)$" + }, + "ExtraVariables": { + "type": "String", + "description": "(Optional) Additional variables to pass to Ansible at runtime. Enter key/value pairs separated by a space. For example: color=red flavor=cherry", + "default": "SSM=True", + "displayType": "textarea", + "allowedPattern": "^$|^\\w+\\=(([^\\s|:();&]+)|('[^|:();&]+'))(\\s+\\w+\\=(([^\\s|:();&]+)|('[^|:();&]+')))*$" + }, + "Check": { + "type": "String", + "description": "(Optional) Use this parameter to run a check of the Ansible execution. The system doesn’t make any changes to your systems. Instead, any module that supports check mode reports the changes it would make rather than making them. Modules that don’t support check mode take no action and don’t report changes that would be made.", + "allowedValues": [ + "True", + "False" + ], + "default": "False" + }, + "Verbose": { + "type": "String", + "description": "(Optional) Set the verbosity level for logging Playbook executions. Specify -v for low verbosity, -vv or –vvv for medium verbosity, and -vvvv for debug level.", + "allowedValues": [ + "-v", + "-vv", + "-vvv", + "-vvvv" + ], + "default": "-v" + }, + "TimeoutSeconds": { + "type": "String", + "description": "(Optional) The time in seconds for a command to be completed before it is considered to have failed.", + "default": "3600" + } + }, + "mainSteps": [ + { + "action": "aws:downloadContent", + "name": "downloadContent", + "inputs": { + "SourceType": "{{ SourceType }}", + "SourceInfo": "{{ SourceInfo }}" + } + }, + { + "action": "aws:runShellScript", + "name": "runShellScript", + "inputs": { + "timeoutSeconds": "{{ TimeoutSeconds }}", + "runCommand": [ + "#!/bin/bash", + "if [[ \"{{InstallDependencies}}\" == True ]] ; then", + " echo \"Installing and/or updating required tools: Ansible, wget, unzip ...\" >&2", + " if [ -f \"/etc/system-release\" ] ; then", + " if grep -q 'Amazon Linux release 2023' /etc/system-release ; then", + " sudo dnf install -y ansible wget unzip", + " elif grep -q 'Amazon Linux release 2' /etc/system-release ; then", + " sudo yum install -y ansible wget unzip", + " elif grep -q 'Red Hat Enterprise Linux' /etc/system-release ; then", + " sudo dnf install -y ansible wget unzip", + " else", + " echo \"Unsupported Amazon Linux or RHEL version. Please install Ansible, wget, and unzip manually.\" >&2", + " exit 1", + " fi", + " elif grep -qi ubuntu /etc/issue ; then", + " sudo apt-get update", + " sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ansible wget unzip", + " else", + " echo \"Unsupported operating system. Please install Ansible, wget, and unzip manually.\" >&2", + " exit 1", + " fi", + "fi", + "echo \"Running Ansible in $(pwd)\"", + "for zip in $(find -iname '*.zip'); do", + " unzip -o $zip", + "done", + "PlaybookFile=\"{{PlaybookFile}}\"", + "if [ ! -f \"${PlaybookFile}\" ] ; then", + " echo \"The specified Playbook file doesn't exist in the downloaded bundle. Please review the relative path and file name.\" >&2", + " exit 2", + "fi", + "PYTHON_INTERPRETER=$(which python3)", + "if [[ \"{{Check}}\" == True ]] ; then", + " ansible-playbook -i \"localhost,\" --check -c local -e \"ansible_python_interpreter=${PYTHON_INTERPRETER}\" -e \"{{ExtraVariables}}\" \"{{Verbose}}\" \"${PlaybookFile}\"", + "else", + " ansible-playbook -i \"localhost,\" -c local -e \"ansible_python_interpreter=${PYTHON_INTERPRETER}\" -e \"{{ExtraVariables}}\" \"{{Verbose}}\" \"${PlaybookFile}\"", + "fi" + ] + } + } + ] +} \ No newline at end of file diff --git a/assets/ansible-playbooks/perforce/helix-core/ssm.tf b/assets/ansible-playbooks/perforce/helix-core/ssm.tf new file mode 100644 index 00000000..730b3760 --- /dev/null +++ b/assets/ansible-playbooks/perforce/helix-core/ssm.tf @@ -0,0 +1,143 @@ +variable "s3_bucket_name" { + description = "S3 bucket where playbook is stored" + type = string + + +} + +variable "playbook_file_name" { + description = "The name of the playbook yaml on s3" + type = string +} + +resource "aws_ssm_document" "ansible_playbook" { + name = "Toolkit-AnsiblePlaybook" + document_type = "Command" + target_type = "/AWS::EC2::Instance" + content = jsonencode({ + schemaVersion = "2.2" + description = "Use this document to run Ansible Playbooks on Systems Manager managed instances. For more information, see https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-state-manager-ansible.html." + parameters = { + SourceType = { + description = "(Optional) Specify the source type." + type = "String" + allowedValues = ["GitHub", "S3"] + } + SourceInfo = { + description = "(Optional) Specify the information required to access the resource from the specified source type. If source type is GitHub, then you can specify any of the following: 'owner', 'repository', 'path', 'getOptions', 'tokenInfo'. Example GitHub parameters: {\"owner\":\"awslabs\",\"repository\":\"amazon-ssm\",\"path\":\"Compliance/InSpec/PortCheck\",\"getOptions\":\"branch:master\"}. If source type is S3, then you can specify 'path'. Important: If you specify S3, then the IAM instance profile on your managed instances must be configured with read access to Amazon S3." + type = "StringMap" + displayType = "textarea" + default = {} + } + InstallDependencies = { + type = "String" + description = "(Required) If set to True, Systems Manager installs Ansible and its dependencies, including Python, from the PyPI repo. If set to False, then verify that Ansible and its dependencies are installed on the target instances. If they aren't, the SSM document fails to run." + allowedValues = ["True", "False"] + default = "True" + } + PlaybookFile = { + type = "String" + description = "(Optional) The Playbook file to run (including relative path). If the main Playbook file is located in the ./automation directory, then specify automation/playbook.yml." + default = "hello-world-playbook.yml" + allowedPattern = "[(a-z_A-Z0-9\\-\\.)/]+(.yml|.yaml)$" + } + ExtraVariables = { + type = "String" + description = "(Optional) Additional variables to pass to Ansible at runtime. Enter key/value pairs separated by a space. For example: color=red flavor=cherry" + default = "SSM=True" + displayType = "textarea" + allowedPattern = "^$|^\\w+\\=(([^\\s|:();&]+)|('[^|:();&]+'))(\\s+\\w+\\=(([^\\s|:();&]+)|('[^|:();&]+')))*$" + } + Check = { + type = "String" + description = "(Optional) Use this parameter to run a check of the Ansible execution. The system doesn't make any changes to your systems. Instead, any module that supports check mode reports the changes it would make rather than making them. Modules that don't support check mode take no action and don't report changes that would be made." + allowedValues = ["True", "False"] + default = "False" + } + Verbose = { + type = "String" + description = "(Optional) Set the verbosity level for logging Playbook executions. Specify -v for low verbosity, -vv or –vvv for medium verbosity, and -vvvv for debug level." + allowedValues = ["-v", "-vv", "-vvv", "-vvvv"] + default = "-v" + } + TimeoutSeconds = { + type = "String" + description = "(Optional) The time in seconds for a command to be completed before it is considered to have failed." + default = "3600" + } + } + mainSteps = [ + { + action = "aws:downloadContent" + name = "downloadContent" + inputs = { + SourceType = "{{ SourceType }}" + SourceInfo = "{{ SourceInfo }}" + } + }, + { + action = "aws:runShellScript" + name = "runShellScript" + inputs = { + timeoutSeconds = "{{ TimeoutSeconds }}" + runCommand = [ + "#!/bin/bash", + "if [[ \"{{InstallDependencies}}\" == True ]] ; then", + " echo \"Installing and/or updating required tools: Ansible, wget, unzip ...\" >&2", + " if [ -f \"/etc/system-release\" ] ; then", + " if grep -q 'Amazon Linux release 2023' /etc/system-release ; then", + " sudo dnf install -y ansible wget unzip", + " elif grep -q 'Amazon Linux release 2' /etc/system-release ; then", + " sudo yum install -y ansible wget unzip", + " elif grep -q 'Red Hat Enterprise Linux' /etc/system-release ; then", + " sudo dnf install -y ansible wget unzip", + " else", + " echo \"Unsupported Amazon Linux or RHEL version. Please install Ansible, wget, and unzip manually.\" >&2", + " exit 1", + " fi", + " elif grep -qi ubuntu /etc/issue ; then", + " sudo apt-get update", + " sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ansible wget unzip", + " else", + " echo \"Unsupported operating system. Please install Ansible, wget, and unzip manually.\" >&2", + " exit 1", + " fi", + "fi", + "echo \"Running Ansible in $(pwd)\"", + "for zip in $(find -iname '*.zip'); do", + " unzip -o $zip", + "done", + "PlaybookFile=\"{{PlaybookFile}}\"", + "if [ ! -f \"$${PlaybookFile}\" ] ; then", + " echo \"The specified Playbook file doesn't exist in the downloaded bundle. Please review the relative path and file name.\" >&2", + " exit 2", + "fi", + "PYTHON_INTERPRETER=$(which python3)", + "if [[ \"{{Check}}\" == True ]] ; then", + " ansible-playbook -i \"localhost,\" --check -c local -e \"ansible_python_interpreter=$${PYTHON_INTERPRETER}\" -e \"{{ExtraVariables}}\" \"{{Verbose}}\" \"$${PlaybookFile}\"", + "else", + " ansible-playbook -i \"localhost,\" -c local -e \"ansible_python_interpreter=$${PYTHON_INTERPRETER}\" -e \"{{ExtraVariables}}\" \"{{Verbose}}\" \"$${PlaybookFile}\"", + "fi" + ] + } + } + ] + }) +} + +resource "aws_ssm_association" "toolkitdoc" { + name = aws_ssm_document.ansible_playbook.name + association_name = "Toolkit-AnsibleAssociation" + + targets { + key = "tag:Project" + values = ["ToolkitTest"] + } + parameters = { + SourceType = "S3" + SourceInfo = jsonencode({ + "path" = "https://s3.amazonaws.com/${var.s3_bucket_name}/${var.playbook_file_name}" + }) + PlaybookFile = "${var.playbook_file_name}" + } +} diff --git a/modules/perforce/examples/complete/dns.tf b/modules/perforce/examples/complete/dns.tf index a3d9a107..652a9e58 100644 --- a/modules/perforce/examples/complete/dns.tf +++ b/modules/perforce/examples/complete/dns.tf @@ -43,21 +43,25 @@ resource "aws_route53_record" "helix_authentication_service" { } resource "aws_route53_record" "perforce_helix_core" { + for_each = module.perforce_helix_core.helix_core_eip_public_ips + zone_id = data.aws_route53_zone.root.zone_id - name = "core.helix.${data.aws_route53_zone.root.name}" + name = "${each.key}.core.helix.${data.aws_route53_zone.root.name}" type = "A" ttl = 300 #checkov:skip=CKV2_AWS_23:The attached resource is managed by CGD Toolkit - records = [module.perforce_helix_core.helix_core_eip_public_ip] + records = [each.value] } resource "aws_route53_record" "perforce_helix_core_pvt" { + for_each = module.perforce_helix_core.helix_core_private_ips + zone_id = aws_route53_zone.helix_private_zone.zone_id - name = "core.${aws_route53_zone.helix_private_zone.name}" + name = "${each.key}.core.${aws_route53_zone.helix_private_zone.name}" type = "A" ttl = 300 #checkov:skip=CKV2_AWS_23:The attached resource is managed by CGD Toolkit - records = [module.perforce_helix_core.helix_core_eip_private_ip] + records = [each.value] } ########################################## diff --git a/modules/perforce/examples/complete/main.tf b/modules/perforce/examples/complete/main.tf index 674c5166..58f0aa6f 100644 --- a/modules/perforce/examples/complete/main.tf +++ b/modules/perforce/examples/complete/main.tf @@ -29,11 +29,25 @@ resource "aws_ecs_cluster_capacity_providers" "providers" { module "perforce_helix_core" { source = "../../helix-core" - vpc_id = aws_vpc.perforce_vpc.id + server_type = "p4d_commit" - instance_subnet_id = aws_subnet.public_subnets[0].id - instance_type = "c6g.large" - instance_architecture = "arm64" + + instance_type = "c6in.xlarge" + instance_architecture = "x86_64" + + server_configuration = [ + { + type = "commit" + vpc_id = aws_vpc.perforce_vpc.id + subnet_id = aws_subnet.public_subnets[0].id + }, + { + type = "replica" + vpc_id = aws_vpc.perforce_vpc.id + subnet_id = aws_subnet.private_subnets[0].id + } + ] + storage_type = "EBS" depot_volume_size = 64 @@ -43,6 +57,9 @@ module "perforce_helix_core" { fully_qualified_domain_name = "core.helix.perforce.${var.root_domain_name}" helix_authentication_service_url = "https://${aws_route53_record.helix_authentication_service.name}" + + helix_core_super_user_password_secret_name = "example-password-secret-name" + helix_core_super_user_username_secret_name = "example-username-secret-name" } ########################################## @@ -74,7 +91,7 @@ module "perforce_helix_swarm" { helix_swarm_alb_subnets = aws_subnet.public_subnets[*].id helix_swarm_service_subnets = aws_subnet.private_subnets[*].id certificate_arn = aws_acm_certificate.helix.arn - p4d_port = "ssl:${aws_route53_record.perforce_helix_core_pvt.name}:1666" + p4d_port = "ssl:${aws_route53_record.perforce_helix_core_pvt[var.helix_core_server_type].name}:1666" p4d_super_user_arn = module.perforce_helix_core.helix_core_super_user_username_secret_arn p4d_super_user_password_arn = module.perforce_helix_core.helix_core_super_user_password_secret_arn p4d_swarm_user_arn = module.perforce_helix_core.helix_core_super_user_username_secret_arn diff --git a/modules/perforce/examples/complete/security.tf b/modules/perforce/examples/complete/security.tf index a682b045..797bb261 100644 --- a/modules/perforce/examples/complete/security.tf +++ b/modules/perforce/examples/complete/security.tf @@ -4,30 +4,37 @@ # Helix Swarm -> Helix Core resource "aws_vpc_security_group_ingress_rule" "helix_core_inbound_swarm" { - security_group_id = module.perforce_helix_core.security_group_id + for_each = module.perforce_helix_core.security_group_ids + + security_group_id = each.value ip_protocol = "TCP" from_port = 1666 to_port = 1666 referenced_security_group_id = module.perforce_helix_swarm.service_security_group_id - description = "Enables Helix Swarm to access Helix Core." + description = "Enables Helix Swarm to access Helix Core ${each.key}." } # Helix Core -> Helix Swarm resource "aws_vpc_security_group_ingress_rule" "helix_swarm_inbound_core" { + for_each = module.perforce_helix_core.helix_core_eip_public_ips + security_group_id = module.perforce_helix_swarm.alb_security_group_id ip_protocol = "TCP" from_port = 443 to_port = 443 - cidr_ipv4 = "${module.perforce_helix_core.helix_core_eip_public_ip}/32" - description = "Enables Helix Core to access Helix Swarm" + cidr_ipv4 = "${each.value}/32" + description = "Enables Helix Core ${each.key} to access Helix Swarm" } # Helix Core -> Helix Authentication Service resource "aws_vpc_security_group_ingress_rule" "helix_auth_inbound_core" { + for_each = module.perforce_helix_core.helix_core_eip_public_ips + security_group_id = module.perforce_helix_authentication_service.alb_security_group_id ip_protocol = "TCP" from_port = 443 to_port = 443 - cidr_ipv4 = "${module.perforce_helix_core.helix_core_eip_public_ip}/32" - description = "Enables Helix Core to access Helix Authentication Service" + cidr_ipv4 = "${each.value}/32" + description = "Enables Helix Core ${each.key} to access Helix Authentication Service" } + diff --git a/modules/perforce/examples/complete/variables.tf b/modules/perforce/examples/complete/variables.tf index cde652cd..d05a8862 100644 --- a/modules/perforce/examples/complete/variables.tf +++ b/modules/perforce/examples/complete/variables.tf @@ -2,3 +2,7 @@ variable "root_domain_name" { type = string description = "The root domain name you would like to use for DNS." } +variable "helix_core_server_type" { + type = string + default = "commit" +} \ No newline at end of file diff --git a/modules/perforce/examples/complete/versions.tf b/modules/perforce/examples/complete/versions.tf index a49b9edb..6e326965 100644 --- a/modules/perforce/examples/complete/versions.tf +++ b/modules/perforce/examples/complete/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "5.66.0" + version = ">= 5.66.0" } } } diff --git a/modules/perforce/helix-authentication-service/iam.tf b/modules/perforce/helix-authentication-service/iam.tf index c05f22a3..3957bf6c 100644 --- a/modules/perforce/helix-authentication-service/iam.tf +++ b/modules/perforce/helix-authentication-service/iam.tf @@ -73,12 +73,20 @@ resource "aws_iam_role" "helix_authentication_service_default_role" { name = "${var.project_prefix}-helix_authentication_service-default-role" assume_role_policy = data.aws_iam_policy_document.ecs_tasks_trust_relationship.json - managed_policy_arns = [ - aws_iam_policy.helix_authentication_service_default_policy[0].arn - ] + # managed_policy_arns = [ + # aws_iam_policy.helix_authentication_service_default_policy[0].arn + # ] tags = local.tags } +resource "aws_iam_role_policy_attachment" "helix_authentication_service_default_policy_attachment" { + count = var.create_helix_authentication_service_default_role ? 1 : 0 + + role = aws_iam_role.helix_authentication_service_default_role[0].name + policy_arn = aws_iam_policy.helix_authentication_service_default_policy[0].arn + +} + data "aws_iam_policy_document" "helix_authentication_service_secrets_manager_policy" { statement { effect = "Allow" @@ -107,5 +115,10 @@ resource "aws_iam_role" "helix_authentication_service_task_execution_role" { name = "${var.project_prefix}-helix_authentication_service-task-execution-role" assume_role_policy = data.aws_iam_policy_document.ecs_tasks_trust_relationship.json - managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", aws_iam_policy.helix_authentication_service_secrets_manager_policy.arn] + # managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", aws_iam_policy.helix_authentication_service_secrets_manager_policy.arn] } + +resource "aws_iam_role_policy_attachment" "helix_authentication_service_secrets_manager_policy_attachment" { + role = aws_iam_role.helix_authentication_service_task_execution_role.name + policy_arn = aws_iam_policy.helix_authentication_service_secrets_manager_policy.arn +} \ No newline at end of file diff --git a/modules/perforce/helix-core/data.tf b/modules/perforce/helix-core/data.tf index 382051b5..73d7d6cd 100644 --- a/modules/perforce/helix-core/data.tf +++ b/modules/perforce/helix-core/data.tf @@ -1,7 +1,13 @@ -data "aws_subnet" "instance_subnet" { - id = var.instance_subnet_id +#data "aws_subnet" "instance_subnet" { +# id = var.instance_subnet_id +#} + +data "aws_subnet" "selected" { + for_each = { for idx, server in var.server_configuration : server.type => server } + id = each.value.subnet_id } + # Fetching custom Perforce Helix Core AMI data "aws_ami" "helix_core_ami" { most_recent = true diff --git a/modules/perforce/helix-core/iam.tf b/modules/perforce/helix-core/iam.tf index 10c24de2..3867b267 100644 --- a/modules/perforce/helix-core/iam.tf +++ b/modules/perforce/helix-core/iam.tf @@ -47,11 +47,23 @@ resource "aws_iam_role" "helix_core_default_role" { name = "${var.project_prefix}-${var.name}-helix-core-${var.server_type}-role" assume_role_policy = data.aws_iam_policy_document.ec2_trust_relationship.json - managed_policy_arns = ["arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", aws_iam_policy.helix_core_default_policy.arn] + #managed_policy_arns = ["arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", aws_iam_policy.helix_core_default_policy.arn] tags = local.tags } +resource "aws_iam_role_policy_attachment" "helix_core_ssm_policy_attachment" { + count = var.create_helix_core_default_role ? 1 : 0 + role = aws_iam_role.helix_core_default_role[0].name + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" +} + +resource "aws_iam_role_policy_attachment" "helix_core_default_policy_attachment" { + count = var.create_helix_core_default_role ? 1 : 0 + role = aws_iam_role.helix_core_default_role[0].name + policy_arn = aws_iam_policy.helix_core_default_policy.arn +} + # Instance Profile resource "aws_iam_instance_profile" "helix_core_instance_profile" { name = "${local.name_prefix}-${random_string.helix_core.result}-instance-profile" diff --git a/modules/perforce/helix-core/locals.tf b/modules/perforce/helix-core/locals.tf index eee2d35e..5c32b099 100644 --- a/modules/perforce/helix-core/locals.tf +++ b/modules/perforce/helix-core/locals.tf @@ -1,10 +1,28 @@ locals { name_prefix = "${var.project_prefix}-${var.name}" - helix_core_az = data.aws_subnet.instance_subnet.availability_zone + #helix_core_az = data.aws_subnet.instance_subnet.availability_zone tags = merge( { "environment" = var.environment }, var.tags, ) + + p4_server_type_tags = { + commit = { + ServerType = "Commit" + Role = "Primary" + } + replica = { + ServerType = "Replica" + Role = "Backup" + } + edge = { + ServerType = "Edge" + Role = "Access" + } + } + + server_private_ips = {for k, v in aws_instance.helix_core_instance : k => v.private_ip} + } diff --git a/modules/perforce/helix-core/main.tf b/modules/perforce/helix-core/main.tf index 1f37aa73..acea33c4 100644 --- a/modules/perforce/helix-core/main.tf +++ b/modules/perforce/helix-core/main.tf @@ -4,7 +4,7 @@ resource "awscc_secretsmanager_secret" "helix_core_super_user_password" { count = var.helix_core_super_user_password_secret_arn == null ? 1 : 0 - name = "perforceHelixCoreSuperUserPassword" + name = var.helix_core_super_user_password_secret_name description = "The password for the created Helix Core super user." generate_secret_string = { exclude_numbers = false @@ -15,7 +15,7 @@ resource "awscc_secretsmanager_secret" "helix_core_super_user_password" { resource "awscc_secretsmanager_secret" "helix_core_super_user_username" { count = var.helix_core_super_user_username_secret_arn == null ? 1 : 0 - name = "perforceHelixCoreSuperUserUsername" + name = var.helix_core_super_user_username_secret_name secret_string = "perforce" } @@ -25,11 +25,13 @@ resource "awscc_secretsmanager_secret" "helix_core_super_user_username" { ########################################## resource "aws_instance" "helix_core_instance" { + for_each = { for idx, server in var.server_configuration : server.type => server} + ami = data.aws_ami.helix_core_ami.id instance_type = var.instance_type - availability_zone = local.helix_core_az - subnet_id = var.instance_subnet_id + #availability_zone = local.helix_core_az + subnet_id = each.value.subnet_id iam_instance_profile = aws_iam_instance_profile.helix_core_instance_profile.id @@ -47,7 +49,9 @@ resource "aws_instance" "helix_core_instance" { EOT - vpc_security_group_ids = var.create_default_sg ? concat(var.existing_security_groups, [aws_security_group.helix_core_security_group[0].id]) : var.existing_security_groups + #vpc_security_group_ids = var.create_default_sg ? concat(var.existing_security_groups, [aws_security_group.helix_core_security_group[0].id]) : var.existing_security_groups + + vpc_security_group_ids = [aws_security_group.helix_core_security_group[each.key].id] metadata_options { http_endpoint = "enabled" @@ -64,8 +68,11 @@ resource "aws_instance" "helix_core_instance" { } tags = merge(local.tags, { - Name = "${local.name_prefix}-${var.server_type}-${local.helix_core_az}" - }) + #Name = "${local.name_prefix}-${var.server_type}-${local.helix_core_az}" + Name = "${local.name_prefix}-${each.key}" + }, + local.p4_server_type_tags[each.key] + ) } ########################################## @@ -73,8 +80,9 @@ resource "aws_instance" "helix_core_instance" { ########################################## resource "aws_eip" "helix_core_eip" { - count = var.internal ? 0 : 1 - instance = aws_instance.helix_core_instance.id + for_each = { for idx, server in var.server_configuration : server.type => server if !var.internal } + #count = var.internal ? 0 : 1 + instance = aws_instance.helix_core_instance[each.key].id domain = "vpc" } @@ -84,47 +92,53 @@ resource "aws_eip" "helix_core_eip" { // hxlogs resource "aws_ebs_volume" "logs" { - availability_zone = local.helix_core_az + for_each = {for idx, server in var.server_configuration : server.type => server } + availability_zone = data.aws_subnet.selected[each.key].availability_zone size = var.logs_volume_size encrypted = true #checkov:skip=CKV_AWS_189: CMK encryption not supported currently - tags = local.tags + tags = merge(local.tags, { Name = "${local.name_prefix}-${each.key}-logs" }) } resource "aws_volume_attachment" "logs_attachment" { + for_each = { for idx, server in var.server_configuration : server.type => server } device_name = "/dev/sdf" - volume_id = aws_ebs_volume.logs.id - instance_id = aws_instance.helix_core_instance.id + volume_id = aws_ebs_volume.logs[each.key].id + instance_id = aws_instance.helix_core_instance[each.key].id } // hxmetadata resource "aws_ebs_volume" "metadata" { - availability_zone = local.helix_core_az + for_each = { for idx, server in var.server_configuration : server.type => server } + availability_zone = data.aws_subnet.selected[each.key].availability_zone size = var.metadata_volume_size encrypted = true #checkov:skip=CKV_AWS_189: CMK encryption not supported currently - tags = local.tags + tags = merge(local.tags, { Name = "${local.name_prefix}-${each.key}-logs" }) } resource "aws_volume_attachment" "metadata_attachment" { + for_each = { for idx, server in var.server_configuration : server.type => server } device_name = "/dev/sdg" - volume_id = aws_ebs_volume.metadata.id - instance_id = aws_instance.helix_core_instance.id + volume_id = aws_ebs_volume.metadata[each.key].id + instance_id = aws_instance.helix_core_instance[each.key].id } // hxdepot resource "aws_ebs_volume" "depot" { - availability_zone = local.helix_core_az + for_each = { for idx, server in var.server_configuration : server.type => server } + availability_zone = data.aws_subnet.selected[each.key].availability_zone size = var.depot_volume_size encrypted = true #checkov:skip=CKV_AWS_189: CMK encryption not supported currently - tags = local.tags + tags = merge(local.tags, { Name = "${local.name_prefix}-${each.key}-logs" }) } resource "aws_volume_attachment" "depot_attachment" { + for_each = { for idx, server in var.server_configuration : server.type => server } device_name = "/dev/sdh" - volume_id = aws_ebs_volume.depot.id - instance_id = aws_instance.helix_core_instance.id + volume_id = aws_ebs_volume.depot[each.key].id + instance_id = aws_instance.helix_core_instance[each.key].id } ########################################## @@ -132,19 +146,48 @@ resource "aws_volume_attachment" "depot_attachment" { ########################################## resource "aws_security_group" "helix_core_security_group" { - count = var.create_default_sg ? 1 : 0 + for_each = { for idx, server in var.server_configuration : server.type => server} + #count = var.create_default_sg ? 1 : 0 #checkov:skip=CKV2_AWS_5:SG is attahced to FSxZ file systems - vpc_id = var.vpc_id - name = "${local.name_prefix}-instance" - description = "Security group for Helix Core machines." + vpc_id = each.value.vpc_id + name = "${local.name_prefix}-${each.key}-instance" + description = "Security group for Helix Core ${each.key} machine." tags = local.tags } resource "aws_vpc_security_group_egress_rule" "helix_core_internet" { - count = var.create_default_sg ? 1 : 0 - security_group_id = aws_security_group.helix_core_security_group[0].id + for_each = {for idx, server in var.server_configuration : server.type => server} + #count = var.create_default_sg ? 1 : 0 + security_group_id = aws_security_group.helix_core_security_group[each.key].id cidr_ipv4 = "0.0.0.0/0" ip_protocol = -1 description = "Helix Core out to Internet" } + +resource "aws_vpc_security_group_ingress_rule" "helix_core_inter_server_1666" { + for_each = { for idx, server in var.server_configuration : server.type => server } + + security_group_id = aws_security_group.helix_core_security_group[each.key].id + + from_port = 1666 + to_port = 1669 + ip_protocol = "tcp" + + referenced_security_group_id = aws_security_group.helix_core_security_group[each.key].id + + description = "Allow incoming traffic on port 1666-1669 from other Perforce servers" +} + +########################################## +# Systems Manager Parameter Store - Add facts about Helix Core servers +########################################## + +resource "aws_ssm_parameter" "server_info" { + for_each = { for idx, server in var.server_configuration : server.type => server } + name = "/perforce/${each.key}/server_info" + type = "StringList" + value = "${aws_instance.helix_core_instance[each.key].private_ip},${aws_instance.helix_core_instance[each.key].private_dns}" + tags = local.tags +} + diff --git a/modules/perforce/helix-core/outputs.tf b/modules/perforce/helix-core/outputs.tf index d86bdf0f..ddfdf0d3 100644 --- a/modules/perforce/helix-core/outputs.tf +++ b/modules/perforce/helix-core/outputs.tf @@ -1,21 +1,21 @@ -output "helix_core_eip_private_ip" { - value = aws_eip.helix_core_eip[0].private_ip - description = "The private IP of your Helix Core instance." +output "helix_core_eip_private_ips" { + value = { for k, v in aws_eip.helix_core_eip : k => v.private_ip } + description = "Map of server types to their private IPs (for EIPs)." } -output "helix_core_eip_public_ip" { - value = aws_eip.helix_core_eip[0].public_ip - description = "The public IP of your Helix Core instance." +output "helix_core_eip_public_ips" { + value = { for k, v in aws_eip.helix_core_eip : k => v.public_ip } + description = "Map of server types to their public IPs." } -output "helix_core_eip_id" { - value = aws_eip.helix_core_eip[0].id - description = "The ID of the Elastic IP associated with your Helix Core instance." +output "helix_core_eip_ids" { + value = { for k, v in aws_eip.helix_core_eip : k => v.id } + description = "Map of server types to their Elastic IP IDs." } -output "security_group_id" { - value = var.create_default_sg ? aws_security_group.helix_core_security_group[0].id : null - description = "The default security group of your Helix Core instance." +output "security_group_ids" { + value = { for k, v in aws_security_group.helix_core_security_group : k => v.id } + description = "Map of server types to their security group IDs." } output "helix_core_super_user_username_secret_arn" { @@ -28,7 +28,21 @@ output "helix_core_super_user_password_secret_arn" { description = "The ARN of the AWS Secrets Manager secret holding your Helix Core super user's password." } -output "helix_core_instance_id" { - value = aws_instance.helix_core_instance.id - description = "Instance ID for the Helix Core instance" -} \ No newline at end of file +output "helix_core_instance_ids" { + value = { for k, v in aws_instance.helix_core_instance : k => v.id } + description = "Map of server types to their EC2 instance IDs." +} + +output "helix_core_private_ips" { + value = { for k, v in aws_instance.helix_core_instance : k => v.private_ip } + description = "Map of server types to their private IP addresses." +} + +output "ebs_volume_ids" { + value = { + logs = { for k, v in aws_ebs_volume.logs : k => v.id } + metadata = { for k, v in aws_ebs_volume.metadata : k => v.id } + depot = { for k, v in aws_ebs_volume.depot : k => v.id } + } + description = "Map of EBS volume types and server types to their volume IDs." +} diff --git a/modules/perforce/helix-core/ssm.tf b/modules/perforce/helix-core/ssm.tf new file mode 100644 index 00000000..730b3760 --- /dev/null +++ b/modules/perforce/helix-core/ssm.tf @@ -0,0 +1,143 @@ +variable "s3_bucket_name" { + description = "S3 bucket where playbook is stored" + type = string + + +} + +variable "playbook_file_name" { + description = "The name of the playbook yaml on s3" + type = string +} + +resource "aws_ssm_document" "ansible_playbook" { + name = "Toolkit-AnsiblePlaybook" + document_type = "Command" + target_type = "/AWS::EC2::Instance" + content = jsonencode({ + schemaVersion = "2.2" + description = "Use this document to run Ansible Playbooks on Systems Manager managed instances. For more information, see https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-state-manager-ansible.html." + parameters = { + SourceType = { + description = "(Optional) Specify the source type." + type = "String" + allowedValues = ["GitHub", "S3"] + } + SourceInfo = { + description = "(Optional) Specify the information required to access the resource from the specified source type. If source type is GitHub, then you can specify any of the following: 'owner', 'repository', 'path', 'getOptions', 'tokenInfo'. Example GitHub parameters: {\"owner\":\"awslabs\",\"repository\":\"amazon-ssm\",\"path\":\"Compliance/InSpec/PortCheck\",\"getOptions\":\"branch:master\"}. If source type is S3, then you can specify 'path'. Important: If you specify S3, then the IAM instance profile on your managed instances must be configured with read access to Amazon S3." + type = "StringMap" + displayType = "textarea" + default = {} + } + InstallDependencies = { + type = "String" + description = "(Required) If set to True, Systems Manager installs Ansible and its dependencies, including Python, from the PyPI repo. If set to False, then verify that Ansible and its dependencies are installed on the target instances. If they aren't, the SSM document fails to run." + allowedValues = ["True", "False"] + default = "True" + } + PlaybookFile = { + type = "String" + description = "(Optional) The Playbook file to run (including relative path). If the main Playbook file is located in the ./automation directory, then specify automation/playbook.yml." + default = "hello-world-playbook.yml" + allowedPattern = "[(a-z_A-Z0-9\\-\\.)/]+(.yml|.yaml)$" + } + ExtraVariables = { + type = "String" + description = "(Optional) Additional variables to pass to Ansible at runtime. Enter key/value pairs separated by a space. For example: color=red flavor=cherry" + default = "SSM=True" + displayType = "textarea" + allowedPattern = "^$|^\\w+\\=(([^\\s|:();&]+)|('[^|:();&]+'))(\\s+\\w+\\=(([^\\s|:();&]+)|('[^|:();&]+')))*$" + } + Check = { + type = "String" + description = "(Optional) Use this parameter to run a check of the Ansible execution. The system doesn't make any changes to your systems. Instead, any module that supports check mode reports the changes it would make rather than making them. Modules that don't support check mode take no action and don't report changes that would be made." + allowedValues = ["True", "False"] + default = "False" + } + Verbose = { + type = "String" + description = "(Optional) Set the verbosity level for logging Playbook executions. Specify -v for low verbosity, -vv or –vvv for medium verbosity, and -vvvv for debug level." + allowedValues = ["-v", "-vv", "-vvv", "-vvvv"] + default = "-v" + } + TimeoutSeconds = { + type = "String" + description = "(Optional) The time in seconds for a command to be completed before it is considered to have failed." + default = "3600" + } + } + mainSteps = [ + { + action = "aws:downloadContent" + name = "downloadContent" + inputs = { + SourceType = "{{ SourceType }}" + SourceInfo = "{{ SourceInfo }}" + } + }, + { + action = "aws:runShellScript" + name = "runShellScript" + inputs = { + timeoutSeconds = "{{ TimeoutSeconds }}" + runCommand = [ + "#!/bin/bash", + "if [[ \"{{InstallDependencies}}\" == True ]] ; then", + " echo \"Installing and/or updating required tools: Ansible, wget, unzip ...\" >&2", + " if [ -f \"/etc/system-release\" ] ; then", + " if grep -q 'Amazon Linux release 2023' /etc/system-release ; then", + " sudo dnf install -y ansible wget unzip", + " elif grep -q 'Amazon Linux release 2' /etc/system-release ; then", + " sudo yum install -y ansible wget unzip", + " elif grep -q 'Red Hat Enterprise Linux' /etc/system-release ; then", + " sudo dnf install -y ansible wget unzip", + " else", + " echo \"Unsupported Amazon Linux or RHEL version. Please install Ansible, wget, and unzip manually.\" >&2", + " exit 1", + " fi", + " elif grep -qi ubuntu /etc/issue ; then", + " sudo apt-get update", + " sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ansible wget unzip", + " else", + " echo \"Unsupported operating system. Please install Ansible, wget, and unzip manually.\" >&2", + " exit 1", + " fi", + "fi", + "echo \"Running Ansible in $(pwd)\"", + "for zip in $(find -iname '*.zip'); do", + " unzip -o $zip", + "done", + "PlaybookFile=\"{{PlaybookFile}}\"", + "if [ ! -f \"$${PlaybookFile}\" ] ; then", + " echo \"The specified Playbook file doesn't exist in the downloaded bundle. Please review the relative path and file name.\" >&2", + " exit 2", + "fi", + "PYTHON_INTERPRETER=$(which python3)", + "if [[ \"{{Check}}\" == True ]] ; then", + " ansible-playbook -i \"localhost,\" --check -c local -e \"ansible_python_interpreter=$${PYTHON_INTERPRETER}\" -e \"{{ExtraVariables}}\" \"{{Verbose}}\" \"$${PlaybookFile}\"", + "else", + " ansible-playbook -i \"localhost,\" -c local -e \"ansible_python_interpreter=$${PYTHON_INTERPRETER}\" -e \"{{ExtraVariables}}\" \"{{Verbose}}\" \"$${PlaybookFile}\"", + "fi" + ] + } + } + ] + }) +} + +resource "aws_ssm_association" "toolkitdoc" { + name = aws_ssm_document.ansible_playbook.name + association_name = "Toolkit-AnsibleAssociation" + + targets { + key = "tag:Project" + values = ["ToolkitTest"] + } + parameters = { + SourceType = "S3" + SourceInfo = jsonencode({ + "path" = "https://s3.amazonaws.com/${var.s3_bucket_name}/${var.playbook_file_name}" + }) + PlaybookFile = "${var.playbook_file_name}" + } +} diff --git a/modules/perforce/helix-core/variables.tf b/modules/perforce/helix-core/variables.tf index c430b7e6..dfdf46bd 100644 --- a/modules/perforce/helix-core/variables.tf +++ b/modules/perforce/helix-core/variables.tf @@ -60,10 +60,10 @@ variable "selinux" { ######################################## # Networking and Security ######################################## -variable "vpc_id" { - type = string - description = "The VPC where Helix Core should be deployed" -} +#variable "vpc_id" { +# type = string +# description = "The VPC where Helix Core Commit server should be deployed" +#} variable "create_default_sg" { type = bool @@ -71,16 +71,16 @@ variable "create_default_sg" { default = true } -variable "instance_subnet_id" { - type = string - description = "The subnet where the Helix Core instance will be deployed." -} +#variable "instance_subnet_id" { +# type = string +# description = "The subnet where the Helix Core instance will be deployed." +#} -variable "existing_security_groups" { - type = list(string) - description = "A list of existing security group IDs to attach to the Helix Core load balancer." - default = [] -} +#variable "existing_security_groups" { +# type = list(string) +# description = "A list of existing security group IDs to attach to the Helix Core load balancer." +# default = [] +#} variable "internal" { type = bool @@ -179,6 +179,19 @@ variable "helix_authentication_service_url" { default = null } +variable "helix_core_super_user_password_secret_name" { + type = string + description = "The name for the Secrets Manager secret holding the Helix Core super user password" + default = "perforceHelixCoreSuperUserPassword" +} + +variable "helix_core_super_user_username_secret_name" { + type = string + description = "The name for the Secrets Manager secret holding the Helix Core super user username" + default = "perforceHelixCoreSuperUserUsername" +} + + ######################################## # Helix Core settings ######################################## @@ -187,3 +200,16 @@ variable "helix_case_sensitive" { description = "Whether or not the server should be case insensitive (Server will run '-C1' mode), or if the server will run with case sensitivity default of the underlying platform. False enables '-C1' mode" default = true } + +variable "server_configuration" { + description = "Perforce Helix Core topology configuration i.e server types to create: commit, replica, edge. Each server requires VPC and Subnet" + type = list(object({ + type = string + subnet_id = string + vpc_id = string + })) + validation { + condition = length([for s in var.server_configuration : s if s.type == "commit"]) == 1 + error_message = "Only one commit server is allowed in configuration." + } +} \ No newline at end of file diff --git a/modules/perforce/helix-swarm/iam.tf b/modules/perforce/helix-swarm/iam.tf index b52f1e32..477b8e63 100644 --- a/modules/perforce/helix-swarm/iam.tf +++ b/modules/perforce/helix-swarm/iam.tf @@ -78,15 +78,26 @@ resource "aws_iam_role" "helix_swarm_default_role" { name = "${var.project_prefix}-helix-swarm-default-role" assume_role_policy = data.aws_iam_policy_document.ecs_tasks_trust_relationship.json - managed_policy_arns = [ - aws_iam_policy.helix_swarm_default_policy[0].arn, - ] tags = local.tags } +resource "aws_iam_role_policy_attachment" "helix_swarm_default_policy_attachment" { + count = var.create_helix_swarm_default_role ? 1 : 0 + role = aws_iam_role.helix_swarm_default_role[0].name + policy_arn = aws_iam_policy.helix_swarm_default_policy[0].arn +} + resource "aws_iam_role" "helix_swarm_task_execution_role" { - name = "${var.project_prefix}-helix-swarm-task-execution-role" + name = "${var.project_prefix}-helix-swarm-task-execution-role" + assume_role_policy = data.aws_iam_policy_document.ecs_tasks_trust_relationship.json +} + +resource "aws_iam_role_policy_attachment" "helix_swarm_task_execution_policy_attachment" { + role = aws_iam_role.helix_swarm_task_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} - assume_role_policy = data.aws_iam_policy_document.ecs_tasks_trust_relationship.json - managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", aws_iam_policy.helix_swarm_ssm_policy.arn] +resource "aws_iam_role_policy_attachment" "helix_swarm_ssm_policy_attachment" { + role = aws_iam_role.helix_swarm_task_execution_role.name + policy_arn = aws_iam_policy.helix_swarm_ssm_policy.arn } From 2d565f7b1a85fc424a5146a60528e0dc873b927b Mon Sep 17 00:00:00 2001 From: Greg Ochmanski Date: Thu, 31 Oct 2024 14:25:32 +0100 Subject: [PATCH 5/8] fix(terraform): fix main module to handle existing security groups feat(core): add Unicode support The main Terraform module has been updated to allow adding existing security groups without creating a default one. This fixes an issue where the module was creating a new security group even when one was already provided. Additionally, the core module has been enhanced to provide Unicode support, enabling expanded character set handling. Other minor fixes and cleanup. --- .../perforce/helix-core/p4_configure.sh | 51 ++++++++----------- modules/perforce/helix-core/main.tf | 7 +-- modules/perforce/helix-core/outputs.tf | 6 +-- modules/perforce/helix-core/variables.tf | 5 -- 4 files changed, 25 insertions(+), 44 deletions(-) diff --git a/assets/packer/perforce/helix-core/p4_configure.sh b/assets/packer/perforce/helix-core/p4_configure.sh index 7af6311b..aacd09db 100644 --- a/assets/packer/perforce/helix-core/p4_configure.sh +++ b/assets/packer/perforce/helix-core/p4_configure.sh @@ -126,10 +126,10 @@ prepare_site_tags() { set_unicode() { log_message "Setting unicode flag for p4d." log_message "sourcing p4_vars" - + # Capture the command output output=$(su - perforce -c "source /p4/common/bin/p4_vars && /p4/common/bin/p4d -xi" 2>&1) - + # Check if the output matches exactly what we expect if [ "$output" = "Server switched to Unicode mode." ]; then log_message "Successfully switched server to Unicode mode" @@ -140,12 +140,6 @@ set_unicode() { fi } -set_selinux() { - # update label for SELinux -> This is optional as by default in some operating systems like Amazon Linux SELinux is disabled - Permissive - semanage fcontext -a -t bin_t /p4/1/bin/p4d_1_init - restorecon -vF /p4/1/bin/p4d_1_init -} - # Starting the script log_message "Starting the p4 configure script." @@ -162,8 +156,7 @@ print_help() { echo " --hx_metadata Path for Helix Core metadata" echo " --hx_depots Path for Helix Core depots" echo " --case_sensitive <0/1> Set the case sensitivity of the Helix Core server" - echo " --unicode Set the Helix Core Server with -xi flag for Unicode" - echo " --selinux Update labels for SELinux" + echo " --unicode Set the Helix Core Server with -xi flag for Unicode" echo " --help Display this help and exit" } @@ -230,22 +223,12 @@ while true; do ;; --unicode) if [ "${2,,}" = "true" ] || [ "${2,,}" = "false" ]; then - UNICODE="$2" - log_message "UNICODE: $UNICODE" - shift 2 + UNICODE="$2" + log_message "UNICODE: $UNICODE" + shift 2 else - log_message "Error: --unicode flag must be either 'true' or 'false'" - exit 1 - fi - ;; - --selinux) - if [ "${2,,}" = "true" ] || [ "${2,,}" = "false" ]; then - SELINUX="$2" - log_message "SELINUX: $SELINUX" - shift 2 - else - log_message "Error: --selinux flag must be either 'true' or 'false'" - exit 1 + log_message "Error: --unicode flag must be either 'true' or 'false'" + exit 1 fi ;; --help) @@ -457,12 +440,10 @@ sed -e "s:__INSTANCE__:$I:g" -e "s:__OSUSER__:perforce:g" $SDP/Server/Unix/p4/co chmod 644 p4d_${I}.service systemctl daemon-reload -if [ "${SELINUX,,}" = "true" ]; then - set_selinux - log_message "SELinux labels updated" -elif [ "${SELINUX,,}" = "false" ]; then - log_message "Skipping SELinux label update" -fi + +# update label for selinux -> This should be optional as by deualt in Amazon Linux selinux is disabled - Permissive +semanage fcontext -a -t bin_t /p4/1/bin/p4d_1_init +restorecon -vF /p4/1/bin/p4d_1_init # start service systemctl start p4d_1 @@ -546,6 +527,14 @@ elif [ "${UNICODE,,}" = "false" ]; then fi +if [ "${UNICODE,,}" = "true" ]; then + set_unicode + log_message "Unicode configuration applied" +elif [ "${UNICODE,,}" = "false" ]; then + log_message "Skipping Unicode configuration" +fi + + # Create the flag file to prevent re-run touch "$FLAG_FILE" diff --git a/modules/perforce/helix-core/main.tf b/modules/perforce/helix-core/main.tf index acea33c4..2c320fdc 100644 --- a/modules/perforce/helix-core/main.tf +++ b/modules/perforce/helix-core/main.tf @@ -44,14 +44,11 @@ resource "aws_instance" "helix_core_instance" { ${var.fully_qualified_domain_name == null ? "" : "--fqdn ${var.fully_qualified_domain_name}"} \ ${var.helix_authentication_service_url == null ? "" : "--auth ${var.helix_authentication_service_url}"} \ --case_sensitive ${var.helix_case_sensitive ? 1 : 0} \ - --unicode ${var.unicode ? "true" : "false"} \ - --selinux ${var.selinux ? "true" : "false"} + --unicode ${var.unicode ? "true" : "false"} EOT - #vpc_security_group_ids = var.create_default_sg ? concat(var.existing_security_groups, [aws_security_group.helix_core_security_group[0].id]) : var.existing_security_groups - - vpc_security_group_ids = [aws_security_group.helix_core_security_group[each.key].id] + vpc_security_group_ids = var.create_default_sg ? concat(var.existing_security_groups, [aws_security_group.helix_core_security_group[0].id]) : var.existing_security_groups metadata_options { http_endpoint = "enabled" diff --git a/modules/perforce/helix-core/outputs.tf b/modules/perforce/helix-core/outputs.tf index ddfdf0d3..e8d2e455 100644 --- a/modules/perforce/helix-core/outputs.tf +++ b/modules/perforce/helix-core/outputs.tf @@ -13,9 +13,9 @@ output "helix_core_eip_ids" { description = "Map of server types to their Elastic IP IDs." } -output "security_group_ids" { - value = { for k, v in aws_security_group.helix_core_security_group : k => v.id } - description = "Map of server types to their security group IDs." +output "security_group_id" { + value = var.create_default_sg ? aws_security_group.helix_core_security_group[0].id : null + description = "The default security group of your Helix Core instance." } output "helix_core_super_user_username_secret_arn" { diff --git a/modules/perforce/helix-core/variables.tf b/modules/perforce/helix-core/variables.tf index dfdf46bd..c89fed56 100644 --- a/modules/perforce/helix-core/variables.tf +++ b/modules/perforce/helix-core/variables.tf @@ -51,11 +51,6 @@ variable "unicode" { default = false } -variable "selinux" { - type = bool - description = "Whether to apply SELinux label updates for Helix Core. Don't enable this if SELinux is disabled on your target operating system." - default = false -} ######################################## # Networking and Security From 3f2f0adb4f55077345af14f9f9d2bdbfad71e0b0 Mon Sep 17 00:00:00 2001 From: Greg Ochmanski Date: Thu, 7 Nov 2024 18:03:59 +0100 Subject: [PATCH 6/8] feat(perforce): add SSM document for Helix Core configuration - Create new SSM module for applying Ansible playbooks - Update p4_configure_playbook.yml with improved installation steps - Modify p4_configure.sh remove unecessary selinux commands This commit introduces an SSM document to streamline the deployment of Perforce Helix Core on EC2 instances using Ansible playbooks. The configuration process is now more automated and consistent across deployments. --- assets/packer/perforce/helix-core/p4_configure.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/packer/perforce/helix-core/p4_configure.sh b/assets/packer/perforce/helix-core/p4_configure.sh index aacd09db..d758717d 100644 --- a/assets/packer/perforce/helix-core/p4_configure.sh +++ b/assets/packer/perforce/helix-core/p4_configure.sh @@ -441,9 +441,9 @@ chmod 644 p4d_${I}.service systemctl daemon-reload -# update label for selinux -> This should be optional as by deualt in Amazon Linux selinux is disabled - Permissive -semanage fcontext -a -t bin_t /p4/1/bin/p4d_1_init -restorecon -vF /p4/1/bin/p4d_1_init +# update label for selinux -> This should be optional as by defualt in Amazon Linux selinux is disabled - Permissive +# semanage fcontext -a -t bin_t /p4/1/bin/p4d_1_init +# restorecon -vF /p4/1/bin/p4d_1_init # start service systemctl start p4d_1 From 1a47dc4c04f69c0499c96babe059904f032d99a1 Mon Sep 17 00:00:00 2001 From: Greg Ochmanski Date: Wed, 4 Dec 2024 21:07:02 +0100 Subject: [PATCH 7/8] feat(infrastructure): add Helix Core topology map to parameter store - Update main.tf - Update locals.tf --- .../helix-core/p4_configure_playbook.yml | 210 +++++++++++++++++- modules/perforce/helix-core/locals.tf | 28 +++ modules/perforce/helix-core/main.tf | 34 ++- 3 files changed, 250 insertions(+), 22 deletions(-) diff --git a/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml b/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml index e2f1771e..01d7a923 100644 --- a/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml +++ b/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml @@ -17,8 +17,28 @@ perforce_ftp_base_url: "https://ftp.perforce.com/perforce" stage_bin_dir: "/hxdepots/sdp/helix_binaries" download_apis: false + flag_file: "/var/run/p4_configure_ran.flag" + p4d_type: "{{ p4d_type | default('p4d_master') }}" + p4d_admin_username_secret_id: "{{ p4d_admin_username_secret_id }}" + p4d_admin_pass_secret_id: "{{ p4d_admin_pass_secret_id }}" + helix_auth_service_url: "{{ helix_auth_service_url | default('') }}" + fqdn: "{{ fqdn | default('') }}" + ebs_logs: "{{ ebs_logs }}" + ebs_metadata: "{{ ebs_metadata }}" + ebs_depots: "{{ ebs_depots }}" + case_sensitive: "{{ case_sensitive | default('1') }}" + unicode: "{{ unicode | default('false') }}" tasks: + - name: Check if script has already run + stat: + path: "{{ flag_file }}" + register: flag_file_stat + + - name: Exit if script has already run + meta: end_play + when: flag_file_stat.stat.exists + - name: Ensure running as root fail: msg: "This playbook must be run as root" @@ -101,11 +121,11 @@ - name: Set platform for each binary set_fact: platform: >- - {%- if os_name == 'linux' -%} - linux26{{ 'x86_64' if os_arch == 'x86_64' else '' }} - {%- else -%} - {{ os_name }}{{ '64' if os_arch == 'x86_64' else '' }} - {%- endif -%} + {%- if os_name == 'linux' -%} + linux26{{ 'x86_64' if os_arch == 'x86_64' else '' }} + {%- else -%} + {{ os_name }}{{ '64' if os_arch == 'x86_64' else '' }} + {%- endif -%} - name: Download binaries get_url: @@ -118,7 +138,6 @@ delay: 5 until: download_result is success - - name: Get binary versions command: "{{ stage_bin_dir }}/{{ item }} -V" register: version_info @@ -168,10 +187,179 @@ owner: perforce group: perforce - - name: Log completion + - name: Resolve AWS secrets + command: aws secretsmanager get-secret-value --secret-id {{ item.secret_id }} --query SecretString --output text + register: aws_secrets + loop: + - { secret_id: "{{ p4d_admin_username_secret_id }}", var_name: "p4d_admin_username" } + - { secret_id: "{{ p4d_admin_pass_secret_id }}", var_name: "p4d_admin_pass" } + no_log: true + + - name: Set fact for resolved secrets + set_fact: + "{{ item.item.var_name }}": "{{ item.stdout }}" + loop: "{{ aws_secrets.results }}" + no_log: true + + - name: Prepare EBS volumes or mount FSx + block: + - name: Create temporary mount points + file: + path: "{{ item }}" + state: directory + loop: + - /mnt/temp_hxlogs + - /mnt/temp_hxmetadata + - /mnt/temp_hxdepots + + - name: Mount EBS volumes or FSx + mount: + path: "{{ item.path }}" + src: "{{ item.src }}" + fstype: "{{ 'nfs' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'xfs' }}" + opts: "{{ 'nconnect=16,rsize=1048576,wsize=1048576,timeo=600' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'defaults' }}" + state: mounted + loop: + - { path: "/mnt/temp_hxlogs", src: "{{ ebs_logs }}" } + - { path: "/mnt/temp_hxmetadata", src: "{{ ebs_metadata }}" } + - { path: "/mnt/temp_hxdepots", src: "{{ ebs_depots }}" } + + - name: Sync directories + synchronize: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + loop: + - { src: "/hxlogs/", dest: "/mnt/temp_hxlogs/" } + - { src: "/hxmetadata/", dest: "/mnt/temp_hxmetadata/" } + - { src: "/hxdepots/", dest: "/mnt/temp_hxdepots/" } + + - name: Unmount temporary mounts + mount: + path: "{{ item }}" + state: unmounted + loop: + - /mnt/temp_hxlogs + - /mnt/temp_hxmetadata + - /mnt/temp_hxdepots + + - name: Clear destination directories + file: + path: "{{ item }}/*" + state: absent + loop: + - /hxlogs + - /hxmetadata + - /hxdepots + + - name: Mount EBS volumes or FSx to final destinations + mount: + path: "{{ item.path }}" + src: "{{ item.src }}" + fstype: "{{ 'nfs' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'xfs' }}" + opts: "{{ 'nconnect=16,rsize=1048576,wsize=1048576,timeo=600' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'defaults' }}" + state: mounted + loop: + - { path: "/hxlogs", src: "{{ ebs_logs }}" } + - { path: "/hxmetadata", src: "{{ ebs_metadata }}" } + - { path: "/hxdepots", src: "{{ ebs_depots }}" } + + - name: Update mkdirs.cfg + lineinfile: + path: /hxdepots/sdp/Server/Unix/setup/mkdirs.cfg + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + loop: + - { regexp: '^P4ADMINPASS=', line: 'P4ADMINPASS={{ p4d_admin_pass }}' } + - { regexp: '^ADMINUSER=', line: 'ADMINUSER={{ p4d_admin_username }}' } + - { regexp: '^P4MASTERHOST=', line: 'P4MASTERHOST={{ ansible_hostname }}' } + - { regexp: '^CASE_SENSITIVE=', line: 'CASE_SENSITIVE=CASE_SENSITIVE' } + + - name: Execute mkdirs.sh + command: /hxdepots/sdp/Server/Unix/setup/mkdirs.sh 1 -t {{ p4d_type }} + args: + chdir: /hxdepots/sdp/Server/Unix/setup + + - name: Update SSL config with EC2 DNS name lineinfile: - path: "{{ log_file }}" - line: "{{ ansible_date_time.iso8601 }} - Perforce installation and configuration completed" - create: yes + path: /p4/ssl/config.txt + regexp: '^REPL_DNSNAME' + line: "{{ fqdn | default(ansible_ec2_public_hostname) }}" + + - name: Generate SSL certificate + command: /p4/common/bin/p4master_run 1 /p4/1/bin/p4d_1 -Gc + + - name: Configure systemd service + template: + src: "{{ sdp }}/Server/Unix/p4/common/etc/systemd/system/p4d_N.service.t" + dest: /etc/systemd/system/p4d_1.service + owner: root + group: root + mode: '0644' + + - name: Reload systemd + systemd: + daemon_reload: yes + + - name: Start p4d service + systemd: + name: p4d_1 + state: started + + - name: Wait for p4d service to start + wait_for: + port: 1666 + timeout: 300 + + - name: Create symlink for p4 binary + file: + src: "{{ sdp_root }}/p4" + dest: /usr/bin/p4 + state: link + + - name: Trust p4 server + command: p4 -p ssl:{{ ansible_hostname }}:1666 trust -y + + - name: Execute configure_new_server.sh + command: /p4/sdp/Server/setup/configure_new_server.sh 1 + + - name: Execute live_checkpoint.sh + command: /p4/sdp/Server/Unix/p4/common/bin/live_checkpoint.sh 1 + become: yes + become_user: perforce + + - name: Execute recreate_offline_db.sh + command: /p4/sdp/Server/Unix/p4/common/bin/recreate_offline_db.sh 1 + become: yes + become_user: perforce + + - name: Fix crontab + lineinfile: + path: /p4/p4.crontab.1 + regexp: '\*/60' + line: '0' + + - name: Initialize crontab for perforce user + cron: + name: "Perforce cron jobs" + user: perforce + cron_file: /p4/p4.crontab.1 + + - name: Verify SDP installation + command: /hxdepots/p4/common/bin/verify_sdp.sh 1 + + - name: Prepare SiteTags + block: + - name: Get AWS region + command: curl -s http://169.254.169.254/latest/meta-data/placement/region + register: aws_region + + - name: Create SiteTags file + template: + src: /hxdepots/sdp/Server/Unix/p4/common/config/SiteTags.cfg.sample + dest: /hxdepots/p4/common/config/SiteTags.cfg - + - name: Append AWS region to SiteTags + lineinfile: + path: /hxdepots/p4/common/config/SiteTags.cfg + line: "aws{{ aws_region.stdout | replace('-', '') } + \ No newline at end of file diff --git a/modules/perforce/helix-core/locals.tf b/modules/perforce/helix-core/locals.tf index 5c32b099..d3916ded 100644 --- a/modules/perforce/helix-core/locals.tf +++ b/modules/perforce/helix-core/locals.tf @@ -24,5 +24,33 @@ locals { } server_private_ips = {for k, v in aws_instance.helix_core_instance : k => v.private_ip} + +########################################## +# Perforce Helix Core Instance Topology Structure +########################################## + + topology = { + version = formatdate("YYYYMMDDhhmmss", timestamp()) + servers = { + for server_type, instance in aws_instance.helix_core_instance : + server_type => { + role = server_type + private_dns = instance.private_dns + private_ip = instance.private_ip + public_ip = var.internal ? null : try(aws_eip.helix_core_eip[server_type].public_ip, null) + instance_id = instance.id + subnet_id = instance.subnet_id + vpc_id = var.server_configuration[index(var.server_configuration.*.type, server_type)].vpc_id + } + } + connections = [ + for server_type, instance in aws_instance.helix_core_instance : + { + from = instance.private_dns + to = aws_instance.helix_core_instance["commit"].private_dns + } + if server_type != "commit" + ] + } } diff --git a/modules/perforce/helix-core/main.tf b/modules/perforce/helix-core/main.tf index 2c320fdc..afd195e2 100644 --- a/modules/perforce/helix-core/main.tf +++ b/modules/perforce/helix-core/main.tf @@ -35,17 +35,17 @@ resource "aws_instance" "helix_core_instance" { iam_instance_profile = aws_iam_instance_profile.helix_core_instance_profile.id - user_data = <<-EOT - #!/bin/bash - /home/ec2-user/gpic_scripts/p4_configure.sh --hx_logs /dev/sdf --hx_metadata /dev/sdg --hx_depots /dev/sdh \ - --p4d_type ${var.server_type} \ - --username ${var.helix_core_super_user_username_secret_arn == null ? awscc_secretsmanager_secret.helix_core_super_user_username[0].secret_id : var.helix_core_super_user_username_secret_arn} \ - --password ${var.helix_core_super_user_password_secret_arn == null ? awscc_secretsmanager_secret.helix_core_super_user_password[0].secret_id : var.helix_core_super_user_password_secret_arn} \ - ${var.fully_qualified_domain_name == null ? "" : "--fqdn ${var.fully_qualified_domain_name}"} \ - ${var.helix_authentication_service_url == null ? "" : "--auth ${var.helix_authentication_service_url}"} \ - --case_sensitive ${var.helix_case_sensitive ? 1 : 0} \ - --unicode ${var.unicode ? "true" : "false"} - EOT + # user_data = <<-EOT + # #!/bin/bash + # /home/ec2-user/gpic_scripts/p4_configure.sh --hx_logs /dev/sdf --hx_metadata /dev/sdg --hx_depots /dev/sdh \ + # --p4d_type ${var.server_type} \ + # --username ${var.helix_core_super_user_username_secret_arn == null ? awscc_secretsmanager_secret.helix_core_super_user_username[0].secret_id : var.helix_core_super_user_username_secret_arn} \ + # --password ${var.helix_core_super_user_password_secret_arn == null ? awscc_secretsmanager_secret.helix_core_super_user_password[0].secret_id : var.helix_core_super_user_password_secret_arn} \ + # ${var.fully_qualified_domain_name == null ? "" : "--fqdn ${var.fully_qualified_domain_name}"} \ + # ${var.helix_authentication_service_url == null ? "" : "--auth ${var.helix_authentication_service_url}"} \ + # --case_sensitive ${var.helix_case_sensitive ? 1 : 0} \ + # --unicode ${var.unicode ? "true" : "false"} + # EOT vpc_security_group_ids = var.create_default_sg ? concat(var.existing_security_groups, [aws_security_group.helix_core_security_group[0].id]) : var.existing_security_groups @@ -188,3 +188,15 @@ resource "aws_ssm_parameter" "server_info" { tags = local.tags } +resource "aws_ssm_parameter" "helix_core_topology" { + name = "/${var.project_prefix}/${var.environment}/perforce/topology" + type = "String" + value = jsonencode(local.topology) + + description = "Perforce Helix Core Server Topology" + + tags = merge(local.tags, { + Name = "${local.name_prefix}-topology" + }) +} + From 37eead0dd96a08729e58838a01aada87f823e26f Mon Sep 17 00:00:00 2001 From: Greg Ochmanski Date: Tue, 7 Jan 2025 18:22:32 +0100 Subject: [PATCH 8/8] feat!: update Perforce EC2 image and add replica support - Change default EC2 image for Perforce to Amazon Linux 2023 - Add additional support for Perforce replica BREAKING CHANGE: The switch to Amazon Linux 2023 may cause compatibility issues with existing setups or scripts tailored for the previous custom image. --- .../helix-core/p4_configure_playbook.yml | 248 ++++++++++++++---- modules/perforce/examples/complete/main.tf | 5 + modules/perforce/helix-core/data.tf | 6 +- 3 files changed, 207 insertions(+), 52 deletions(-) diff --git a/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml b/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml index 01d7a923..258392aa 100644 --- a/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml +++ b/assets/ansible-playbooks/perforce/helix-core/p4_configure_playbook.yml @@ -2,6 +2,8 @@ - name: Install and Configure Perforce hosts: all become: yes + collections: + - amazon.aws vars: log_file: "/var/log/p4_setup.log" sdp_root: "/hxdepots/sdp/helix_binaries" @@ -28,6 +30,7 @@ ebs_depots: "{{ ebs_depots }}" case_sensitive: "{{ case_sensitive | default('1') }}" unicode: "{{ unicode | default('false') }}" + topology_file: "/etc/perforce/topology.json" tasks: - name: Check if script has already run @@ -44,6 +47,96 @@ msg: "This playbook must be run as root" when: ansible_user_id != "root" + - name: Ensure required directories exist + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "/etc/perforce" + - "/var/log/perforce" + + - name: Fetch topology from AWS Parameter Store + set_fact: + ssm_topology: "{{ lookup('amazon.aws.ssm_parameter', '/{{ lookup('env', 'PROJECT_PREFIX') }}/{{ lookup('env', 'ENVIRONMENT') }}/perforce/topology') }}" + + + - name: Parse fetched topology + set_fact: + new_topology: "{{ ssm_topology }}" + + - name: Check if local topology file was previously written + stat: + path: "{{ topology_file }}" + register: topology_stat + + - name: Read existing topology if it exists + block: + - name: Read existing topology + slurp: + src: "{{ topology_file }}" + register: existing_topology_raw + + - name: Parse existing topology + set_fact: + existing_topology: "{{ existing_topology_raw['content'] | b64decode | from_json }}" + when: topology_stat.stat.exists + + - name: Compare topologies and update if necessary + block: + - name: Check if topology has changed + set_fact: + topology_changed: "{{ new_topology.version != existing_topology.version }}" + + - name: Log topology change + lineinfile: + path: "{{ log_file }}" + line: "{{ ansible_date_time.iso8601 }} - Topology change detected. New version: {{ new_topology.version }}" + create: yes + when: topology_changed + + - name: Update local topology file + copy: + content: "{{ new_topology | to_nice_json }}" + dest: "{{ topology_file }}" + when: topology_changed + when: topology_stat.stat.exists + + - name: Store new topology if it doesn't exist locally + copy: + content: "{{ new_topology | to_nice_json }}" + dest: "{{ topology_file }}" + when: not topology_stat.stat.exists + + - name: Fetch instance metadata + ec2_metadata_facts: + + - name: Check if ServerType tag exists + set_fact: + server_type_tag_exists: "{{ 'ServerType' in ansible_ec2_instance_tags_keys }}" + + - name: Debug ServerType tag existence + debug: + var: server_type_tag_exists + + - name: Set facts based on instance metadata + set_fact: + server_type: "{{ 'ServerType' in ansible_ec2_instance_tags_keys }}" + + - name: Set server-specific facts + set_fact: + commit_server_dns: "{{ new_topology.servers.commit.private_dns }}" + replica_server_dns: "{{ new_topology.servers.replica.private_dns | default('') }}" + edge_server_dns: "{{ new_topology.servers.edge.private_dns | default('') }}" + + - name: Display server information + debug: + msg: + - "Server Type: {{ server_type }}" + - "Commit Server DNS: {{ commit_server_dns }}" + - "Replica Server DNS: {{ replica_server_dns }}" + - "Edge Server DNS: {{ edge_server_dns }}" + - name: Ensure perforce group exists group: name: perforce @@ -212,56 +305,113 @@ - /mnt/temp_hxmetadata - /mnt/temp_hxdepots - - name: Mount EBS volumes or FSx - mount: - path: "{{ item.path }}" - src: "{{ item.src }}" - fstype: "{{ 'nfs' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'xfs' }}" - opts: "{{ 'nconnect=16,rsize=1048576,wsize=1048576,timeo=600' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'defaults' }}" - state: mounted - loop: - - { path: "/mnt/temp_hxlogs", src: "{{ ebs_logs }}" } - - { path: "/mnt/temp_hxmetadata", src: "{{ ebs_metadata }}" } - - { path: "/mnt/temp_hxdepots", src: "{{ ebs_depots }}" } - - - name: Sync directories - synchronize: - src: "{{ item.src }}" - dest: "{{ item.dest }}" - loop: - - { src: "/hxlogs/", dest: "/mnt/temp_hxlogs/" } - - { src: "/hxmetadata/", dest: "/mnt/temp_hxmetadata/" } - - { src: "/hxdepots/", dest: "/mnt/temp_hxdepots/" } + - name: Get device information + set_fact: + device_info: + - path: "/mnt/temp_hxlogs" + src: "{{ ebs_logs }}" + - path: "/mnt/temp_hxmetadata" + src: "{{ ebs_metadata }}" + - path: "/mnt/temp_hxdepots" + src: "{{ ebs_depots }}" + + - name: Identify device types and prepare for mounting + set_fact: + mount_info: "{{ mount_info | default([]) + [{ + 'path': item.path, + 'src': item.src, + 'is_nfs': item.src is regex('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/'), + 'fstype': 'nfs' if (item.src is regex('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/')) else 'xfs', + 'opts': 'nconnect=16,rsize=1048576,wsize=1048576,timeo=600' if (item.src is regex('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/')) else 'defaults' + }] }}" + loop: "{{ device_info }}" + + - name: Debug mount info + debug: + var: mount_info - - name: Unmount temporary mounts - mount: - path: "{{ item }}" - state: unmounted - loop: - - /mnt/temp_hxlogs - - /mnt/temp_hxmetadata - - /mnt/temp_hxdepots + - name: Ensure mount directories exist + file: + path: "{{ item.path }}" + state: directory + mode: '0755' + loop: "{{ mount_info }}" - - name: Clear destination directories - file: - path: "{{ item }}/*" - state: absent - loop: - - /hxlogs - - /hxmetadata - - /hxdepots - - - name: Mount EBS volumes or FSx to final destinations - mount: - path: "{{ item.path }}" - src: "{{ item.src }}" - fstype: "{{ 'nfs' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'xfs' }}" - opts: "{{ 'nconnect=16,rsize=1048576,wsize=1048576,timeo=600' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'defaults' }}" - state: mounted - loop: - - { path: "/hxlogs", src: "{{ ebs_logs }}" } - - { path: "/hxmetadata", src: "{{ ebs_metadata }}" } - - { path: "/hxdepots", src: "{{ ebs_depots }}" } + - name: Get actual device names for EBS volumes + shell: | + lsblk -nlo NAME,SERIAL | grep "{{ item.src | regex_replace('/dev/', '') }}" | awk '{print $1}' + register: actual_device_names + when: not item.is_nfs + loop: "{{ mount_info }}" + + - name: Update mount info with actual device names + set_fact: + mount_info: "{{ mount_info | map('combine', {'actual_src': item.stdout if item.stdout else item.item.src}) | list }}" + loop: "{{ actual_device_names.results }}" + when: not item.skipped | default(false) + + - name: Check if EBS filesystems are formatted + command: "blkid {{ item.actual_src }}" + register: blkid_results + failed_when: false + changed_when: false + when: not item.is_nfs + loop: "{{ mount_info }}" + + - name: Format EBS filesystems if needed + filesystem: + fstype: xfs + dev: "{{ item.item.actual_src }}" + when: not item.item.is_nfs and item.rc != 0 + loop: "{{ blkid_results.results }}" + + - name: Mount volumes or FSx + mount: + path: "{{ item.path }}" + src: "{{ item.actual_src | default(item.src) }}" + fstype: "{{ item.fstype }}" + opts: "{{ item.opts }}" + state: mounted + loop: "{{ mount_info }}" + + - name: Sync directories + synchronize: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + loop: + - { src: "/hxlogs/", dest: "/mnt/temp_hxlogs/" } + - { src: "/hxmetadata/", dest: "/mnt/temp_hxmetadata/" } + - { src: "/hxdepots/", dest: "/mnt/temp_hxdepots/" } + + - name: Unmount temporary mounts + mount: + path: "{{ item }}" + state: unmounted + loop: + - /mnt/temp_hxlogs + - /mnt/temp_hxmetadata + - /mnt/temp_hxdepots + + - name: Clear destination directories + file: + path: "{{ item }}/*" + state: absent + loop: + - /hxlogs + - /hxmetadata + - /hxdepots + + - name: Mount EBS volumes or FSx to final destinations + mount: + path: "{{ item.path }}" + src: "{{ item.src }}" + fstype: "{{ 'nfs' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'xfs' }}" + opts: "{{ 'nconnect=16,rsize=1048576,wsize=1048576,timeo=600' if item.src | regex_search('fs-[0-9a-f]{17}\\.fsx\\.[a-z0-9-]+\\.amazonaws\\.com:/') else 'defaults' }}" + state: mounted + loop: + - { path: "/hxlogs", src: "{{ ebs_logs }}" } + - { path: "/hxmetadata", src: "{{ ebs_metadata }}" } + - { path: "/hxdepots", src: "{{ ebs_depots }}" } - name: Update mkdirs.cfg lineinfile: @@ -361,5 +511,5 @@ - name: Append AWS region to SiteTags lineinfile: path: /hxdepots/p4/common/config/SiteTags.cfg - line: "aws{{ aws_region.stdout | replace('-', '') } + line: "aws{{ aws_region.stdout | replace('-', '') }}" \ No newline at end of file diff --git a/modules/perforce/examples/complete/main.tf b/modules/perforce/examples/complete/main.tf index 58f0aa6f..1e3cb31e 100644 --- a/modules/perforce/examples/complete/main.tf +++ b/modules/perforce/examples/complete/main.tf @@ -45,6 +45,11 @@ module "perforce_helix_core" { type = "replica" vpc_id = aws_vpc.perforce_vpc.id subnet_id = aws_subnet.private_subnets[0].id + }, + { + type = "edge" + vpc_id = aws_vpc.perforce_vpc.id + subnet_id = aws_subnet.private_subnets[0].id } ] diff --git a/modules/perforce/helix-core/data.tf b/modules/perforce/helix-core/data.tf index 73d7d6cd..07b19cc1 100644 --- a/modules/perforce/helix-core/data.tf +++ b/modules/perforce/helix-core/data.tf @@ -11,12 +11,12 @@ data "aws_subnet" "selected" { # Fetching custom Perforce Helix Core AMI data "aws_ami" "helix_core_ami" { most_recent = true - name_regex = "p4_al2023-*" - owners = ["self"] + name_regex = "al2023-ami-2023.5.*" + owners = ["amazon"] filter { name = "name" - values = ["p4_al2023-*"] + values = ["al2023-ami-2023.5.*"] } filter {