├── roles └── oneagent │ ├── tests │ ├── ansible │ │ ├── __init__.py │ │ ├── runner.py │ │ └── config.py │ ├── command │ │ ├── __init__.py │ │ ├── unix │ │ │ ├── __init__.py │ │ │ └── unix_command_wrapper.py │ │ ├── windows │ │ │ ├── __init__.py │ │ │ ├── windows_command_executor.py │ │ │ └── windows_command_wrapper.py │ │ ├── command_result.py │ │ ├── command_wrapper.py │ │ └── platform_command_wrapper.py │ ├── deployment │ │ ├── __init__.py │ │ ├── deployment_platform.py │ │ ├── ssl_certificate_generator.py │ │ ├── installer_fetching.py │ │ └── deployment_operations.py │ ├── resources │ │ ├── __init__.py │ │ ├── ansible │ │ │ ├── credentials.yml │ │ │ ├── oneagent.yml │ │ │ └── hosts.yml │ │ ├── installers │ │ │ ├── uninstall.sh │ │ │ ├── oneagentctl.sh │ │ │ └── Dynatrace-OneAgent-Linux.sh │ │ └── file_operations.py │ ├── installer_server │ │ ├── __init__.py │ │ └── server.py │ ├── requirements.txt │ ├── pytest.ini │ ├── test_local_installer.py │ ├── test_check_mode.py │ ├── constants.py │ ├── README.md │ ├── test_upgrade.py │ ├── test_install_and_config.py │ ├── conftest.py │ └── test_resilience.py │ ├── vars │ ├── version.yml │ ├── win32nt.yml │ ├── main.yml │ ├── aix.yml │ ├── messages.yml │ └── linux.yml │ ├── tasks │ ├── uninstall │ │ ├── uninstall.yml │ │ ├── uninstall-unix.yml │ │ └── uninstall-windows.yml │ ├── cleanup │ │ ├── cleanup.yml │ │ ├── cleanup-windows.yml │ │ └── cleanup-unix.yml │ ├── provide-installer │ │ ├── transfer-windows.yml │ │ ├── transfer-unix.yml │ │ ├── new-installer-info-unix.yml │ │ ├── new-installer-info-windows.yml │ │ ├── download.yml │ │ ├── download-unix.yml │ │ ├── download-windows.yml │ │ ├── signature-unix.yml │ │ └── provide-installer.yml │ ├── params │ │ ├── params-windows.yml │ │ ├── params-unix.yml │ │ └── params.yml │ ├── install │ │ ├── install-check.yml │ │ ├── post-install-unix.yml │ │ ├── post-install-windows.yml │ │ ├── install-unix.yml │ │ ├── install.yml │ │ └── install-windows.yml │ ├── config │ │ ├── config.yml │ │ ├── config-unix.yml │ │ └── config-windows.yml │ ├── gather-info │ │ ├── gather-info-unix.yml │ │ ├── gather-info-windows.yml │ │ └── gather-info.yml │ └── main.yml │ ├── examples │ ├── oneagentctl_config │ │ ├── inventory.yml │ │ └── oneagentctl_config.yml │ ├── local_installer │ │ ├── local_installer.yml │ │ └── inventory.yml │ └── advanced_config │ │ ├── inventory.yml │ │ └── advanced_config.yml │ ├── defaults │ └── main.yml │ └── README.md ├── meta └── runtime.yml ├── pyproject.toml ├── .github ├── pull_request_template.md ├── actions │ ├── make-release │ │ ├── get_current_version.py │ │ ├── parse_changelog.py │ │ └── action.yaml │ ├── get-ansible-versions │ │ └── action.yaml │ ├── download-collection │ │ └── action.yaml │ ├── set-environment-variables │ │ └── action.yaml │ ├── upload-collection │ │ └── action.yaml │ ├── build-collection │ │ └── action.yaml │ ├── update-version │ │ ├── action.yaml │ │ └── update_version.py │ ├── update-changelog │ │ └── action.yaml │ ├── set-up-build-environment │ │ └── action.yaml │ ├── run-sanity-and-linter-tests │ │ └── action.yaml │ ├── get-app-versions │ │ ├── action.yaml │ │ └── eol_client.py │ ├── run-component-tests │ │ └── action.yaml │ └── commit-changes │ │ └── action.yaml └── workflows │ ├── refresh-automation-hub-api-token.yaml │ ├── check-commits.yaml │ ├── release.yaml │ ├── update-version-and-changelog.yaml │ └── build-and-test.yaml ├── .gitignore ├── galaxy.yml ├── git-conventional-commits.yaml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── README.md └── LICENSE /roles/oneagent/tests/ansible/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roles/oneagent/tests/command/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roles/oneagent/tests/deployment/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roles/oneagent/tests/resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roles/oneagent/tests/command/unix/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roles/oneagent/tests/command/windows/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roles/oneagent/tests/installer_server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roles/oneagent/vars/version.yml: -------------------------------------------------------------------------------- 1 | --- 2 | oneagent_script_version: 1.2.5 3 | -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Required version of ansible-core to use the collection 3 | requires_ansible: ">=2.15.0" 4 | -------------------------------------------------------------------------------- /roles/oneagent/tests/resources/ansible/credentials.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_user: "" 3 | ansible_password: "" 4 | ansible_sudo_pass: "{{ ansible_password }}" 5 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/uninstall/uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Uninstall OneAgent 3 | ansible.builtin.include_tasks: tasks/uninstall/uninstall-{{ oneagent_system_family }}.yml 4 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/cleanup/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup installer and leftovers 3 | ansible.builtin.include_tasks: tasks/cleanup/cleanup-{{ oneagent_system_family }}.yml 4 | -------------------------------------------------------------------------------- /roles/oneagent/tests/requirements.txt: -------------------------------------------------------------------------------- 1 | ansible >= 10 2 | Flask == 3.0.3 3 | netifaces ~= 0.11.0 4 | pytest ~= 8.2.1 5 | pywinrm == 0.4.3 6 | PyYAML ~= 6.0.1 7 | typing-extensions ~= 4.13.0 8 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/cleanup/cleanup-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Remove leftover installer 3 | ansible.windows.win_file: 4 | path: "{{ oneagent_installer_path }}" 5 | state: absent 6 | -------------------------------------------------------------------------------- /roles/oneagent/tests/command/command_result.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class CommandResult: 6 | returncode: int 7 | stdout: str 8 | stderr: str 9 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/provide-installer/transfer-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Transfer installer 3 | ansible.windows.win_copy: 4 | src: "{{ oneagent_local_installer }}" 5 | dest: "{{ oneagent_installer_path }}" 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pylint.BASIC] 2 | max-line-length = 120 3 | 4 | [tool.pylint.'MESSAGES CONTROL'] 5 | disable = ''' 6 | missing-docstring, 7 | ''' 8 | 9 | [tool.black] 10 | line-length = 120 11 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/params/params-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Validate if download directory is available 3 | ansible.windows.win_stat: 4 | path: "{{ oneagent_download_path }}" 5 | register: _oneagent_download_path_state 6 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/provide-installer/transfer-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Transfer installer 3 | ansible.builtin.copy: 4 | src: "{{ oneagent_local_installer }}" 5 | dest: "{{ oneagent_installer_path }}" 6 | mode: "0644" 7 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/provide-installer/new-installer-info-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Determine version of new installer 3 | ansible.builtin.command: /bin/sh {{ oneagent_installer_path }} --version 4 | register: _oneagent_new_agent_version 5 | changed_when: false 6 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/provide-installer/new-installer-info-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Determine version of new installer 3 | ansible.windows.win_command: '"{{ oneagent_installer_path }}" --version --quiet' 4 | register: _oneagent_new_agent_version 5 | changed_when: false 6 | -------------------------------------------------------------------------------- /roles/oneagent/tests/resources/installers/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script simulates the basic behavior of the uninstall.sh script 4 | 5 | set -eu 6 | 7 | main() { 8 | rm -rf /opt/dynatrace 9 | } 10 | 11 | ################## 12 | ## SCRIPT START ## 13 | ################## 14 | 15 | main 16 | -------------------------------------------------------------------------------- /roles/oneagent/examples/oneagentctl_config/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | children: 4 | unix: 5 | children: 6 | linux: 7 | children: 8 | linux_other: 9 | hosts: 10 | www.otherhost1.com: 11 | www.otherhost2.com: 12 | vars: 13 | ansible_become: true 14 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/cleanup/cleanup-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Remove leftover signature 3 | ansible.builtin.file: 4 | path: "{{ oneagent_ca_cert_dest_path }}" 5 | state: absent 6 | 7 | - name: Remove leftover installer 8 | ansible.builtin.file: 9 | path: "{{ oneagent_installer_path }}" 10 | state: absent 11 | changed_when: false 12 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/install/install-check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Validate minimal version of installer 3 | ansible.builtin.fail: 4 | msg: "{{ oneagent_version_lower_than_minimal | format(_oneagent_new_agent_version, oneagent_minimal_install_version) }}" 5 | when: _oneagent_new_agent_version is defined and _oneagent_new_agent_version.stdout < oneagent_minimal_install_version 6 | -------------------------------------------------------------------------------- /roles/oneagent/tests/resources/ansible/oneagent.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Basic playbook 3 | hosts: all 4 | gather_subset: 5 | - "!hardware" # may fail on Windows VMs 6 | vars_files: 7 | - credentials.yml 8 | vars: {} 9 | tasks: 10 | - name: Import Dynatrace OneAgent role 11 | ansible.builtin.import_role: 12 | name: dynatrace.oneagent.oneagent 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | 4 | 5 | ## Please follow our general PR guidelines: 6 | 7 | - [ ] Make sure that changes are grouped in a sensible way 8 | - [ ] Make sure that documentation is changed accordignly 9 | - [ ] Make sure that commits follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification 10 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/config/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Combine configuration parameters 3 | ansible.builtin.set_fact: 4 | _oneagent_all_config_args: "{{ oneagent_passed_install_args | map('regex_search', '(--set-(.*))') | select('string') | list + ['--restart-service'] }}" 5 | no_log: true 6 | 7 | - name: Apply OneAgent configuration 8 | ansible.builtin.include_tasks: config-{{ oneagent_system_family }}.yml 9 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/gather-info/gather-info-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if OneAgent is installed 3 | ansible.builtin.stat: 4 | path: "{{ oneagent_ctl_bin_path }}" 5 | register: _oneagent_ctl_state 6 | 7 | - name: Determine version of installed OneAgent 8 | ansible.builtin.command: "{{ oneagent_ctl_bin_path }} --version" 9 | register: _oneagent_installed_agent_version 10 | when: _oneagent_ctl_state.stat.exists 11 | changed_when: false 12 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/install/post-install-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reboot the host 3 | ansible.builtin.reboot: 4 | reboot_timeout: oneagent_reboot_timeout 5 | register: _oneagent_reboot_status 6 | when: oneagent_reboot_host | bool 7 | 8 | - name: Check the host reboot status 9 | ansible.builtin.fail: 10 | msg: "{{ oneagent_reboot_failed }}" 11 | when: oneagent_reboot_host | bool and not _oneagent_reboot_status.rebooted|default(false) 12 | -------------------------------------------------------------------------------- /roles/oneagent/examples/local_installer/local_installer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: > 3 | Basic OneAgent installation using a local installer. Hosts placed in unix hosts groups have its local installer paths defined in inventory file. 4 | Main node communicates with Windows hosts over SSH. 5 | hosts: windows,unix 6 | tasks: 7 | - name: Import Dynatrace OneAgent role 8 | ansible.builtin.import_role: 9 | name: dynatrace.oneagent.oneagent 10 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/install/post-install-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reboot the host 3 | ansible.windows.win_reboot: 4 | reboot_timeout: oneagent_reboot_timeout 5 | register: _oneagent_reboot_status 6 | when: oneagent_reboot_host | bool 7 | 8 | - name: Check the host reboot status 9 | ansible.builtin.fail: 10 | msg: "{{ oneagent_reboot_failed }}" 11 | when: oneagent_reboot_host | bool and not _oneagent_reboot_status.rebooted|default(false) 12 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/gather-info/gather-info-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if OneAgent is installed 3 | ansible.windows.win_stat: 4 | path: "{{ oneagent_ctl_bin_path }}" 5 | register: _oneagent_ctl_state 6 | 7 | - name: Determine version of installed OneAgent 8 | ansible.windows.win_command: '"{{ oneagent_ctl_bin_path }}" --version' 9 | register: _oneagent_installed_agent_version 10 | when: _oneagent_ctl_state.stat.exists 11 | changed_when: false 12 | -------------------------------------------------------------------------------- /roles/oneagent/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = . 3 | 4 | dynatrace_ca_cert_url = https://ca.dynatrace.com/dt-root.cert.pem 5 | 6 | filterwarnings = ignore::urllib3.exceptions.InsecureRequestWarning 7 | 8 | log_cli = true 9 | log_cli_level = INFO 10 | log_cli_format = %(asctime)s %(levelname)s %(message)s 11 | log_cli_date_format = %Y-%m-%d %H:%M:%S 12 | 13 | log_file_level = INFO 14 | log_file_format = %(asctime)s %(levelname)s %(message)s 15 | log_file_date_format = %Y-%m-%d %H:%M:%S 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Installer logs 7 | pip-log.txt 8 | pip-delete-this-directory.txt 9 | 10 | # Environments 11 | .env 12 | .venv 13 | env/ 14 | venv/ 15 | ENV/ 16 | env.bak/ 17 | venv.bak/ 18 | 19 | # Flask 20 | instance/ 21 | .webassets-cache 22 | 23 | # Ansible 24 | *.retry 25 | dynatrace-oneagent-*.tar.gz 26 | 27 | # JetBrains IDE 28 | .idea 29 | 30 | # Tests working directory 31 | ansible_oneagent_tests_run_dir/ 32 | -------------------------------------------------------------------------------- /roles/oneagent/examples/oneagentctl_config/oneagentctl_config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Apply host level configuration with oneagentctl 3 | hosts: linux_other 4 | vars: 5 | oneagent_install_args: 6 | - --set-host-name=new_host_name 7 | - --set-host-tag=new_tag 8 | - --set-host-property=property1 9 | - --set-host-property=other=property 10 | tasks: 11 | - name: Import Dynatrace OneAgent role 12 | ansible.builtin.import_role: 13 | name: dynatrace.oneagent.oneagent 14 | -------------------------------------------------------------------------------- /.github/actions/make-release/get_current_version.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | import argparse 4 | from pathlib import Path 5 | 6 | def get_version(input_file: Path) -> str: 7 | with open("galaxy.yml", "r", encoding="utf-8") as f: 8 | data = yaml.safe_load(f) 9 | return data.get("version") 10 | 11 | if __name__ == "__main__": 12 | parser = argparse.ArgumentParser(description="Parse changelog version") 13 | parser.add_argument("input_file", help="Path to input file") 14 | args = parser.parse_args() 15 | 16 | version = get_version(args.input_file) 17 | print(version) 18 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/install/install-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install OneAgent 3 | ansible.builtin.command: "{{ oneagent_install_cmd }} {{ _oneagent_all_install_args | join(' ') }}" 4 | no_log: "{{ oneagent_no_log }}" 5 | ignore_errors: true 6 | register: _oneagent_install_result 7 | changed_when: true 8 | 9 | - name: Verify installation result 10 | ansible.builtin.fail: 11 | msg: "{{ oneagent_installation_failed_unix | format(_oneagent_install_result.rc, _oneagent_install_result.stdout, _oneagent_install_result.stderr) }}" 12 | when: _oneagent_install_result.failed|default(false) 13 | -------------------------------------------------------------------------------- /roles/oneagent/tests/command/windows/windows_command_executor.py: -------------------------------------------------------------------------------- 1 | import winrm 2 | from command.command_result import CommandResult 3 | 4 | 5 | class WindowsCommandExecutor: 6 | def __init__(self, user: str, password: str): 7 | self.user: str = user 8 | self.password: str = password 9 | 10 | def execute(self, address: str, command: str, *args: str) -> CommandResult: 11 | session = winrm.Session(address, auth=(self.user, self.password)) 12 | out = session.run_cmd(command, args) 13 | return CommandResult(out.status_code, out.std_out.decode("utf-8"), out.std_err.decode("utf-8")) 14 | -------------------------------------------------------------------------------- /.github/actions/get-ansible-versions/action.yaml: -------------------------------------------------------------------------------- 1 | name: Get Ansible Versions 2 | 3 | description: "Fetches supported and latest Ansible versions" 4 | 5 | outputs: 6 | supported_ansible_versions: 7 | description: "List of supported Ansible versions" 8 | value: ${{ steps.get.outputs.supported_versions }} 9 | latest_ansible_version: 10 | description: "Latest Ansible version" 11 | value: ${{ steps.get.outputs.latest_version }} 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | - name: Get Ansible versions 17 | id: get 18 | uses: ./.github/actions/get-app-versions 19 | with: 20 | app: ansible 21 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/install/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check installers info 3 | ansible.builtin.include_tasks: install/install-check.yml 4 | 5 | - name: Combine installation parameters 6 | ansible.builtin.set_fact: 7 | _oneagent_all_install_args: "{{ (oneagent_passed_install_args + oneagent_additional_reporting_params) | map('regex_replace', '(.*)', '\"\\1\"') | list }}" 8 | no_log: "{{ oneagent_no_log }}" 9 | 10 | - name: Install OneAgent 11 | ansible.builtin.include_tasks: install/install-{{ oneagent_system_family }}.yml 12 | - name: Post-installation steps 13 | ansible.builtin.include_tasks: install/post-install-{{ oneagent_system_family }}.yml 14 | -------------------------------------------------------------------------------- /roles/oneagent/tests/resources/file_operations.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import cast 3 | 4 | import yaml 5 | 6 | YamlScalar = str | int | float | bool | None 7 | YamlValue = YamlScalar | dict[str, YamlScalar] | list[YamlScalar] 8 | ParsedYaml = dict[str, YamlValue] | list[YamlValue] | YamlScalar 9 | 10 | 11 | def read_yaml_file(file: Path) -> ParsedYaml: 12 | with file.open("r") as config: 13 | data = cast(ParsedYaml, yaml.safe_load(config)) 14 | return data 15 | 16 | 17 | def write_yaml_file(file: Path, data: ParsedYaml) -> None: 18 | with file.open("w") as config: 19 | _unused = config.write(yaml.dump(data)) 20 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/params/params-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Validate installation directory name doesn't contains spaces 3 | ansible.builtin.fail: 4 | msg: "{{ oneagent_install_dir_contains_spaces }}" 5 | when: "' ' in oneagent_install_path" 6 | 7 | - name: Validate download directory name doesn't contain spaces 8 | ansible.builtin.fail: 9 | msg: "{{ oneagent_download_dir_contains_spaces }}" 10 | when: "oneagent_is_operation_installation and ' ' in oneagent_download_path" 11 | 12 | - name: Validate if download directory is available 13 | ansible.builtin.stat: 14 | path: "{{ oneagent_download_path }}" 15 | register: _oneagent_download_path_state 16 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: dynatrace 3 | name: oneagent 4 | version: 1.2.5 5 | readme: README.md 6 | authors: 7 | - Dynatrace LLC 8 | description: Module to install and configure Dynatrace OneAgent deployment 9 | license_file: LICENSE 10 | tags: 11 | - dynatrace 12 | - oneagent 13 | - agent 14 | - deployment 15 | - monitoring 16 | - infrastructure 17 | - linux 18 | - windows 19 | repository: https://github.com/Dynatrace/Dynatrace-OneAgent-Ansible 20 | documentation: https://docs.dynatrace.com/docs/setup-and-configuration/dynatrace-oneagent/deployment-orchestration/ansible 21 | build_ignore: 22 | - .* 23 | - CHANGELOG.md 24 | - git-conventional-commits.yaml 25 | - pyproject.toml 26 | - venv 27 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/install/install-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Dynatrace OneAgent 3 | ansible.windows.win_package: 4 | path: "{{ oneagent_installer_path }}" 5 | creates_path: "{{ oneagent_ctl_bin_path }}" 6 | creates_version: "{{ _oneagent_new_agent_version.stdout }}" 7 | arguments: "{{ _oneagent_all_install_args | join(' ') }} --quiet" 8 | state: present 9 | no_log: "{{ oneagent_no_log }}" 10 | ignore_errors: true 11 | register: _oneagent_install_result 12 | 13 | - name: Verify installation result 14 | ansible.builtin.fail: 15 | msg: "{{ oneagent_installation_failed_windows | format(_oneagent_install_result.rc) }}" 16 | when: _oneagent_install_result.failed|default(false) 17 | -------------------------------------------------------------------------------- /roles/oneagent/examples/advanced_config/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | children: 4 | unix: 5 | children: 6 | linux: 7 | children: 8 | linux_other: 9 | hosts: 10 | www.otherhost1.com: 11 | www.otherhost2.com: 12 | vars: 13 | oneagent_installer_arch: x86 14 | oneagent_platform_install_args: 15 | - USER=other_user 16 | linux_arm: 17 | hosts: arm_host.com 18 | vars: 19 | oneagent_installer_arch: arm 20 | oneagent_platform_install_args: 21 | - USER=arm_user 22 | vars: 23 | ansible_become: true 24 | -------------------------------------------------------------------------------- /git-conventional-commits.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | convention: 3 | commitTypes: 4 | - feat 5 | - fix 6 | - refactor 7 | - perf 8 | - style 9 | - test 10 | - build 11 | - docs 12 | - chore 13 | - ci 14 | - merge 15 | - revert 16 | commitScopes: [] 17 | releaseTagGlobPattern: 'v[0-9]*.[0-9]*.[0-9]*' 18 | changelog: 19 | commitTypes: 20 | - feat 21 | - fix 22 | - perf 23 | includeInvalidCommits: false 24 | commitIgnoreRegexPattern: '^WIP ' 25 | headlines: 26 | feat: Features 27 | fix: Bug Fixes 28 | perf: Performance Improvements 29 | merge: Merges 30 | breakingChange: BREAKING CHANGES 31 | -------------------------------------------------------------------------------- /roles/oneagent/tests/resources/ansible/hosts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | children: 4 | unix: 5 | children: 6 | aix_ppc: 7 | hosts: {} 8 | vars: {} 9 | linux_arm: 10 | hosts: {} 11 | vars: {} 12 | linux_ppcle: 13 | hosts: {} 14 | vars: {} 15 | linux_s390: 16 | hosts: {} 17 | vars: {} 18 | linux_x86: 19 | hosts: {} 20 | vars: {} 21 | vars: 22 | ansible_become: true 23 | ansible_ssh_common_args: '-o StrictHostKeyChecking=no' 24 | windows: 25 | children: 26 | windows_x86: 27 | hosts: {} 28 | vars: {} 29 | vars: 30 | ansible_connection: winrm 31 | ansible_port: 5985 32 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/config/config-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if OneAgent is installed 3 | ansible.builtin.stat: 4 | path: "{{ oneagent_ctl_bin_path }}" 5 | register: _oneagent_ctl_state 6 | 7 | - name: Applying OneAgent configuration 8 | ansible.builtin.command: "{{ oneagent_ctl_bin_path }} {{ _oneagent_all_config_args | join(' ') }}" 9 | no_log: "{{ oneagent_no_log }}" 10 | ignore_errors: true 11 | changed_when: true 12 | register: _oneagent_config_result 13 | when: _oneagent_ctl_state.stat.exists 14 | 15 | - name: Verify configuration result 16 | ansible.builtin.debug: 17 | msg: "{{ oneagent_configuration_failed | format(_oneagent_config_result.rc, _oneagent_config_result.stdout, _oneagent_config_result.stderr) }}" 18 | when: _oneagent_ctl_state.stat.exists and _oneagent_config_result.failed|default(false) 19 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/uninstall/uninstall-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if OneAgent is installed 3 | ansible.builtin.stat: 4 | path: "{{ oneagent_ctl_bin_path }}" 5 | register: _oneagent_ctl_state 6 | 7 | - name: Uninstall OneAgent 8 | ansible.builtin.command: "{{ oneagent_uninstall_cmd }}" 9 | args: 10 | removes: "{{ oneagent_ctl_bin_path }}" 11 | register: _oneagent_uninstall_result 12 | no_log: "{{ oneagent_no_log }}" 13 | ignore_errors: true 14 | when: _oneagent_ctl_state.stat.exists 15 | 16 | - name: Verify uninstall result 17 | ansible.builtin.fail: 18 | msg: "{{ oneagent_uninstall_failed_unix | format(_oneagent_uninstall_result.rc, _oneagent_uninstall_result.stdout, _oneagent_uninstall_result.stderr) }}" 19 | when: _oneagent_ctl_state.stat.exists and _oneagent_uninstall_result.failed|default(false) 20 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/config/config-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if OneAgent is installed 3 | ansible.windows.win_stat: 4 | path: "{{ oneagent_ctl_bin_path }}" 5 | register: _oneagent_ctl_state 6 | 7 | - name: Applying OneAgent configuration 8 | ansible.windows.win_command: "\"{{ oneagent_ctl_bin_path }}\" {{ _oneagent_all_config_args | join(' ') }}" 9 | no_log: "{{ oneagent_no_log }}" 10 | ignore_errors: true 11 | changed_when: true 12 | register: _oneagent_config_result 13 | when: _oneagent_ctl_state.stat.exists 14 | 15 | - name: Verify configuration result 16 | ansible.builtin.fail: 17 | msg: "{{ oneagent_configuration_failed | format(_oneagent_config_result.rc, _oneagent_config_result.stdout, _oneagent_config_result.stderr) }}" 18 | when: _oneagent_ctl_state.stat.exists and _oneagent_config_result.failed|default(false) 19 | -------------------------------------------------------------------------------- /roles/oneagent/tests/command/command_wrapper.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from pathlib import Path 3 | 4 | from command.command_result import CommandResult 5 | 6 | 7 | class CommandWrapper: 8 | @abstractmethod 9 | def get_file_content(self, address: str, file: Path) -> CommandResult: 10 | raise NotImplementedError 11 | 12 | @abstractmethod 13 | def file_exists(self, address: str, file: Path) -> CommandResult: 14 | raise NotImplementedError 15 | 16 | @abstractmethod 17 | def directory_exists(self, address: str, directory: Path) -> CommandResult: 18 | raise NotImplementedError 19 | 20 | @abstractmethod 21 | def create_directory(self, address: str, directory: Path) -> CommandResult: 22 | raise NotImplementedError 23 | 24 | @abstractmethod 25 | def run_command(self, address: str, command: str, *args: str) -> CommandResult: 26 | raise NotImplementedError 27 | -------------------------------------------------------------------------------- /.github/actions/download-collection/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Download collection" 16 | description: Downloads collection from GitHub storage 17 | 18 | runs: 19 | using: composite 20 | steps: 21 | - name: "Publish artifact" 22 | uses: actions/download-artifact@v4 23 | with: 24 | name: dynatrace-oneagent-${{ github.sha }} 25 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/provide-installer/download.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set URL for agent download 3 | ansible.builtin.set_fact: 4 | _oneagent_installer_url: "{{ _oneagent_installer_url | default(oneagent_default_download_url) | replace(item.key, item.value) }}" 5 | no_log: "{{ oneagent_no_log }}" 6 | with_dict: 7 | "#URL": "{{ oneagent_environment_url }}" 8 | "#SYS": "{{ oneagent_download_system }}" 9 | "#VER": "{{ (oneagent_version == 'latest') | ternary('', 'version/') + oneagent_version }}" 10 | "#ARCH": "{{ oneagent_download_arch }}" 11 | 12 | - name: Include system-specific download tasks 13 | ansible.builtin.include_tasks: provide-installer/download-{{ oneagent_system_family }}.yml 14 | 15 | - name: Verify download result 16 | ansible.builtin.fail: 17 | msg: "{{ oneagent_failed_download | format(_oneagent_download_result.response | default(_oneagent_download_result.msg)) }}" 18 | when: _oneagent_download_result.failed|default(false) 19 | -------------------------------------------------------------------------------- /.github/actions/set-environment-variables/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Set environment variables" 16 | description: "Set environment variables required for other actions" 17 | 18 | runs: 19 | using: composite 20 | steps: 21 | - name: "Set environment variables" 22 | shell: bash 23 | run: printf "FILES_DIR=roles/oneagent/files\n" >> $GITHUB_ENV 24 | -------------------------------------------------------------------------------- /.github/workflows/refresh-automation-hub-api-token.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Refresh Automation Hub token 16 | on: 17 | schedule: 18 | - cron: '0 0 * * 0' 19 | workflow_dispatch: 20 | jobs: 21 | refresh: 22 | uses: ansible/ansible-content-actions/.github/workflows/refresh_ah_token.yaml@main 23 | with: 24 | environment: release 25 | secrets: 26 | ah_token: ${{ secrets.AH_TOKEN }} -------------------------------------------------------------------------------- /.github/actions/upload-collection/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Upload collection" 16 | description: Uploads the collection to GitHub storage 17 | 18 | runs: 19 | using: composite 20 | steps: 21 | - name: "Publish artifact" 22 | uses: actions/upload-artifact@v4 23 | with: 24 | name: dynatrace-oneagent-${{ github.sha }} 25 | retention-days: 7 26 | path: dynatrace-oneagent* 27 | -------------------------------------------------------------------------------- /roles/oneagent/examples/advanced_config/advanced_config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: > 3 | Download OneAgent installer in specific version to a custom directory with additional OneAgent install parameters. 4 | Both linux_other and linux_arm have different user specified by platform args parameter. 5 | hosts: linux_other,linux_arm 6 | # credentials.yml file contains oneagent_environment_url and 7 | # oneagent_paas_token variables that needs to be stored securely 8 | vars_files: 9 | - credentials.yml 10 | vars: 11 | oneagent_download_dir: /home/user1 12 | oneagent_version: 1.199.247.20200714-111723 13 | oneagent_install_args: 14 | - INSTALL_PATH=/opt/example 15 | - GROUP=example_group 16 | - --set-host-name=new_host_name 17 | - --set-host-tag=new_tag 18 | - --set-host-property=property1 19 | - --set-host-property=other=property 20 | tasks: 21 | - name: Import Dynatrace OneAgent role 22 | ansible.builtin.import_role: 23 | name: dynatrace.oneagent.oneagent 24 | -------------------------------------------------------------------------------- /roles/oneagent/examples/local_installer/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | children: 4 | unix: 5 | children: 6 | linux: 7 | children: 8 | linux_x86: 9 | hosts: www[01:50].example.com 10 | vars: 11 | oneagent_local_installer: /path/to/linux-x86-installer.sh 12 | oneagent_install_args: 13 | - INSTALL_PATH=/opt/example 14 | - USER=example_user 15 | aix_ppc: 16 | hosts: hosts[1:5] 17 | vars: 18 | oneagent_local_installer: /path/to/aix-ppc-installer.sh 19 | vars: 20 | ansible_become: true 21 | windows: 22 | hosts: www[51:100].example.com 23 | vars: 24 | oneagent_local_installer: /path/to/dynatrace-windows-installer.exe 25 | oneagent_install_args: 26 | - USER=LocalService 27 | - INSTALL_PATH=C:\\example 28 | ansible_connection: ssh 29 | ansible_shell_type: cmd 30 | ansible_user: some_admin 31 | -------------------------------------------------------------------------------- /.github/actions/build-collection/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Build collection" 16 | description: "Builds collection" 17 | 18 | runs: 19 | using: composite 20 | steps: 21 | - name: "Download certificate" 22 | shell: bash 23 | run: mkdir -p $FILES_DIR && wget https://ca.dynatrace.com/dt-root.cert.pem -P $FILES_DIR 24 | - name: "Build the collection" 25 | shell: bash 26 | run: ansible-galaxy collection build . -vvv 27 | -------------------------------------------------------------------------------- /.github/actions/update-version/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Update version in all needed files" 16 | inputs: 17 | version: 18 | description: "Version to be released (e.g. 1.2.3)" 19 | required: true 20 | 21 | runs: 22 | using: composite 23 | steps: 24 | - name: Update version in files 25 | shell: bash 26 | run: | 27 | python $GITHUB_WORKSPACE/.github/actions/update-version/update_version.py "$VERSION" 28 | env: 29 | VERSION: ${{ inputs.version }} 30 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/provide-installer/download-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure Download directory exists 3 | ansible.builtin.file: 4 | path: "{{ oneagent_download_path }}" 5 | state: directory 6 | mode: "0755" 7 | when: not _oneagent_download_path_state.stat.exists 8 | 9 | - name: Check if OneAgent installer exists 10 | ansible.builtin.stat: 11 | path: "{{ oneagent_installer_path }}" 12 | register: _oneagent_installer_stat 13 | 14 | - name: Download OneAgent installer 15 | ansible.builtin.get_url: 16 | timeout: 60 17 | url: "{{ _oneagent_installer_url }}" 18 | dest: "{{ oneagent_installer_path }}" 19 | validate_certs: "{{ oneagent_validate_certs | default(true) }}" 20 | mode: "0755" 21 | headers: 22 | Authorization: Api-Token {{ oneagent_paas_token }} 23 | environment: 24 | SSL_CERT_FILE: "{{ oneagent_installer_download_cert | default(omit) }}" 25 | no_log: "{{ oneagent_no_log }}" 26 | ignore_errors: true 27 | register: _oneagent_download_result 28 | retries: 5 29 | until: _oneagent_download_result is succeeded 30 | delay: 10 31 | changed_when: false 32 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/provide-installer/download-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure Download directory exists 3 | ansible.windows.win_file: 4 | path: "{{ oneagent_download_path }}" 5 | state: directory 6 | when: not _oneagent_download_path_state.stat.exists 7 | 8 | - name: Check if OneAgent installer exists 9 | ansible.windows.win_stat: 10 | path: "{{ oneagent_installer_path }}" 11 | register: _oneagent_installer_stat 12 | 13 | - name: Download OneAgent installer 14 | ansible.windows.win_get_url: 15 | url_timeout: 60 16 | url: "{{ _oneagent_installer_url }}" 17 | dest: "{{ oneagent_installer_path }}" 18 | validate_certs: "{{ oneagent_validate_certs | default(true) }}" 19 | headers: 20 | Authorization: Api-Token {{ oneagent_paas_token }} 21 | environment: 22 | SSL_CERT_FILE: "{{ oneagent_installer_download_cert | default(omit) }}" 23 | no_log: "{{ oneagent_no_log }}" 24 | ignore_errors: true 25 | register: _oneagent_download_result 26 | retries: 5 27 | until: _oneagent_download_result is succeeded 28 | delay: 10 29 | changed_when: not _oneagent_installer_stat.stat.exists or _oneagent_download_result.changed 30 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/ansible/ansible-lint 4 | rev: v24.12.2 5 | hooks: 6 | - id: ansible-lint 7 | pass_filenames: false 8 | 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v5.0.0 11 | hooks: 12 | - id: mixed-line-ending 13 | - id: trailing-whitespace 14 | 15 | - repo: https://github.com/psf/black 16 | rev: 24.10.0 17 | hooks: 18 | - id: black 19 | 20 | - repo: local 21 | hooks: 22 | - id: shfmt 23 | name: shfmt 24 | additional_dependencies: [mvdan.cc/sh/v3/cmd/shfmt@v3.8.0] 25 | entry: shfmt 26 | language: golang 27 | exclude: roles/oneagent/tests/resources/installers/Dynatrace-OneAgent-Linux.sh 28 | args: 29 | - "-s" 30 | - "-w" 31 | types: [shell] 32 | 33 | - repo: https://github.com/shellcheck-py/shellcheck-py 34 | rev: "v0.10.0.1" 35 | hooks: 36 | - id: shellcheck 37 | exclude: gradlew 38 | args: 39 | - "--external-sources" 40 | 41 | - repo: https://github.com/adrienverge/yamllint 42 | rev: "v1.35.0" 43 | hooks: 44 | - id: yamllint 45 | files: \.(yaml|yml) 46 | -------------------------------------------------------------------------------- /.github/actions/update-changelog/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Update changelog" 16 | inputs: 17 | version: 18 | description: "Version to be released (e.g. 1.2.3)" 19 | required: true 20 | 21 | runs: 22 | using: composite 23 | steps: 24 | - name: Install git-conventional-commits 25 | shell: bash 26 | run: | 27 | npm i git-conventional-commits 28 | 29 | - name: Generate changelog 30 | id: generate_changelog 31 | shell: bash 32 | run: | 33 | npx git-conventional-commits changelog --file CHANGELOG.md --release "$VERSION" 34 | env: 35 | VERSION: ${{ inputs.version }} 36 | -------------------------------------------------------------------------------- /.github/actions/set-up-build-environment/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Set up build environment" 16 | description: "Sets up build environment" 17 | 18 | inputs: 19 | ansible-version: 20 | description: "The Ansible version to install" 21 | required: true 22 | 23 | runs: 24 | using: composite 25 | steps: 26 | - uses: actions/setup-python@v5 27 | with: 28 | python-version: "3.12" 29 | - name: "Upgrade pip" 30 | shell: bash 31 | run: | 32 | pip install --upgrade pip 33 | - name: "Install dependencies" 34 | shell: bash 35 | run: | 36 | pip install "ansible==${{ inputs.ansible-version }}.*" ansible-lint 37 | -------------------------------------------------------------------------------- /.github/actions/run-sanity-and-linter-tests/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: run-sanity-and-linter-tests 16 | description: "Runs sanity and linter tests" 17 | 18 | runs: 19 | using: composite 20 | steps: 21 | - name: "Install the collection" 22 | shell: bash 23 | run: ansible-galaxy collection install -vvv dynatrace-oneagent* 24 | 25 | - name: "Run sanity test" 26 | shell: bash 27 | run: pushd ~/.ansible/collections/ansible_collections/dynatrace/oneagent && ansible-test sanity && popd 28 | 29 | - name: "Run linter test" 30 | shell: bash 31 | run: pushd ~/.ansible/collections/ansible_collections/dynatrace/oneagent && ansible-lint && popd 32 | -------------------------------------------------------------------------------- /roles/oneagent/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Set environment variables 3 | oneagent_environment_url: "" 4 | oneagent_paas_token: "" 5 | 6 | # Set local installer location 7 | oneagent_local_installer: "" 8 | 9 | # Set installer download architecture 10 | oneagent_installer_arch: "" 11 | 12 | # Set the OneAgent version to download 13 | oneagent_version: latest 14 | 15 | # Set the OneAgent common installer parameters 16 | oneagent_install_args: [] 17 | # Set the OneAgent platform-specific installer parameters 18 | oneagent_platform_install_args: [] 19 | # Set the OneAgent package state 20 | oneagent_package_state: present 21 | 22 | # Set the OneAgent download directory 23 | oneagent_download_dir: "" 24 | 25 | # Set the need of preserving installers on secondary machines 26 | oneagent_preserve_installer: false 27 | 28 | # Set the need of rebooting machine after installation 29 | oneagent_reboot_host: false 30 | 31 | # Set the need of verifying the installer signature 32 | oneagent_verify_signature: true 33 | 34 | # Set the timeout for platform reboot 35 | oneagent_reboot_timeout: 3600 36 | 37 | # Set CA certificate download url 38 | oneagent_ca_cert_download_url: https://ca.dynatrace.com/dt-root.cert.pem 39 | 40 | # Sets Ansible `no_log` attribute value in tasks which may log sensitive values 41 | oneagent_no_log: true 42 | -------------------------------------------------------------------------------- /roles/oneagent/tests/deployment/deployment_platform.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from command.command_result import CommandResult 4 | 5 | 6 | class DeploymentPlatform(Enum): 7 | AIX_PPC = "aix_ppc" 8 | LINUX_ARM = "linux_arm" 9 | LINUX_PPCLE = "linux_ppcle" 10 | LINUX_S390 = "linux_s390" 11 | LINUX_X86 = "linux_x86" 12 | WINDOWS_X86 = "windows_x86" 13 | 14 | def __str__(self) -> str: 15 | return str(self.name) 16 | 17 | def family(self) -> str: 18 | return "windows" if self == DeploymentPlatform.WINDOWS_X86 else "unix" 19 | 20 | def arch(self) -> str: 21 | return str(self.value).split("_")[1] 22 | 23 | def system(self) -> str: 24 | return str(self.value).split("_", maxsplit=1)[0] 25 | 26 | @staticmethod 27 | def from_str(param: str) -> "DeploymentPlatform": 28 | try: 29 | return DeploymentPlatform[param.upper()] 30 | except KeyError: 31 | raise ValueError(f"Invalid option passed: {param}") from KeyError 32 | 33 | @staticmethod 34 | def from_system_and_arch(system: str, arch: str) -> "DeploymentPlatform": 35 | return DeploymentPlatform.from_str(f"{system}_{arch}") 36 | 37 | 38 | DeploymentResult = list[CommandResult] 39 | PlatformCollection = dict[DeploymentPlatform, list[str]] 40 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/uninstall/uninstall-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if OneAgent is uninstalled 3 | ansible.windows.win_command: wmic product where name='{{ oneagent_service_name }}' get IdentifyingNumber 4 | register: _oneagent_wmic_output 5 | 6 | - name: Search for product code 7 | ansible.builtin.set_fact: 8 | _oneagent_product_code_list: "{{ _oneagent_wmic_output.stdout_lines | select('regex', '{.*}') | map('trim') | list }}" 9 | no_log: true 10 | 11 | - name: Gather product code 12 | ansible.builtin.set_fact: 13 | _oneagent_product_code: "{{ _oneagent_product_code_list | first }}" 14 | no_log: true 15 | when: _oneagent_product_code_list|length > 0 16 | 17 | - name: Uninstall OneAgent 18 | ansible.windows.win_package: 19 | product_id: "{{ _oneagent_product_code }}" 20 | state: absent 21 | log_path: "{{ oneagent_uninstall_log_path }}" 22 | no_log: "{{ oneagent_no_log }}" 23 | ignore_errors: true 24 | register: _oneagent_uninstall_result 25 | when: _oneagent_product_code is defined 26 | 27 | - name: Verify uninstall result 28 | ansible.builtin.fail: 29 | msg: "Uninstall failed, exit code: {{ _oneagent_uninstall_result.rc }}. For details, see {{ oneagent_uninstall_log_file_name }}" 30 | when: _oneagent_product_code is defined and _oneagent_uninstall_result.failed|default(false) 31 | -------------------------------------------------------------------------------- /roles/oneagent/vars/win32nt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | oneagent_default_arch: x86 3 | oneagent_default_install_dir: "{{ ansible_env.SystemDrive }}\\Program Files\\dynatrace\\oneagent" 4 | oneagent_default_download_dir: "{{ ansible_env['TEMP'] | default('C:\\Windows\\Temp') }}" 5 | 6 | oneagent_download_system: windows 7 | oneagent_available_arch: ["{{ oneagent_default_arch }}"] 8 | oneagent_download_arch: "{{ oneagent_installer_arch | default(oneagent_default_arch, true) }}" 9 | 10 | oneagent_install_path: >- 11 | {{ oneagent_passed_install_args | select('regex', 'INSTALL_PATH') | first 12 | | default(oneagent_default_install_dir) | regex_replace('INSTALL_PATH=(.*)', '\\1') }} 13 | oneagent_ctl_bin_path: "{{ oneagent_install_path }}\\agent\\tools\\oneagentctl.exe" 14 | oneagent_download_path: "{{ oneagent_download_dir | default(oneagent_default_download_dir, true) }}" 15 | oneagent_installer_path: "{{ oneagent_download_path }}\\Dynatrace-OneAgent-Windows-{{ oneagent_version }}.exe" 16 | oneagent_log_dir: "{{ ansible_env['ProgramData'] }}\\dynatrace\\oneagent\\log\\installer" 17 | oneagent_uninstall_log_file_name: uninstall-{{ ansible_date_time.date | replace('-', '') }}-{{ ansible_date_time.time | replace(':', '') }}.log 18 | oneagent_uninstall_log_path: "{{ oneagent_log_dir }}\\{{ oneagent_uninstall_log_file_name }}" 19 | 20 | oneagent_service_name: Dynatrace OneAgent 21 | -------------------------------------------------------------------------------- /roles/oneagent/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | oneagent_minimal_install_version: "1.199" 3 | oneagent_minimal_reporting_version: "1.203" 4 | 5 | oneagent_system_name: "{{ ansible_system | lower }}" 6 | oneagent_system_is_unix: "{{ oneagent_system_name in ['aix', 'linux'] }}" 7 | oneagent_system_is_windows: "{{ oneagent_system_name == 'win32nt' }}" 8 | oneagent_system_family: "{{ oneagent_system_is_windows | ternary('windows', 'unix') }}" 9 | 10 | oneagent_is_operation_uninstall: "{{ oneagent_package_state == 'absent' }}" 11 | oneagent_is_operation_configuration: >- 12 | {{ not oneagent_is_operation_uninstall and oneagent_environment_url | length == 0 and 13 | oneagent_paas_token | length == 0 and oneagent_local_installer | length == 0 }} 14 | oneagent_is_operation_installation: "{{ not (oneagent_is_operation_uninstall or oneagent_is_operation_configuration | bool) }}" 15 | 16 | oneagent_passed_install_args: "{{ (oneagent_install_args + oneagent_platform_install_args) | unique(case_sensitive=False) }}" 17 | oneagent_default_download_url: "#URL/api/v1/deployment/installer/agent/#SYS/default/#VER?arch=#ARCH" 18 | oneagent_additional_reporting_params: 19 | - "--set-deployment-metadata=\"orchestration_tech=Ansible\"" 20 | - "--set-deployment-metadata=\"tech_version={{ ansible_version.full }}\"" 21 | - "--set-deployment-metadata=\"script_version={{ oneagent_script_version }}\"" 22 | -------------------------------------------------------------------------------- /.github/actions/make-release/parse_changelog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from pathlib import Path 5 | 6 | def get_section(input_file: Path, version: str): 7 | result = [] 8 | with open(input_file, encoding="utf-8") as f: 9 | found: bool = False 10 | header_start = "## " 11 | lines = f.readlines() 12 | for i, line in enumerate(lines): 13 | if not found and line.startswith(header_start) and line.find(version) != -1: 14 | found = True 15 | continue 16 | if found: 17 | if line.startswith(header_start): 18 | break 19 | result.append(line) 20 | return result 21 | 22 | 23 | if __name__ == "__main__": 24 | parser = argparse.ArgumentParser(description="Parse changelog version") 25 | parser.add_argument("input_file", help="Path to input file") 26 | parser.add_argument("output_file", help="Path to output file") 27 | parser.add_argument("version", help="Version to process") 28 | args = parser.parse_args() 29 | 30 | section = get_section(args.input_file, args.version) 31 | 32 | if not section: 33 | print(f"Version {args.version} not found in {args.input_file}") 34 | exit(1) 35 | 36 | with open(args.output_file, "w") as file: 37 | file.write("".join(section).strip()) -------------------------------------------------------------------------------- /roles/oneagent/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Load version variable 3 | ansible.builtin.include_vars: vars/version.yml 4 | - name: Load variables based on OS 5 | ansible.builtin.include_vars: vars/{{ oneagent_system_name }}.yml 6 | - name: Load messages 7 | ansible.builtin.include_vars: vars/messages.yml 8 | - name: Check passed parameters 9 | ansible.builtin.include_tasks: params/params.yml 10 | - name: Prepare and perform installation 11 | block: 12 | - name: Gather installers info 13 | ansible.builtin.include_tasks: gather-info/gather-info.yml 14 | when: oneagent_is_operation_installation 15 | 16 | - name: Provide OneAgent installer 17 | ansible.builtin.include_tasks: provide-installer/provide-installer.yml 18 | when: oneagent_is_operation_installation 19 | 20 | - name: Apply OneAgent configuration 21 | ansible.builtin.include_tasks: config/config.yml 22 | when: oneagent_is_operation_configuration 23 | 24 | - name: Install OneAgent 25 | ansible.builtin.include_tasks: install/install.yml 26 | when: oneagent_is_operation_installation 27 | always: 28 | - name: Cleanup 29 | ansible.builtin.include_tasks: cleanup/cleanup.yml 30 | when: oneagent_is_operation_installation and not (oneagent_preserve_installer | bool) 31 | 32 | - name: Uninstall OneAgent 33 | ansible.builtin.include_tasks: uninstall/uninstall.yml 34 | when: oneagent_is_operation_uninstall 35 | -------------------------------------------------------------------------------- /roles/oneagent/vars/aix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | oneagent_default_arch: ppc 3 | oneagent_default_install_dir: /opt/dynatrace/oneagent 4 | oneagent_default_download_dir: "{{ ansible_env['TEMP'] | default('/tmp') }}" 5 | 6 | oneagent_download_system: aix 7 | oneagent_available_arch: ["{{ oneagent_default_arch }}"] 8 | oneagent_download_arch: "{{ oneagent_installer_arch | default(oneagent_default_arch, true) }}" 9 | 10 | oneagent_install_path: >- 11 | {{ oneagent_passed_install_args | select('regex', 'INSTALL_PATH') | first 12 | | default(oneagent_default_install_dir) | regex_replace('INSTALL_PATH=(.*)', '\\1') }} 13 | oneagent_download_path: "{{ oneagent_download_dir | default(oneagent_default_download_dir, true) }}" 14 | oneagent_installer_path: "{{ oneagent_download_path }}/Dynatrace-OneAgent-AIX-{{ oneagent_version }}.sh" 15 | oneagent_ctl_bin_path: "{{ oneagent_install_path }}/agent/tools/oneagentctl" 16 | 17 | oneagent_install_cmd: sh {{ oneagent_installer_path }} 18 | oneagent_uninstall_cmd: sh {{ oneagent_install_path }}/agent/uninstall.sh 19 | 20 | oneagent_ca_cert_src_path: "{{ role_path }}/files/dt-root.cert.pem" 21 | oneagent_ca_cert_dest_path: "{{ oneagent_download_path }}/dt-root.cert.pem" 22 | oneagent_certificate_verification_header: > 23 | Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha-256"; boundary="--SIGNED-INSTALLER" 24 | oneagent_additional_openssl_env: LDR_CNTRL=MAXDATA=0x40000000 25 | oneagent_service_name: oneagent 26 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/gather-info/gather-info.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Gather installers info 3 | ansible.builtin.include_tasks: tasks/gather-info/gather-info-{{ oneagent_system_family }}.yml 4 | 5 | - name: Gather installer metainfo from tenant 6 | ansible.builtin.uri: 7 | url: >- 8 | {{ oneagent_environment_url }}/api/v1/deployment/installer/agent/{{ oneagent_system_family }}/default/latest/metainfo?arch={{ oneagent_download_arch }} 9 | validate_certs: "{{ oneagent_validate_certs | default(true) }}" 10 | headers: 11 | Authorization: Api-Token {{ oneagent_paas_token }} 12 | environment: 13 | SSL_CERT_FILE: "{{ oneagent_ca_cert_download_cert | default(omit) }}" 14 | register: oneagent_installer_from_tenant_json 15 | no_log: "{{ oneagent_no_log }}" 16 | ignore_errors: true 17 | retries: 5 18 | until: oneagent_installer_from_tenant_json is succeeded 19 | delay: 10 20 | when: oneagent_version == 'latest' and oneagent_local_installer|length == 0 21 | 22 | - name: Verify getting metainfo result 23 | ansible.builtin.debug: 24 | msg: "{{ oneagent_fetch_metadata_failed | format(oneagent_installer_from_tenant_json.response | default(oneagent_installer_from_tenant_json.msg)) }}" 25 | when: oneagent_installer_from_tenant_json.failed|default(false) 26 | 27 | - name: Parse metainfo output 28 | ansible.builtin.set_fact: 29 | oneagent_installer_metainfo_version: "{{ oneagent_installer_from_tenant_json.json.latestAgentVersion | default('') }}" 30 | when: oneagent_installer_from_tenant_json is not failed 31 | -------------------------------------------------------------------------------- /.github/workflows/check-commits.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Check commits 16 | on: 17 | pull_request: 18 | 19 | jobs: 20 | check-commits: 21 | runs-on: ubuntu-latest 22 | steps: 23 | # Checkout the branch 24 | - name: checkout 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Check commit messages 30 | env: 31 | BASE_REF: ${{ github.event.pull_request.base.sha }} 32 | HEAD_REF: ${{ github.event.pull_request.head.sha }} 33 | run: | 34 | echo "Checking commits from $BASE_REF to $HEAD_REF" 35 | for COMMIT in $(git rev-list $BASE_REF..$HEAD_REF); do 36 | echo "Checking commit $COMMIT" 37 | commit_msg_file=$(mktemp) 38 | git show --format=%B $COMMIT > ${commit_msg_file} 39 | npx git-conventional-commits commit-msg-hook ${commit_msg_file} 40 | done 41 | -------------------------------------------------------------------------------- /roles/oneagent/vars/messages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | oneagent_failed_download: "Failed to download installer: %s" 3 | oneagent_version_lower_than_installed: New installer has a lower version than already installed OneAgent, installation is not possible 4 | oneagent_version_lower_than_minimal: Version of new installer (%s) does not meet the requirements (minimum compatible is %s), installation is not possible 5 | oneagent_installation_failed_unix: "Installation failed, exit code: %d, output: %s %s" 6 | oneagent_installation_failed_windows: "Installation failed, exit code: %s. Check installation log for more details" 7 | oneagent_configuration_failed: "Configuration failed, exit code: %d, output: %s %s" 8 | oneagent_install_dir_contains_spaces: Installation directory must not contain spaces 9 | oneagent_download_dir_contains_spaces: Download directory must not contain spaces 10 | oneagent_missing_mandatory_params: One or more mandatory variables are not defined. Please see documentation for further information. 11 | oneagent_unknown_arch: "Unknown architecture '%s'. Available architectures are: %s." 12 | oneagent_missing_local_installer: Local installer is not available for specified system 13 | oneagent_multiple_install_dir: Multiple INSTALL_PATH parameters are not allowed 14 | oneagent_signature_verification_failed: "Signature verification failed: %s" 15 | oneagent_uninstall_failed_unix: "Uninstall failed, exit code: %d, output: %s %s" 16 | oneagent_reboot_failed: Failed to reboot the host 17 | oneagent_fetch_metadata_failed: "Failed to fetch metadata from tenant: %s" 18 | -------------------------------------------------------------------------------- /roles/oneagent/vars/linux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | oneagent_default_arch: x86 3 | oneagent_default_install_dir: /opt/dynatrace/oneagent 4 | oneagent_default_download_dir: "{{ ansible_env['TEMP'] | default('/tmp') }}" 5 | 6 | oneagent_download_system: unix 7 | oneagent_available_arch: ["{{ oneagent_default_arch }}", "ppcle", "s390", "arm"] 8 | oneagent_download_arch: "{{ oneagent_installer_arch | default(oneagent_default_arch, true) }}" 9 | 10 | oneagent_install_path: >- 11 | {{ oneagent_passed_install_args | select('regex', 'INSTALL_PATH') | first 12 | | default(oneagent_default_install_dir) | regex_replace('INSTALL_PATH=(.*)', '\1') }} 13 | oneagent_download_path: "{{ oneagent_download_dir | default(oneagent_default_download_dir, true) }}" 14 | oneagent_installer_arch_name: "{{ (oneagent_download_arch == oneagent_default_arch) | ternary('', oneagent_download_arch + '-') }}" 15 | oneagent_installer_path: "{{ oneagent_download_path }}/Dynatrace-OneAgent-Linux-{{ oneagent_installer_arch_name }}{{ oneagent_version }}.sh" 16 | oneagent_ctl_bin_path: "{{ oneagent_install_path }}/agent/tools/oneagentctl" 17 | 18 | oneagent_install_cmd: sh {{ oneagent_installer_path }} 19 | oneagent_uninstall_cmd: sh {{ oneagent_install_path }}/agent/uninstall.sh 20 | 21 | oneagent_ca_cert_src_path: "{{ role_path }}/files/dt-root.cert.pem" 22 | oneagent_ca_cert_dest_path: "{{ oneagent_download_path }}/dt-root.cert.pem" 23 | oneagent_certificate_verification_header: > 24 | Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha-256"; boundary="--SIGNED-INSTALLER" 25 | oneagent_service_name: oneagent 26 | -------------------------------------------------------------------------------- /roles/oneagent/tests/command/unix/unix_command_wrapper.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from pathlib import Path 3 | 4 | from command.command_result import CommandResult 5 | from command.command_wrapper import CommandWrapper 6 | from typing_extensions import override 7 | 8 | 9 | class UnixCommandWrapper(CommandWrapper): 10 | def __init__(self, user: str, password: str): 11 | self.password: str = password 12 | 13 | def _execute(self, address: str, command: str, *args: str) -> CommandResult: 14 | out = subprocess.run( 15 | " ".join([command, *args]), 16 | stdout=subprocess.PIPE, 17 | stderr=subprocess.PIPE, 18 | universal_newlines=True, 19 | check=False, 20 | shell=True, 21 | ) 22 | return CommandResult(out.returncode, out.stdout, out.stderr) 23 | 24 | @override 25 | def get_file_content(self, address: str, file: Path) -> CommandResult: 26 | return self._execute(address, "cat", str(file)) 27 | 28 | @override 29 | def file_exists(self, address: str, file: Path) -> CommandResult: 30 | return self._execute(address, "test", "-f", str(file)) 31 | 32 | @override 33 | def directory_exists(self, address: str, directory: Path) -> CommandResult: 34 | return self._execute(address, "test", "-d", str(directory)) 35 | 36 | @override 37 | def create_directory(self, address: str, directory: Path) -> CommandResult: 38 | return self._execute(address, "mkdir", "-p", str(directory)) 39 | 40 | @override 41 | def run_command(self, address: str, command: str, *args: str) -> CommandResult: 42 | return self._execute(address, f"echo {self.password} | sudo -S {command}", *args) 43 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/provide-installer/signature-unix.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if CA certificate exists 3 | delegate_to: localhost 4 | ansible.builtin.stat: 5 | path: "{{ oneagent_ca_cert_src_path }}" 6 | register: _oneagent_ca_cert_state 7 | 8 | - name: Transfer CA certificate 9 | ansible.builtin.copy: 10 | src: "{{ oneagent_ca_cert_src_path }}" 11 | dest: "{{ oneagent_ca_cert_dest_path }}" 12 | mode: "0644" 13 | when: not (oneagent_force_cert_download | default(false)) and _oneagent_ca_cert_state.stat.exists 14 | 15 | - name: Download CA certificate 16 | ansible.builtin.get_url: 17 | url: "{{ oneagent_ca_cert_download_url }}" 18 | dest: "{{ oneagent_ca_cert_dest_path }}" 19 | mode: "0644" 20 | validate_certs: "{{ oneagent_validate_certs | default(true) }}" 21 | environment: 22 | SSL_CERT_FILE: "{{ oneagent_ca_cert_download_cert | default(omit) }}" 23 | when: oneagent_force_cert_download | default(false) or not _oneagent_ca_cert_state.stat.exists 24 | changed_when: false 25 | 26 | - name: Validate installer signature 27 | ansible.builtin.shell: > 28 | ( printf '%s\n' '{{ oneagent_certificate_verification_header }}'; 29 | printf '\n\n----SIGNED-INSTALLER\n'; cat "{{ oneagent_installer_path }}" ) | 30 | {{ oneagent_additional_openssl_env | default('') }} openssl cms -verify -CAfile "{{ oneagent_ca_cert_dest_path }}" > /dev/null 31 | no_log: "{{ oneagent_no_log }}" 32 | ignore_errors: true 33 | changed_when: false 34 | register: _oneagent_cert_verification_result 35 | 36 | - name: Installer signature validation result 37 | ansible.builtin.fail: 38 | msg: "{{ oneagent_signature_verification_failed | format(_oneagent_cert_verification_result.stderr) }}" 39 | when: _oneagent_cert_verification_result.rc != 0 40 | -------------------------------------------------------------------------------- /.github/actions/make-release/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Make GitHub release" 16 | inputs: 17 | gha_token: 18 | descrition: "GHA bot token" 19 | required: true 20 | 21 | runs: 22 | using: composite 23 | steps: 24 | - name: Get the collection version 25 | id: get_version 26 | shell: bash 27 | run: | 28 | VERSION=$(python $GITHUB_WORKSPACE/.github/actions/make-release/get_current_version.py galaxy.yml) 29 | echo "version=$VERSION" >> $GITHUB_OUTPUT 30 | - name: Create a notice file 31 | shell: bash 32 | run: | 33 | python $GITHUB_WORKSPACE/.github/actions/make-release/parse_changelog.py CHANGELOG.md release-notes.md "$VERSION" 34 | env: 35 | VERSION: ${{ steps.get_version.outputs.version }} 36 | - name: Make a release 37 | shell: bash 38 | run: | 39 | gh release create "v$VERSION" --title="$VERSION" --notes-file release-notes.md 40 | env: 41 | GH_TOKEN: ${{ inputs.gha_token }} 42 | VERSION: ${{ steps.get_version.outputs.version }} 43 | - name: Remove the release notes file 44 | if: always() 45 | shell: bash 46 | run: | 47 | rm -f release-notes.md 48 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/params/params.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Validate URL and token 3 | ansible.builtin.fail: 4 | msg: "{{ oneagent_missing_mandatory_params }}" 5 | when: > 6 | oneagent_is_operation_installation and oneagent_local_installer|length == 0 and (oneagent_environment_url|length == 0 or oneagent_paas_token|length == 0) 7 | 8 | - name: Check local installer 9 | ansible.builtin.stat: 10 | path: "{{ oneagent_local_installer }}" 11 | delegate_to: localhost 12 | register: _oneagent_local_installer_state 13 | when: oneagent_local_installer|length > 0 14 | 15 | - name: Validate availability of local installer 16 | ansible.builtin.fail: 17 | msg: "{{ oneagent_missing_local_installer }}" 18 | when: oneagent_is_operation_installation and oneagent_local_installer|length > 0 and not _oneagent_local_installer_state.stat.exists 19 | 20 | - name: Validate installer architecture 21 | ansible.builtin.fail: 22 | msg: "{{ oneagent_unknown_arch | format(oneagent_download_arch, oneagent_available_arch) }}" 23 | when: oneagent_is_operation_installation and not oneagent_download_arch in oneagent_available_arch 24 | 25 | - name: Include system-specific parameters validators 26 | ansible.builtin.include_tasks: tasks/params/params-{{ oneagent_system_family }}.yml 27 | 28 | - name: Validate OneAgent installer version parameter 29 | ansible.builtin.fail: 30 | msg: "{{ oneagent_version_lower_than_minimal | format(oneagent_version, oneagent_minimal_install_version) }}" 31 | when: oneagent_is_operation_installation and oneagent_version != 'latest' and oneagent_version < oneagent_minimal_install_version 32 | 33 | - name: Validate installation arguments 34 | ansible.builtin.fail: 35 | msg: "{{ oneagent_multiple_install_dir }}" 36 | when: oneagent_passed_install_args| select('regex', 'INSTALL_PATH')|list|length > 1 37 | -------------------------------------------------------------------------------- /.github/actions/get-app-versions/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Get application versions" 16 | description: "Retrieves a list of supported versions of specified application and its latest version" 17 | 18 | inputs: 19 | app: 20 | description: "Name of the application" 21 | required: true 22 | outputs: 23 | supported_versions: 24 | description: "List of supported versions" 25 | value: ${{ steps.get-app-versions.outputs.supported_versions }} 26 | latest_version: 27 | description: "Latest supported version" 28 | value: ${{ steps.get-app-versions.outputs.latest_version }} 29 | 30 | runs: 31 | using: composite 32 | steps: 33 | - name: Get app versions 34 | id: get-app-versions 35 | shell: bash 36 | run: | 37 | supportedVersions="$(python $GITHUB_WORKSPACE/.github/actions/get-app-versions/eol_client.py ${{ inputs.app }})" 38 | echo "supported_versions=${supportedVersions}" 39 | echo "supported_versions=${supportedVersions}" >> $GITHUB_OUTPUT 40 | 41 | latestVersion="$(python $GITHUB_WORKSPACE/.github/actions/get-app-versions/eol_client.py ${{ inputs.app }} --get_latest)" 42 | echo "latest_version=${latestVersion}" 43 | echo "latest_version=${latestVersion}" >> $GITHUB_OUTPUT 44 | -------------------------------------------------------------------------------- /roles/oneagent/tests/test_local_installer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ansible.config import AnsibleConfigurator 4 | from ansible.runner import AnsibleRunner 5 | from command.platform_command_wrapper import PlatformCommandWrapper 6 | from constants import ( 7 | UNIX_DOWNLOAD_DIR_PATH, 8 | WINDOWS_DOWNLOAD_DIR_PATH, 9 | WORK_INSTALLERS_DIR_PATH, 10 | ) 11 | from deployment.deployment_operations import ( 12 | check_agent_state, 13 | check_download_directory, 14 | get_installers, 15 | perform_operation_on_platforms, 16 | run_deployment, 17 | set_ca_cert_download_params, 18 | ) 19 | from deployment.deployment_platform import PlatformCollection 20 | 21 | 22 | def test_local_installer( 23 | runner: AnsibleRunner, 24 | configurator: AnsibleConfigurator, 25 | platforms: PlatformCollection, 26 | wrapper: PlatformCommandWrapper, 27 | installer_server_url: str, 28 | ): 29 | set_ca_cert_download_params(configurator, installer_server_url) 30 | 31 | for platform in platforms: 32 | installers_location = WORK_INSTALLERS_DIR_PATH 33 | latest_installer_name = get_installers(platform.system(), platform.arch(), "latest")[-1] 34 | configurator.set_platform_parameter( 35 | platform, 36 | configurator.LOCAL_INSTALLER_KEY, 37 | f"{installers_location}/{latest_installer_name}", 38 | ) 39 | 40 | _unused = run_deployment(runner, configurator) 41 | 42 | logging.info("Check if agent is installed") 43 | perform_operation_on_platforms(platforms, check_agent_state, wrapper, True) 44 | 45 | logging.info("Check if installer was removed") 46 | perform_operation_on_platforms( 47 | platforms, 48 | check_download_directory, 49 | wrapper, 50 | False, 51 | UNIX_DOWNLOAD_DIR_PATH, 52 | WINDOWS_DOWNLOAD_DIR_PATH, 53 | ) 54 | -------------------------------------------------------------------------------- /roles/oneagent/tests/command/platform_command_wrapper.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from command.command_result import CommandResult 4 | from command.command_wrapper import CommandWrapper 5 | from command.unix.unix_command_wrapper import UnixCommandWrapper 6 | from command.windows.windows_command_wrapper import WindowsCommandWrapper 7 | from deployment.deployment_platform import DeploymentPlatform 8 | 9 | 10 | class PlatformCommandWrapper: 11 | def __init__(self, user: str, password: str): 12 | self.unix_command_wrapper: UnixCommandWrapper = UnixCommandWrapper(user, password) 13 | self.windows_command_wrapper: WindowsCommandWrapper = WindowsCommandWrapper(user, password) 14 | 15 | def _get_command_wrapper(self, platform: DeploymentPlatform) -> CommandWrapper: 16 | if platform == DeploymentPlatform.WINDOWS_X86: 17 | return self.windows_command_wrapper 18 | return self.unix_command_wrapper 19 | 20 | def get_file_content(self, platform: DeploymentPlatform, address: str, file: Path) -> CommandResult: 21 | return self._get_command_wrapper(platform).get_file_content(address, file) 22 | 23 | def file_exists(self, platform: DeploymentPlatform, address: str, file: Path) -> CommandResult: 24 | return self._get_command_wrapper(platform).file_exists(address, file) 25 | 26 | def directory_exists(self, platform: DeploymentPlatform, address: str, directory: Path) -> CommandResult: 27 | return self._get_command_wrapper(platform).directory_exists(address, directory) 28 | 29 | def create_directory(self, platform: DeploymentPlatform, address: str, directory: Path) -> CommandResult: 30 | return self._get_command_wrapper(platform).create_directory(address, directory) 31 | 32 | def run_command(self, platform: DeploymentPlatform, address: str, command: str, *args: str) -> CommandResult: 33 | return self._get_command_wrapper(platform).run_command(address, command, *args) 34 | -------------------------------------------------------------------------------- /.github/actions/run-component-tests/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Run component tests" 16 | description: "Runs component tests" 17 | 18 | inputs: 19 | ansible-version: 20 | description: "The Ansible version being tested" 21 | required: false 22 | default: "unknown" 23 | 24 | runs: 25 | using: composite 26 | steps: 27 | - name: "Prepare Python environment" 28 | shell: bash 29 | run: | 30 | python -m venv venv 31 | source venv/bin/activate 32 | pip install --upgrade pip 33 | pip install -r roles/oneagent/tests/requirements.txt 34 | pip install "ansible==${{ inputs.ansible-version }}.*" 35 | - name: "Install the collection" 36 | shell: bash 37 | run: sudo bash -c "source venv/bin/activate && ansible-galaxy collection install -vvv dynatrace-oneagent*" 38 | 39 | - name: "Run component tests" 40 | shell: bash 41 | run: sudo bash -c "source venv/bin/activate && pytest roles/oneagent/tests --linux_x86=localhost" 42 | 43 | - name: "Upload logs" 44 | uses: actions/upload-artifact@v4 45 | if: ${{ always() }} 46 | with: 47 | name: component-tests-ansible-${{ inputs.ansible-version }}-${{ github.run_id }} 48 | retention-days: 1 49 | path: roles/oneagent/tests/ansible_oneagent_tests_run_dir 50 | if-no-files-found: error 51 | -------------------------------------------------------------------------------- /roles/oneagent/tests/resources/installers/oneagentctl.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script simulates deployment functionalities of oneagentctl binary, used to configure installation. 4 | 5 | set -eu 6 | 7 | readonly INSTALLER_VERSION="##VERSION##" 8 | readonly DEPLOYMENT_CONF_PATH="/var/lib/dynatrace/oneagent/agent/config" 9 | 10 | cutVariable() { 11 | local variable="${1}" 12 | local delimiter="${2}" 13 | local fields="${3}" 14 | printf "%s" "${variable}" | cut -d "${delimiter}" -f "${fields}" 15 | } 16 | 17 | saveToConfig() { 18 | while [ $# -gt 0 ]; do 19 | # example command: --set-host-property=TENANT=tenant1 20 | # setter: --set-host-property 21 | local setter 22 | setter="$(cutVariable "${1}" "=" 1)" 23 | 24 | # setterType: property 25 | local setterType 26 | setterType="$(cutVariable "${setter}" "-" "5-")" 27 | 28 | # value: TENANT=tenant1 29 | local value 30 | value="$(cutVariable "${1}" "=" "2-")" 31 | 32 | # property: TENANT 33 | local property 34 | property="$(cutVariable "${value}" "=" "1")" 35 | 36 | local setterFile="${DEPLOYMENT_CONF_PATH}/${setterType}" 37 | if grep -q "${property}" "${setterFile}"; then 38 | sed -i "s/${property}.*/${value}/" "${setterFile}" 39 | else 40 | printf '%s\n' "${value}" >>"${setterFile}" 41 | fi 42 | 43 | shift 44 | done 45 | } 46 | 47 | readFromConfig() { 48 | local getterType 49 | getterType="$(cutVariable "${1}" "-" "5-")" 50 | 51 | if [ "${getterType}" = "properties" ]; then 52 | getterType="property" 53 | fi 54 | 55 | getterType="$(cutVariable "${getterType}" "s" "1")" 56 | cat "${DEPLOYMENT_CONF_PATH}/${getterType}" 57 | } 58 | 59 | main() { 60 | if [ "${1}" = '--version' ]; then 61 | printf '%s\n' "${INSTALLER_VERSION}" 62 | elif printf "%s" "${1}" | grep -q "^--get"; then 63 | readFromConfig "${1}" 64 | elif printf "%s" "${1}" | grep -q "^--set"; then 65 | saveToConfig "$@" 66 | fi 67 | } 68 | 69 | ################## 70 | ## SCRIPT START ## 71 | ################## 72 | 73 | main "$@" 74 | -------------------------------------------------------------------------------- /roles/oneagent/tests/command/windows/windows_command_wrapper.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from command.command_result import CommandResult 4 | from command.command_wrapper import CommandWrapper 5 | from command.windows.windows_command_executor import WindowsCommandExecutor 6 | from typing_extensions import override 7 | 8 | 9 | class WindowsCommandWrapper(CommandWrapper): 10 | def __init__(self, user: str, password: str): 11 | self.executor: WindowsCommandExecutor = WindowsCommandExecutor(user, password) 12 | 13 | @override 14 | def get_file_content(self, address: str, file: Path) -> CommandResult: 15 | return self.executor.execute(address, "type", str(file)) 16 | 17 | @override 18 | def file_exists(self, address: str, file: Path) -> CommandResult: 19 | # Windows needs double quoting for passing paths 20 | # containing spaces, single quotes don't work 21 | return self.executor.execute(address, f'if exist "{file}" (exit 0) else (exit 1)') 22 | 23 | @override 24 | def directory_exists(self, address: str, directory: Path) -> CommandResult: 25 | return self.executor.execute(address, f'if exist "{directory}\\*" (exit 0) else (exit 1)') 26 | 27 | def _run_directory_creation_command(self, address: str, directory: Path) -> CommandResult: 28 | result = CommandResult(0, "", "") 29 | if self.directory_exists(address, directory).returncode == 1: 30 | result = self.executor.execute(address, "md", str(directory)) 31 | 32 | return result 33 | 34 | @override 35 | def create_directory(self, address: str, directory: Path) -> CommandResult: 36 | for parent in list(directory.parents)[::-1][1::]: 37 | result = self._run_directory_creation_command(address, parent) 38 | if result.returncode != 0: 39 | return result 40 | return self._run_directory_creation_command(address, directory) 41 | 42 | @override 43 | def run_command(self, address: str, command: str, *args: str) -> CommandResult: 44 | return self.executor.execute(address, f'"{command}"', *args) 45 | -------------------------------------------------------------------------------- /roles/oneagent/tests/test_check_mode.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ansible.config import AnsibleConfigurator 4 | from ansible.runner import AnsibleRunner 5 | from deployment.deployment_operations import ( 6 | run_deployment, 7 | set_installer_download_params, 8 | ) 9 | 10 | 11 | def parse_changed_tasks(ansible_output: str): 12 | changed_tasks = [] 13 | current_task = None 14 | 15 | for line in ansible_output.splitlines(): 16 | if line.startswith("TASK [") and "]" in line: 17 | current_task = line.split("TASK [", 1)[1].split("]", 1)[0] 18 | 19 | if "changed:" in line and current_task: 20 | changed_tasks.append(current_task) 21 | 22 | return changed_tasks 23 | 24 | 25 | def collect_changed_tasks_from_results(results): 26 | all_changed_tasks = [] 27 | 28 | for i, res in enumerate(results): 29 | ansible_output = getattr(res, "stdout", "") 30 | if not isinstance(ansible_output, str): 31 | logging.warning("Unexpected stdout type in result[%d]: %s", i, type(ansible_output)) 32 | continue 33 | 34 | changed_tasks = parse_changed_tasks(ansible_output) 35 | if changed_tasks: 36 | logging.warning("Changed tasks found in result[%d]: %s", i, changed_tasks) 37 | all_changed_tasks.extend([(i, task) for task in changed_tasks]) 38 | 39 | return all_changed_tasks 40 | 41 | 42 | def test_check_mode( 43 | runner: AnsibleRunner, 44 | configurator: AnsibleConfigurator, 45 | installer_server_url: str, 46 | ): 47 | set_installer_download_params(configurator, installer_server_url) 48 | configurator.set_common_parameter(configurator.VALIDATE_DOWNLOAD_CERTS_KEY, False) 49 | configurator.set_common_parameter(configurator.PRESERVE_INSTALLER_KEY, True) 50 | 51 | logging.info("Running deployment with --check mode enabled") 52 | 53 | result = run_deployment(runner, configurator, check_mode=True) 54 | all_changed_tasks = collect_changed_tasks_from_results(result) 55 | 56 | assert not all_changed_tasks, f"Unexpected changed tasks found in check mode: {all_changed_tasks}" 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Release 16 | on: 17 | workflow_dispatch: 18 | inputs: 19 | ah_publish: 20 | description: Publish the collection on Automation Hub 21 | default: true 22 | type: boolean 23 | galaxy_publish: 24 | default: true 25 | description: Publish the collection on Galaxy 26 | type: boolean 27 | github_publish: 28 | description: Publish GitHub Release 29 | default: true 30 | type: boolean 31 | 32 | jobs: 33 | release-on-github: 34 | name: Release on GitHub 35 | runs-on: ubuntu-latest 36 | if: ${{ inputs.github_publish }} 37 | steps: 38 | - name: checkout 39 | uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 0 42 | - name: Make GitHub release 43 | uses: ./.github/actions/make-release 44 | with: 45 | gha_token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | release-on-ah: 48 | name: Release on Automation Hub 49 | uses: ansible/ansible-content-actions/.github/workflows/release_ah.yaml@main 50 | if: ${{ inputs.ah_publish }} 51 | with: 52 | environment: release 53 | secrets: 54 | ah_token: ${{ secrets.AH_TOKEN }} 55 | 56 | release-on-galaxy: 57 | name: Release on Galaxy 58 | uses: ansible/ansible-content-actions/.github/workflows/release_galaxy.yaml@main 59 | if: ${{ inputs.galaxy_publish }} 60 | with: 61 | environment: release 62 | secrets: 63 | ansible_galaxy_api_key: ${{ secrets.ANSIBLE_GALAXY_API_KEY }} 64 | -------------------------------------------------------------------------------- /roles/oneagent/tasks/provide-installer/provide-installer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if installer should be downloaded 3 | ansible.builtin.set_fact: 4 | _oneagent_skip_installer_download: >- 5 | {{ _oneagent_installed_agent_version.stdout | default('') | length > 0 and 6 | ((oneagent_version == 'latest' and oneagent_installer_metainfo_version | default('') | length > 0 and 7 | (_oneagent_installed_agent_version.stdout | default('') is version(oneagent_installer_metainfo_version, '>='))) or 8 | (oneagent_version != 'latest' and 9 | (_oneagent_installed_agent_version.stdout | default('') is version(oneagent_version, '>=')))) }} 10 | 11 | - name: Download OneAgent installer 12 | ansible.builtin.include_tasks: provide-installer/download.yml 13 | when: oneagent_local_installer|length == 0 and not _oneagent_skip_installer_download | bool 14 | 15 | - name: Transfer OneAgent installer 16 | ansible.builtin.include_tasks: provide-installer/transfer-{{ oneagent_system_family }}.yml 17 | when: oneagent_local_installer|length > 0 18 | 19 | - name: Verify OneAgent installer signature 20 | ansible.builtin.include_tasks: provide-installer/signature-{{ oneagent_system_family }}.yml 21 | when: oneagent_system_is_unix and oneagent_verify_signature and not _oneagent_skip_installer_download | bool 22 | 23 | - name: Gather new installer info 24 | ansible.builtin.include_tasks: provide-installer/new-installer-info-{{ oneagent_system_family }}.yml 25 | when: oneagent_local_installer|length > 0 or not _oneagent_skip_installer_download | bool 26 | 27 | - name: Compare versions of installed OneAgent and uploaded installer 28 | ansible.builtin.set_fact: 29 | _oneagent_is_installation_possible: >- 30 | {{ _oneagent_new_agent_version.stdout | default('') | length > 0 and 31 | (_oneagent_installed_agent_version.stdout | default('') | length == 0 or 32 | _oneagent_installed_agent_version.stdout is version(_oneagent_new_agent_version.stdout, '<')) }} 33 | 34 | - name: Determine deployment type 35 | ansible.builtin.set_fact: 36 | oneagent_is_operation_installation: "{{ _oneagent_is_installation_possible | bool }}" 37 | -------------------------------------------------------------------------------- /.github/actions/update-version/update_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import yaml 4 | import re 5 | import sys 6 | 7 | official_semver_regex = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" 8 | galaxy_yml = "galaxy.yml" 9 | version_yml = "roles/oneagent/vars/version.yml" 10 | 11 | 12 | class ListIndenter(yaml.Dumper): 13 | 14 | def increase_indent(self, flow=False, indentless=False): 15 | return super(ListIndenter, self).increase_indent(flow, False) 16 | 17 | 18 | def string_presenter(dumper, data): 19 | return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="") 20 | 21 | 22 | def update_version_in_yaml(file_path, version_field_name, version): 23 | with open(file_path, "r") as file: 24 | yaml_data = yaml.safe_load(file) 25 | yaml_data[version_field_name] = version 26 | 27 | yaml.add_representer(str, string_presenter) 28 | with open(file_path, "w") as file: 29 | yaml.dump( 30 | yaml_data, 31 | file, 32 | Dumper=ListIndenter, 33 | sort_keys=False, 34 | explicit_start=True, 35 | indent=2, 36 | default_flow_style=False, 37 | ) 38 | 39 | 40 | def update_galaxy_yaml(version): 41 | update_version_in_yaml(galaxy_yml, "version", version) 42 | 43 | 44 | def update_version_yaml(version): 45 | update_version_in_yaml(version_yml, "oneagent_script_version", version) 46 | 47 | 48 | if __name__ == "__main__": 49 | parser = argparse.ArgumentParser( 50 | prog="update_version", description="Update version in all needed places across the repository" 51 | ) 52 | 53 | parser.add_argument("new_version", help="New version in semver format") 54 | args = parser.parse_args() 55 | 56 | is_version_valid = re.match(official_semver_regex, args.new_version) 57 | if not is_version_valid: 58 | print("Version do not follow semver", file=sys.stderr) 59 | sys.exit(1) 60 | print(f"Apply new version: {args.new_version}") 61 | update_galaxy_yaml(args.new_version) 62 | update_version_yaml(args.new_version) 63 | -------------------------------------------------------------------------------- /.github/actions/get-app-versions/eol_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import requests 4 | import sys 5 | import json 6 | import re 7 | 8 | 9 | class ParseException(Exception): 10 | pass 11 | 12 | 13 | class Date: 14 | def __init__(self, date: str): 15 | pattern = r"(\d{4})-(\d{2})-(\d{2})" 16 | match = re.search(pattern, date) 17 | if match: 18 | year, month, day = match.groups() 19 | self.year = int(year) 20 | self.month = int(month) 21 | self.day = int(day) 22 | else: 23 | raise ParseException("Date is not properly formatted") 24 | 25 | def __gt__(self, other): 26 | if self.year < other.year: 27 | return False 28 | if self.year == other.year and self.month < other.month: 29 | return False 30 | if self.month == other.month and self.day <= other.day: 31 | return False 32 | return True 33 | 34 | 35 | def is_supported(val: dict) -> bool: 36 | return not val.get("isEol", True) 37 | 38 | 39 | def print_formatted(val): 40 | print(json.dumps(val)) 41 | 42 | 43 | def get_versions(app: str) -> dict: 44 | api_url = f"https://endoflife.date/api/v1/products/{app}" 45 | response = requests.get(api_url) 46 | value = None 47 | if response.status_code == requests.codes.ok: 48 | value = response.json() 49 | else: 50 | print(f"ERROR: {response.status_code}: {response.reason}", file=sys.stderr) 51 | return value 52 | 53 | 54 | if __name__ == "__main__": 55 | parser = argparse.ArgumentParser(description="Simple end of life api client") 56 | parser.add_argument("app", help="Application name") 57 | parser.add_argument("--get_latest", help="Get latest version", action="store_true") 58 | args = parser.parse_args() 59 | releases = get_versions(args.app)["result"]["releases"] 60 | if not releases: 61 | sys.exit(1) 62 | supported_versions = [release.get("name") for release in releases if is_supported(release)] 63 | if args.get_latest: 64 | latest_release = max(releases, key=lambda v: Date(v.get("releaseDate"))) 65 | print(latest_release.get("name")) 66 | else: 67 | print_formatted(supported_versions) 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## **1.2.5** 2025-11-21 (f903c8f0f8156ebfeb82a7c1638e5d94d085e47b...ab81691af95dae02a1f7cfbf644404b40edcaac6) 2 | 3 | ### Features 4 | 5 | - add retry mechanism for OneAgent installer download (79658309a21dff5f54d3758131643485a0623ac6) 6 | - Remove unintended config feature from the collection (5cb5d4e8f19b78ac717485b0a708e6d1b9b2bb56) 7 | - Add check for latest OneAgent version before download (3c7670931c7d274f365e16f1ee837acaea324009) 8 | 9 | ### Bug Fixes 10 | 11 | - fix broken conditionals \(\#105\) (7cd6ee37f063e34740678455a8fcadee9cf16f54) 12 | 13 |
14 | 15 | ## **1.2.4** 2025-04-30 (47a986a76f99ea3ffd5290c5d51585660050157f..2056802937c9096f48fe3a15ac31874bd7b97ffa) 16 | 17 | ### Features 18 | 19 | - Added parameter `oneagent_no_log` controlling Ansible no_log attribute 20 | 21 |
22 | 23 | ## **1.2.3** 2025-02-03 (08bc7b499a6c74e1d5eca815eaa47e380de6ba57..08bc7b499a6c74e1d5eca815eaa47e380de6ba57) 24 | 25 | ### Bug Fixes 26 | 27 | - Fixed issue with skipping CA certificate transfer task 28 | 29 |
30 | 31 | ## **1.2.2** 2025-10-1 (2effbf1975f46a4669c246722069a827ad0ffec3..1f71c5b165a870b0f11ccc51e06c5e1ddc2fe84f) 32 | 33 | ### Bug Fixes 34 | 35 | - Fixed linters issues 36 | 37 |
38 | 39 | ## **1.2.1** 2024-12-19 (a412454a7c1356a53b07c56d6f14b412684b1d79..df9f55f7ac08d58741eb87ae4b2ecba55bc23fad) 40 | 41 | ### Bug Fixes 42 | 43 | - Fixed problem with installer signature verification on AIX 44 | 45 |
46 | 47 | ## **1.2.0** 2024-11-29 (6f16a253a8ec3630817a2facfb590fbb0b3a473f..7f3bc5f0cc799a88c48d078f23deb2adfd6f3fc9) 48 | 49 | ### Features 50 | 51 | - Added parameter `--restart-service` parameter, once OneAgent configuration is performed 52 | - Added parameter `oneagent_verify_signature` controlling signature verification step 53 | - Removed `oneagent_validate_certs` parameter 54 | - Added ability for downloading installer's certificate if the certificate is not embedded in the collection 55 | 56 | ### Bug Fixes 57 | 58 | - Fixed problem with `Invalid version string` during collection install 59 | - Fixed problem with `dt-root.cert.pem` not being copied to the target host 60 | 61 |
-------------------------------------------------------------------------------- /roles/oneagent/tests/resources/installers/Dynatrace-OneAgent-Linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script acts as a self contained installer of the procuct 4 | 5 | set -eu 6 | 7 | readonly DEFAULT_INSTALL_DIR="/opt/dynatrace/oneagent" 8 | readonly INSTALLER_VERSION="##VERSION##" 9 | readonly DEPLOYMENT_CONF_PATH="/var/lib/dynatrace/oneagent/agent/config" 10 | 11 | readonly UNINSTALL_SCRIPT="uninstall.sh" 12 | UNINSTALL_CODE="$(cat <<-ENDUNINSTALL 13 | ##UNINSTALL_CODE## 14 | ENDUNINSTALL 15 | )" 16 | readonly UNINSTALL_CODE 17 | 18 | readonly ONEAGENTCTL_BIN="oneagentctl" 19 | ONEAGENTCTL_CODE="$(cat <<-ENDCTL 20 | ##ONEAGENTCTL_CODE## 21 | ENDCTL 22 | )" 23 | readonly ONEAGENTCTL_CODE 24 | 25 | CTL_PARAMS= 26 | INSTALL_DIR="${DEFAULT_INSTALL_DIR}" 27 | 28 | parseParams() { 29 | while [ $# -gt 0 ]; do 30 | local param="${1}" 31 | if [ "${param}" = "--version" ]; then 32 | printf "%s\n" "${INSTALLER_VERSION}" 33 | exit 0 34 | elif [ "${param}" = "INSTALL_PATH" ]; then 35 | INSTALL_DIR="$(printf "%s" "${param}" | cut -d "=" -f "2-")" 36 | elif printf "%s" "${param}" | grep -q -- "--set"; then 37 | CTL_PARAMS="${CTL_PARAMS} ${1}" 38 | fi 39 | shift 40 | done 41 | } 42 | 43 | uninstall() { 44 | local uninstallScript="${INSTALL_DIR}/agent/${UNINSTALL_SCRIPT}" 45 | if [ -f "${uninstallScript}" ]; then 46 | "${uninstallScript}" 47 | fi 48 | } 49 | 50 | deployOneagentCtl() { 51 | local ONEAGENTCTL_DIR="${INSTALL_DIR}/agent/tools" 52 | mkdir -p "${ONEAGENTCTL_DIR}" 53 | mkdir -p "${DEPLOYMENT_CONF_PATH}" 54 | printf '%s' "${ONEAGENTCTL_CODE}" >"${ONEAGENTCTL_DIR}/${ONEAGENTCTL_BIN}" 55 | chmod +x "${ONEAGENTCTL_DIR}/${ONEAGENTCTL_BIN}" 56 | } 57 | 58 | deployUninstallScript() { 59 | local UNINSTALL_DIR="${INSTALL_DIR}/agent" 60 | mkdir -p "${UNINSTALL_DIR}" 61 | printf '%s' "${UNINSTALL_CODE}" >"${UNINSTALL_DIR}/${UNINSTALL_SCRIPT}" 62 | chmod +x "${UNINSTALL_DIR}/${UNINSTALL_SCRIPT}" 63 | } 64 | 65 | # shellcheck disable=SC2086 66 | applyConfig() { 67 | "${INSTALL_DIR}/agent/tools/${ONEAGENTCTL_BIN}" ${CTL_PARAMS} 68 | } 69 | 70 | main() { 71 | parseParams "$@" 72 | uninstall 73 | deployOneagentCtl 74 | deployUninstallScript 75 | applyConfig 76 | exit 0 77 | } 78 | 79 | ################## 80 | ## SCRIPT START ## 81 | ################## 82 | 83 | main "$@" 84 | -------------------------------------------------------------------------------- /.github/workflows/update-version-and-changelog.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Update version and changelog 16 | on: 17 | workflow_dispatch: 18 | inputs: 19 | version: 20 | description: Version to be released (e.g. 1.2.3) 21 | required: true 22 | update_version: 23 | description: Update the version in galaxy.yml and roles/oneagent/vars/version.yml 24 | default: true 25 | type: boolean 26 | update_changelog: 27 | description: Update CHANGELOG.md 28 | default: true 29 | type: boolean 30 | 31 | jobs: 32 | git-changes: 33 | name: Update the collection version and the changelog 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: checkout 37 | uses: actions/checkout@v4 38 | with: 39 | fetch-depth: 0 40 | - name: Update version 41 | if: ${{ inputs.update_version }} 42 | uses: ./.github/actions/update-version 43 | with: 44 | version: ${{ inputs.version }} 45 | - name: Update changelog 46 | if: ${{ inputs.update_changelog }} 47 | uses: ./.github/actions/update-changelog 48 | with: 49 | version: ${{ inputs.version }} 50 | - name: Generate GitHub App token 51 | if: ${{ inputs.update_version || inputs.update_changelog }} 52 | id: app-token 53 | uses: actions/create-github-app-token@v1 54 | with: 55 | app-id: ${{ secrets.APP_ID }} 56 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 57 | - name: Commit changes 58 | if: ${{ inputs.update_version || inputs.update_changelog }} 59 | uses: ./.github/actions/commit-changes 60 | with: 61 | version: ${{ inputs.version }} 62 | gha_token: ${{ steps.app-token.outputs.token }} 63 | -------------------------------------------------------------------------------- /roles/oneagent/tests/ansible/runner.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | import os 4 | 5 | from ansible.config import AnsibleConfigurator 6 | from command.command_result import CommandResult 7 | from constants import CREDENTIALS_FILE_NAME, HOSTS_TEMPLATE_FILE_NAME, PLAYBOOK_TEMPLATE_FILE_NAME 8 | from deployment.deployment_platform import DeploymentResult 9 | 10 | 11 | class AnsibleRunner: 12 | def __init__(self, work_dir: str, user: str, password: str): 13 | self.work_dir = work_dir 14 | self.user = user 15 | self.password = password 16 | self.run_index = 0 17 | 18 | def _generate_file_name(self, file_name: str, new_extension: str = None) -> str: 19 | base_name, extension = os.path.splitext(file_name) 20 | if new_extension is not None: 21 | extension = new_extension 22 | return f"{base_name}_{self.run_index}{extension}" 23 | 24 | def run_deployment(self, configurator: AnsibleConfigurator, check_mode: bool = False) -> DeploymentResult: 25 | playbook_file_path = self._generate_file_name(PLAYBOOK_TEMPLATE_FILE_NAME) 26 | inventory_file_path = self._generate_file_name(HOSTS_TEMPLATE_FILE_NAME) 27 | log_file_path = self.work_dir / self._generate_file_name(PLAYBOOK_TEMPLATE_FILE_NAME, ".log") 28 | self.run_index += 1 29 | 30 | configurator.create_playbook_file(self.work_dir / playbook_file_path) 31 | configurator.create_inventory_file(self.work_dir / inventory_file_path) 32 | configurator.create_credentials_file(self.work_dir / CREDENTIALS_FILE_NAME) 33 | 34 | commandline = [ 35 | "ansible-playbook", 36 | "-i", 37 | inventory_file_path, 38 | playbook_file_path, 39 | ] 40 | if check_mode: 41 | commandline.append("--check") 42 | 43 | logging.info("Running '%s' in '%s'. ", " ".join(commandline), self.work_dir) 44 | 45 | res = subprocess.run( 46 | commandline, 47 | cwd=self.work_dir, 48 | stdout=subprocess.PIPE, 49 | stderr=subprocess.PIPE, 50 | universal_newlines=True, 51 | check=False, 52 | ) 53 | 54 | logging.info("Deployment finished") 55 | 56 | with log_file_path.open("w") as config: 57 | logging.info("Saving log to '%s'", log_file_path) 58 | config.write(res.stdout + res.stderr) 59 | 60 | return [CommandResult(res.returncode, res.stdout, res.stderr)] 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynatrace OneAgent Ansible collection 2 | 3 | Ansible collection for deploying Dynatrace OneAgent. 4 | 5 | ## Description 6 | 7 | The Dynatrace OneAgent Ansible collection consists of a single role that handles the installation and 8 | configuration of OneAgent, ensuring the OneAgent service remains in a running state. 9 | 10 | ## Requirements 11 | 12 | - `ansible` (any supported version, for more details see: https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html) 13 | - `pywinrm >= 0.4.3` (Windows only) 14 | 15 | ## Installation 16 | 17 | Before using this collection, you need to install it with the Ansible Galaxy command-line tool: 18 | 19 | ``` 20 | ansible-galaxy collection install dynatrace.oneagent 21 | ``` 22 | 23 | You can also include it in a requirements.yml file and install it with `ansible-galaxy collection install -r` 24 | requirements.yml, using the format: 25 | 26 | ```yaml 27 | collections: 28 | - name: dynatrace.oneagent 29 | ``` 30 | 31 | Note that if you install any collections from Ansible Galaxy, they will not be upgraded automatically when you upgrade 32 | the Ansible package. 33 | To upgrade the collection to the latest available version, run the following command: 34 | 35 | ``` 36 | ansible-galaxy collection install dynatrace.oneagent --upgrade 37 | ``` 38 | 39 | You can also install a specific version of the collection, for example, if you need to downgrade when something is 40 | broken in the latest version (please report an issue in this repository). Use the following syntax to install version 41 | 1.0.0: 42 | 43 | ``` 44 | ansible-galaxy collection install dynatrace.oneagent:==1.0.0 45 | ``` 46 | 47 | See [using Ansible collections](https://docs.ansible.com/ansible/devel/user_guide/collections_using.html) for more details. 48 | 49 | ## Use Cases 50 | 51 | See [OneAgent role README](roles/oneagent/README.md) for more details. 52 | 53 | ## Testing 54 | 55 | The collection was tested against Ansible sanity tests and component tests. The latter runs regular deployment with 56 | the installer and checks variety of installation scenarios. 57 | See [OneAgent role tests README](roles/oneagent/tests/README.md) for more details. 58 | 59 | ## Support 60 | 61 | You can submit a support request [here](https://www.dynatrace.com/support/contact-support). 62 | 63 | ## Release Notes and Roadmap 64 | 65 | For release notes see [CHANGELOG](CHANGELOG.md). 66 | 67 | ## Related Information 68 | 69 | See more on https://docs.dynatrace.com/docs/shortlink/oneagent-ansible. 70 | 71 | ## License Information 72 | 73 | This Ansible collection is published under Apache 2.0 license. See [LICENSE](LICENSE) for more details. 74 | -------------------------------------------------------------------------------- /roles/oneagent/tests/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from pathlib import Path 3 | 4 | COLLECTION_NAMESPACE = "dynatrace" 5 | COLLECTION_NAME = "oneagent" 6 | ROLE_NAME = "oneagent" 7 | 8 | ANSIBLE_USER_KEY = "ansible_user" 9 | ANSIBLE_PASS_KEY = "ansible_password" 10 | ANSIBLE_CONNECTION_KEY = "ansible_connection" 11 | 12 | HOSTS_TEMPLATE_FILE_NAME = "hosts.yml" 13 | CREDENTIALS_FILE_NAME = "credentials.yml" 14 | PLAYBOOK_TEMPLATE_FILE_NAME = "oneagent.yml" 15 | 16 | INSTALLED_COLLECTIONS_DIR_PATH = Path.home() / ".ansible" / "collections" 17 | COLLECTION_NAMESPACE_DIR_PATH = INSTALLED_COLLECTIONS_DIR_PATH / "ansible_collections" / COLLECTION_NAMESPACE 18 | ROLE_DIR_PATH = COLLECTION_NAMESPACE_DIR_PATH / COLLECTION_NAME / "roles" / ROLE_NAME 19 | TESTS_DIR_PATH = ROLE_DIR_PATH / "tests" 20 | 21 | FAILED_DEPLOYMENT_EXIT_CODE = 2 22 | SUCCESSFUL_DEPLOYMENT_EXIT_CODE = 0 23 | VARIABLE_PREFIX = f"{COLLECTION_NAME}_" 24 | 25 | RESOURCES_DIR_PATH = TESTS_DIR_PATH / "resources" 26 | ANSIBLE_RESOURCES_DIR_PATH = RESOURCES_DIR_PATH / "ansible" 27 | INSTALLERS_DIR_PATH = RESOURCES_DIR_PATH / "installers" 28 | 29 | INSTALLER_CERTIFICATE_FILE_NAME = "dt-root.cert.pem" 30 | INSTALLER_PRIVATE_KEY_FILE_NAME = "dt-root.key" 31 | SERVER_CERTIFICATE_FILE_NAME = "server.pem" 32 | SERVER_PRIVATE_KEY_FILE_NAME = "server.key" 33 | 34 | INSTALLER_PARTIAL_NAME = "Dynatrace-OneAgent" 35 | INSTALLER_SYSTEM_NAME_TYPE_MAP = {"linux": "linux", "unix": "linux", "aix": "aix", "windows": "windows"} 36 | 37 | INSTALLER_SERVER_TOKEN = "abcdefghijk1234567890" 38 | 39 | 40 | class InstallerVersion(Enum): 41 | OLD = "1.199.0.20241008-150308" 42 | LATEST = "1.300.0.20241008-150308" 43 | 44 | 45 | UNIX_DOWNLOAD_DIR_PATH = Path("/tmp") 46 | UNIX_INSTALL_DIR_PATH = Path("/opt") / "dynatrace" / "oneagent" 47 | UNIX_ONEAGENTCTL_BIN_NAME = "oneagentctl" 48 | UNIX_ONEAGENTCTL_PATH = UNIX_INSTALL_DIR_PATH / "agent" / "tools" / UNIX_ONEAGENTCTL_BIN_NAME 49 | 50 | WINDOWS_DOWNLOAD_DIR_PATH = Path("C:\\Windows\\Temp") 51 | WINDOWS_INSTALL_DIR_PATH = Path("C:\\Program Files") / "dynatrace" / "oneagent" 52 | WINDOWS_ONEAGENTCTL_BIN_NAME = "oneagentctl.exe" 53 | WINDOWS_ONEAGENTCTL_PATH = WINDOWS_INSTALL_DIR_PATH / "agent" / "tools" / WINDOWS_ONEAGENTCTL_BIN_NAME 54 | 55 | TEST_RUN_DIR_PATH = Path(__file__).parent.absolute() / "ansible_oneagent_tests_run_dir" 56 | WORK_INSTALLERS_DIR_PATH = TEST_RUN_DIR_PATH / "installers" 57 | WORK_SERVER_DIR_PATH = TEST_RUN_DIR_PATH / "server" 58 | WORK_DIR_PATH = TEST_RUN_DIR_PATH / "work_dir" 59 | 60 | TEST_SIGNATURE_FILE = ( 61 | COLLECTION_NAMESPACE_DIR_PATH 62 | / COLLECTION_NAME 63 | / "roles" 64 | / ROLE_NAME 65 | / "files" 66 | / INSTALLER_CERTIFICATE_FILE_NAME 67 | ) 68 | 69 | ERROR_MESSAGES_FILE_PATH = ROLE_DIR_PATH / "vars" / "messages.yml" 70 | INSTALLER_SIGNATURE_FILE_PATH = ROLE_DIR_PATH / "files" / INSTALLER_CERTIFICATE_FILE_NAME 71 | -------------------------------------------------------------------------------- /roles/oneagent/tests/README.md: -------------------------------------------------------------------------------- 1 | # Component tests 2 | 3 | The tests support two types of deployment: 4 | 5 | - local - the tests are run on the same Unix machine as main node; 6 | - remote - the tests are run on a remote Windows (Unix is not supported at the moment) machine; 7 | Currently, there is no option to mix these two types of deployment and the tests must be run for one platform at a time. 8 | 9 | ## Remote deployment 10 | 11 | For remote deployment, regular OneAgent installers are used, which are downloaded from the Dynatrace environment during 12 | the tests. To use this type of deployment, the following parameters must be provided: 13 | 14 | - `--user` - username for the remote machine; 15 | - `--password` - password for the remote machine; 16 | - `--tenant` - The environment URL from which the installer will be downloaded, in form of `https://abc123456.com`; 17 | - `--tenant_token` - Token for downloading the installer, generated in Deployment UI; 18 | - `--windows_x86=` - IP address of the remote Windows machine; 19 | Failing to provide any of these parameters will result in failure. 20 | 21 | ## Local deployment 22 | 23 | For local deployment, the tests are using mocked version of the OneAgent installer, which simulates its basic behavior - 24 | returning version, deploying `uninstall.sh` script and creating `oneagentctl`, used for configuring installation. 25 | To use this type of deployment, the only required parameter is `--linux_x86=localhost`. In case, multiple platforms for 26 | local deployment are specified or any other platforms is used along with local one, only the first local platform is used. 27 | 28 | ## Requirements 29 | 30 | - Python 3.10+ 31 | - pip 21.0+ 32 | - venv 20.0+ 33 | 34 | ## Running tests 35 | 36 | ### Preparing test environment 37 | 38 | Upon downloading the collection 39 | 40 | ```console 41 | # Install dependencies 42 | $ apt-get install -y python3-venv python3-pip sshpass 43 | 44 | # Create virtual environment 45 | $ python -m venv venv && source venv/bin/activate 46 | 47 | # Install requirements 48 | $ pip install -r roles/oneagent/tests/requirements.txt 49 | 50 | # Install ansible (any supported version, for more details see: https://endoflife.date/api/v1/products/ansible/) 51 | $ pip install ansible 52 | 53 | # Build and install the collection 54 | $ mkdir -p roles/oneagent/files && wget https://ca.dynatrace.com/dt-root.cert.pem -P roles/oneagent/files 55 | $ ansible-galaxy collection build . -vvvf 56 | $ sudo bash -c "source venv/bin/activate && ansible-galaxy collection install -vvvf dynatrace-oneagent*" 57 | ``` 58 | 59 | ### Running tests locally and remotely 60 | 61 | Running tests with regular manner requires one of the following commands: 62 | 63 | ```console 64 | # Run tests for any platform (except from Windows) on local machine 65 | $ sudo bash -c "source venv/bin/activate && pytest roles/oneagent/tests --linux_x86=localhost" 66 | 67 | # Run tests with regular installer on remote Windows machine 68 | $ sudo bash -c "source venv/bin/activate && pytest roles/oneagent/tests --user= --password= \ 69 | --tenant=https://abc123456.com --tenant_token= --windows_x86=" 70 | ``` 71 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Build and test" 16 | on: 17 | pull_request: 18 | workflow_dispatch: 19 | push: 20 | branches: ["master"] 21 | schedule: 22 | - cron: '0 7 * * 1' # Runs every Monday at 07:00 UTC 23 | 24 | 25 | jobs: 26 | fetch-ansible-versions: 27 | runs-on: ubuntu-latest 28 | name: "Fetch Ansible versions" 29 | outputs: 30 | supported_ansible_versions: ${{ steps.v.outputs.supported_ansible_versions }} 31 | latest_ansible_version : ${{ steps.v.outputs.latest_ansible_version }} 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: ./.github/actions/get-ansible-versions 35 | id: v 36 | 37 | build: 38 | needs: fetch-ansible-versions 39 | runs-on: ubuntu-latest 40 | name: Build with Ansible ${{ needs.fetch-ansible-versions.outputs.latest_ansible_version }} 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Set environment variables 44 | uses: ./.github/actions/set-environment-variables 45 | - name: Set up environment 46 | uses: ./.github/actions/set-up-build-environment 47 | with: 48 | ansible-version: ${{ needs.fetch-ansible-versions.outputs.latest_ansible_version }} 49 | - name: Build the collection 50 | uses: ./.github/actions/build-collection 51 | - name: Upload the collection 52 | uses: ./.github/actions/upload-collection 53 | 54 | test: 55 | name: Test with Ansible ${{ matrix.ansible-version }} 56 | needs: [fetch-ansible-versions, build] 57 | runs-on: ubuntu-latest 58 | strategy: 59 | matrix: 60 | ansible-version: ${{ fromJson(needs.fetch-ansible-versions.outputs.supported_ansible_versions) }} 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Set environment variables 64 | uses: ./.github/actions/set-environment-variables 65 | - name: Set up environment 66 | uses: ./.github/actions/set-up-build-environment 67 | with: 68 | ansible-version: ${{ matrix.ansible-version }} 69 | - name: Download the collection 70 | uses: ./.github/actions/download-collection 71 | - name: Run sanity tests 72 | uses: ./.github/actions/run-sanity-and-linter-tests 73 | - name: Run component tests 74 | uses: ./.github/actions/run-component-tests 75 | with: 76 | ansible-version: ${{ matrix.ansible-version }} 77 | 78 | # workaround for PR status check from dynamic matrix jobs (GitHub does not support entire workflow as a PR status check yet 79 | test-results: 80 | name: Test results 81 | runs-on: ubuntu-latest 82 | needs: test 83 | if: always() 84 | steps: 85 | - name: All tests ok 86 | if: ${{ !(contains(needs.*.result, 'failure')) }} 87 | run: exit 0 88 | - name: Some tests failed 89 | if: ${{ contains(needs.*.result, 'failure') }} 90 | run: exit 1 91 | -------------------------------------------------------------------------------- /roles/oneagent/tests/deployment/ssl_certificate_generator.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | import logging 3 | import uuid 4 | from dataclasses import dataclass 5 | from datetime import datetime, timedelta, timezone 6 | from pathlib import Path 7 | 8 | from cryptography import x509 9 | from cryptography.hazmat.primitives import hashes, serialization 10 | from cryptography.hazmat.primitives.asymmetric import rsa 11 | from cryptography.x509.oid import NameOID 12 | 13 | 14 | @dataclass 15 | class SSLCertificateInfo: 16 | country_name: str 17 | state_name: str 18 | locality_name: str 19 | organization_name: str 20 | common_name: str 21 | 22 | 23 | class SSLCertificateGenerator: 24 | def __init__(self, cert_info: SSLCertificateInfo, validity_days: int = 365): 25 | self.cert_info: SSLCertificateInfo = cert_info 26 | self.validity_days: int = validity_days 27 | 28 | def _generate_key_pair(self) -> tuple[rsa.RSAPrivateKey, rsa.RSAPublicKey]: 29 | private_key = rsa.generate_private_key( 30 | public_exponent=65537, 31 | key_size=2048, 32 | ) 33 | return private_key, private_key.public_key() 34 | 35 | def _generate_certificate(self, private_key: rsa.RSAPrivateKey, public_key: rsa.RSAPublicKey) -> x509.Certificate: 36 | x509_name = x509.Name( 37 | [ 38 | x509.NameAttribute(NameOID.COUNTRY_NAME, self.cert_info.country_name), 39 | x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, self.cert_info.state_name), 40 | x509.NameAttribute(NameOID.LOCALITY_NAME, self.cert_info.locality_name), 41 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, self.cert_info.organization_name), 42 | x509.NameAttribute(NameOID.COMMON_NAME, self.cert_info.common_name), 43 | ] 44 | ) 45 | 46 | builder = ( 47 | x509.CertificateBuilder() 48 | .subject_name(x509_name) 49 | .issuer_name(x509_name) 50 | .not_valid_before(datetime.now(timezone.utc)) 51 | .not_valid_after(datetime.now(timezone.utc) + timedelta(days=self.validity_days)) 52 | .serial_number(int(uuid.uuid4())) 53 | .public_key(public_key) 54 | .add_extension( 55 | x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address(self.cert_info.common_name))]), 56 | critical=False, 57 | ) 58 | ) 59 | 60 | return builder.sign( 61 | private_key=private_key, 62 | algorithm=hashes.SHA256(), 63 | ) 64 | 65 | def _save_private_key(self, filename: Path, private_key: rsa.RSAPrivateKey) -> None: 66 | with open(filename, "wb") as f: 67 | _unused = f.write( 68 | private_key.private_bytes( 69 | encoding=serialization.Encoding.PEM, 70 | format=serialization.PrivateFormat.TraditionalOpenSSL, 71 | encryption_algorithm=serialization.NoEncryption(), 72 | ) 73 | ) 74 | 75 | def _save_certificate(self, filename: Path, certificate: x509.Certificate) -> None: 76 | with open(filename, "wb") as f: 77 | _unused = f.write(certificate.public_bytes(serialization.Encoding.PEM)) 78 | 79 | def generate_and_save(self, private_key_path: Path, certificate_path: Path) -> None: 80 | logging.info("Generating self-signed certificate and key pair") 81 | private_key, public_key = self._generate_key_pair() 82 | self._save_private_key(private_key_path, private_key) 83 | self._save_certificate(certificate_path, self._generate_certificate(private_key, public_key)) 84 | -------------------------------------------------------------------------------- /roles/oneagent/tests/test_upgrade.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | from ansible.config import AnsibleConfigurator 5 | from ansible.runner import AnsibleRunner 6 | from command.platform_command_wrapper import PlatformCommandWrapper 7 | from deployment.deployment_operations import ( 8 | check_agent_state, 9 | check_download_directory, 10 | get_installers, 11 | get_oneagentctl_path, 12 | perform_operation_on_platforms, 13 | run_deployment, 14 | set_installer_download_params, 15 | ) 16 | from deployment.deployment_platform import DeploymentPlatform, PlatformCollection 17 | 18 | from constants import UNIX_DOWNLOAD_DIR_PATH, WINDOWS_DOWNLOAD_DIR_PATH 19 | 20 | 21 | def _get_versions_for_platforms(platforms: PlatformCollection, latest: bool) -> dict[DeploymentPlatform, str]: 22 | versions: dict[DeploymentPlatform, str] = {} 23 | for platform in platforms: 24 | installers = get_installers(platform.system(), platform.arch()) 25 | versioned_installer = installers[-1 if latest else 0] 26 | versions[platform] = re.search(r"\d.\d+.\d+.\d+-\d+", str(versioned_installer)).group() 27 | return versions 28 | 29 | 30 | def _check_agent_version( 31 | platform: DeploymentPlatform, 32 | address: str, 33 | wrapper: PlatformCommandWrapper, 34 | versions: dict[DeploymentPlatform, str], 35 | ) -> None: 36 | installed_version = wrapper.run_command(platform, address, f"{get_oneagentctl_path(platform)}", "--version") 37 | assert installed_version.stdout.strip() == versions[platform] 38 | 39 | 40 | def test_upgrade( 41 | runner: AnsibleRunner, 42 | configurator: AnsibleConfigurator, 43 | platforms: PlatformCollection, 44 | wrapper: PlatformCommandWrapper, 45 | installer_server_url: str, 46 | ): 47 | set_installer_download_params(configurator, installer_server_url) 48 | configurator.set_common_parameter(configurator.VALIDATE_DOWNLOAD_CERTS_KEY, False) 49 | 50 | old_versions = _get_versions_for_platforms(platforms, False) 51 | for platform, version in old_versions.items(): 52 | configurator.set_platform_parameter(platform, configurator.INSTALLER_VERSION_KEY, version) 53 | 54 | _unused = run_deployment(runner, configurator) 55 | 56 | logging.info("Check if agent is installed") 57 | perform_operation_on_platforms(platforms, check_agent_state, wrapper, True) 58 | 59 | logging.info("Check if agent has proper version") 60 | perform_operation_on_platforms(platforms, _check_agent_version, wrapper, old_versions) 61 | 62 | for platform in platforms: 63 | configurator.set_platform_parameter(platform, configurator.INSTALLER_VERSION_KEY, "latest") 64 | configurator.set_common_parameter(configurator.PRESERVE_INSTALLER_KEY, False) 65 | 66 | _unused = run_deployment(runner, configurator) 67 | 68 | logging.info("Check if agent is installed") 69 | perform_operation_on_platforms(platforms, check_agent_state, wrapper, True) 70 | 71 | logging.info("Check if agent has proper version") 72 | new_versions = _get_versions_for_platforms(platforms, True) 73 | perform_operation_on_platforms(platforms, _check_agent_version, wrapper, new_versions) 74 | 75 | for platform in platforms: 76 | configurator.set_platform_parameter(platform, configurator.INSTALLER_VERSION_KEY, "latest") 77 | configurator.set_common_parameter(configurator.PRESERVE_INSTALLER_KEY, True) 78 | 79 | _unused = run_deployment(runner, configurator) 80 | 81 | logging.info("Check if agent was not downloaded after second deployment") 82 | perform_operation_on_platforms( 83 | platforms, 84 | check_download_directory, 85 | wrapper, 86 | False, 87 | UNIX_DOWNLOAD_DIR_PATH, 88 | WINDOWS_DOWNLOAD_DIR_PATH, 89 | ) 90 | -------------------------------------------------------------------------------- /.github/actions/commit-changes/action.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: "Commit changes" 16 | inputs: 17 | version: 18 | description: "Version to be released (e.g. 1.2.3)" 19 | required: true 20 | gha_token: 21 | description: "GHA bot token" 22 | required: true 23 | 24 | runs: 25 | using: composite 26 | steps: 27 | - name: Setup git config 28 | shell: bash 29 | run: | 30 | git config user.name "${{ steps.app-token.outputs.app-slug }}[bot]" 31 | git config user.email "${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com" 32 | 33 | - name: Commit 34 | shell: bash 35 | run: | 36 | git add -u galaxy.yml roles/oneagent/vars/version.yml CHANGELOG.md 37 | git commit -m "chore: Update the collection version to $VERSION" 38 | git push origin HEAD:"$VERSION" 39 | env: 40 | VERSION: ${{ inputs.version }} 41 | 42 | - name: Create Pull Request 43 | shell: bash 44 | run: | 45 | gh pr create --title "Version update: $VERSION" --body "" --head "$VERSION" --base "$GITHUB_REF" 46 | for i in {1..60}; do 47 | PR_JSON=$(gh pr list --head "$VERSION" --state open --json number,url) 48 | PR_URL=$(echo "$PR_JSON" | jq -r '.[0].url') 49 | if [[ -n "$PR_URL" && "$PR_URL" != "null" ]]; then 50 | echo "Pull request created: $PR_URL" 51 | echo "PR_NUMBER=$(echo "$PR_JSON" | jq -r .[0].number)" >> "$GITHUB_ENV" 52 | echo "PR_URL=$PR_URL" >> "$GITHUB_ENV" 53 | exit 0 54 | fi 55 | echo "Waiting for pull request to be created..." 56 | sleep 10 57 | done 58 | 59 | echo "Pull request was not created within timeout." 60 | exit 1 61 | env: 62 | GH_TOKEN: ${{ inputs.gha_token }} 63 | VERSION: ${{ inputs.version }} 64 | 65 | - name: Enable auto-merge 66 | shell: bash 67 | run: | 68 | gh pr merge "$PR_URL" --rebase --auto --delete-branch 69 | env: 70 | GH_TOKEN: ${{ inputs.gha_token }} 71 | 72 | - name: Wait for PR to be merged 73 | shell: bash 74 | run: | 75 | echo "Waiting for PR #${PR_NUMBER} to be merged..." 76 | for i in {1..60}; do 77 | STATE=$(gh pr view $PR_NUMBER --json state -q '.state') 78 | if [ "$STATE" = "MERGED" ]; then 79 | echo "PR merged!" 80 | exit 0 81 | fi 82 | sleep 10 83 | done 84 | echo "PR #${PR_NUMBER} was not merged within timeout." 85 | exit 1 86 | env: 87 | GH_TOKEN: ${{ inputs.gha_token }} 88 | 89 | - name: Add git tag 90 | shell: bash 91 | run: | 92 | MERGE_SHA=$(gh pr view $PR_NUMBER --json mergeCommit -q '.mergeCommit.oid') 93 | git fetch origin "$MERGE_SHA" 94 | git checkout "$MERGE_SHA" 95 | git tag "v$VERSION" 96 | git push origin "v$VERSION" 97 | env: 98 | GH_TOKEN: ${{ inputs.gha_token }} 99 | VERSION: ${{ inputs.version }} 100 | 101 | - name: Delete version branch 102 | if: always() 103 | shell: bash 104 | run: | 105 | git branch -D "$VERSION" || true 106 | git push origin --delete "$VERSION" || true 107 | env: 108 | VERSION: ${{ inputs.version }} 109 | 110 | -------------------------------------------------------------------------------- /roles/oneagent/tests/ansible/config.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from constants import ( 4 | ANSIBLE_CONNECTION_KEY, 5 | ANSIBLE_PASS_KEY, 6 | ANSIBLE_RESOURCES_DIR_PATH, 7 | ANSIBLE_USER_KEY, 8 | CREDENTIALS_FILE_NAME, 9 | HOSTS_TEMPLATE_FILE_NAME, 10 | PLAYBOOK_TEMPLATE_FILE_NAME 11 | ) 12 | from deployment.deployment_platform import DeploymentPlatform, PlatformCollection 13 | from resources.file_operations import read_yaml_file, write_yaml_file 14 | 15 | 16 | class AnsibleConfigurator: 17 | HOSTS_PARAM_KEY = "hosts" 18 | PARAM_SECTION_KEY = "vars" 19 | 20 | # Platform-agnostic 21 | ENVIRONMENT_URL_KEY = "oneagent_environment_url" 22 | INSTALLER_ARGS_KEY = "oneagent_install_args" 23 | INSTALLER_VERSION_KEY = "oneagent_version" 24 | PAAS_TOKEN_KEY = "oneagent_paas_token" 25 | PACKAGE_STATE_KEY = "oneagent_package_state" 26 | PRESERVE_INSTALLER_KEY = "oneagent_preserve_installer" 27 | VERIFY_SIGNATURE_KEY = "oneagent_verify_signature" 28 | 29 | # Internal parameters 30 | CA_CERT_DOWNLOAD_CERT_KEY = "oneagent_ca_cert_download_cert" 31 | CA_CERT_DOWNLOAD_URL_KEY = "oneagent_ca_cert_download_url" 32 | FORCE_CERT_DOWNLOAD_KEY = "oneagent_force_cert_download" 33 | INSTALLER_DOWNLOAD_CERT_KEY = "oneagent_installer_download_cert" 34 | VALIDATE_DOWNLOAD_CERTS_KEY = "oneagent_validate_certs" 35 | 36 | # Platform-specific 37 | DOWNLOAD_DIR_KEY = "oneagent_download_dir" 38 | INSTALLER_ARCH_KEY = "oneagent_installer_arch" 39 | INSTALLER_PLATFORM_ARGS_KEY = "oneagent_platform_install_args" 40 | LOCAL_INSTALLER_KEY = "oneagent_local_installer" 41 | 42 | def __init__(self, user: str, password: str, platforms: PlatformCollection): 43 | self.user = user 44 | self.password = password 45 | self.platforms = platforms 46 | self.playbook = read_yaml_file(ANSIBLE_RESOURCES_DIR_PATH / PLAYBOOK_TEMPLATE_FILE_NAME) 47 | self.inventory = read_yaml_file(ANSIBLE_RESOURCES_DIR_PATH / HOSTS_TEMPLATE_FILE_NAME) 48 | self._prepare_inventory(self.user, self.platforms) 49 | self.credentials = read_yaml_file(ANSIBLE_RESOURCES_DIR_PATH / CREDENTIALS_FILE_NAME) 50 | self._prepare_credentials(self.user, self.password) 51 | 52 | def _prepare_inventory(self, user: str, platforms: PlatformCollection) -> None: 53 | for platform, hosts in platforms.items(): 54 | group_data = self.inventory["all"]["children"][platform.family()]["children"][platform.value] 55 | group_data["hosts"] = {k: None for k in hosts} 56 | group_data["vars"][ANSIBLE_USER_KEY] = user 57 | # TODO: Add condition to fail test if localhost is used with multiple platforms 58 | # We assume that localhost is used only with single platform 59 | if "localhost" in hosts: 60 | group_data = self.inventory["all"]["children"][platform.family()]["vars"] 61 | group_data[ANSIBLE_CONNECTION_KEY] = "local" 62 | 63 | def _prepare_credentials(self, user: str, password: str) -> None: 64 | self.credentials[ANSIBLE_USER_KEY] = user 65 | self.credentials[ANSIBLE_PASS_KEY] = password 66 | 67 | def set_common_parameter(self, key: str, value: Any) -> None: 68 | self.playbook[0][AnsibleConfigurator.PARAM_SECTION_KEY][key] = value 69 | 70 | def set_platform_parameter(self, platform: DeploymentPlatform, key: str, value: Any) -> None: 71 | group_data = self.inventory["all"]["children"][platform.family()]["children"][platform.value] 72 | group_data[self.PARAM_SECTION_KEY][key] = value 73 | 74 | def set_deployment_hosts(self, hosts: str) -> None: 75 | self.playbook[0][self.HOSTS_PARAM_KEY] = hosts 76 | 77 | def create_playbook_file(self, path: str) -> None: 78 | write_yaml_file(path, self.playbook) 79 | 80 | def create_inventory_file(self, path: str) -> None: 81 | write_yaml_file(path, self.inventory) 82 | 83 | def create_credentials_file(self, path: str) -> None: 84 | write_yaml_file(path, self.credentials) 85 | -------------------------------------------------------------------------------- /roles/oneagent/tests/installer_server/server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from collections.abc import Generator 4 | from http import HTTPStatus 5 | 6 | from flask import Blueprint, Flask, Response, request, send_file 7 | from constants import ( 8 | INSTALLER_CERTIFICATE_FILE_NAME, 9 | SERVER_CERTIFICATE_FILE_NAME, 10 | SERVER_PRIVATE_KEY_FILE_NAME, 11 | WORK_INSTALLERS_DIR_PATH, 12 | WORK_SERVER_DIR_PATH, 13 | ) 14 | from deployment.deployment_operations import get_installers 15 | from deployment.ssl_certificate_generator import ( 16 | SSLCertificateGenerator, 17 | SSLCertificateInfo, 18 | ) 19 | from werkzeug.serving import make_server 20 | 21 | app = Flask(__name__) 22 | installer_bp = Blueprint("installer", __name__) 23 | certificate_bp = Blueprint("certificate", __name__) 24 | 25 | TransferResult = tuple[str, HTTPStatus] | Response 26 | 27 | 28 | def get_installer(system: str, arch: str, version: str) -> TransferResult: 29 | logging.info("Getting installer for system %s_%s in %s version", system, arch, version) 30 | 31 | installers = get_installers(system, arch, version, True) 32 | 33 | if len(installers) > 0: 34 | logging.info("Serving installer %s", installers[-1]) 35 | return send_file(installers[-1]) 36 | 37 | msg = f"Installer for system {system} in {version} version was not found" 38 | logging.warning(msg) 39 | return msg, HTTPStatus.NOT_FOUND 40 | 41 | 42 | @certificate_bp.route(f"/{INSTALLER_CERTIFICATE_FILE_NAME}") 43 | def get_ca_certificate() -> TransferResult: 44 | cert_file = WORK_INSTALLERS_DIR_PATH / INSTALLER_CERTIFICATE_FILE_NAME 45 | if not cert_file.exists(): 46 | logging.warning("%s not found", cert_file) 47 | return f"{cert_file} not found", HTTPStatus.NOT_FOUND 48 | return send_file(cert_file) 49 | 50 | 51 | @installer_bp.route("//default/latest") 52 | def get_latest_agent(system: str) -> TransferResult: 53 | return get_installer(system, request.args["arch"], "latest") 54 | 55 | 56 | @installer_bp.route("//default/version/") 57 | def get_agent_in_version(system: str, version: str) -> TransferResult: 58 | return get_installer(system, request.args["arch"], version) 59 | 60 | 61 | @installer_bp.route("//default/latest/metainfo") 62 | def get_latest_agent_metainfo(system: str) -> TransferResult: 63 | logging.info("Getting metainfo for latest installer - %s_%s, ", system, request.args["arch"]) 64 | single_installer = get_installers(system, request.args["arch"], "latest")[0] 65 | version_parts = single_installer.stem.split("-") 66 | return {"latestAgentVersion": f"{version_parts[3]}-{version_parts[4]}"}, HTTPStatus.OK 67 | 68 | 69 | def run_server(ip_address: str, port: int) -> Generator[None, None, None]: 70 | logging.info("Starting server on %s:%d", ip_address, port) 71 | 72 | ssl_info = SSLCertificateInfo( 73 | country_name="US", 74 | state_name="California", 75 | locality_name="San Francisco", 76 | organization_name="Dynatrace", 77 | common_name=ip_address, 78 | ) 79 | ssl_generator = SSLCertificateGenerator(ssl_info, validity_days=365) 80 | ssl_generator.generate_and_save( 81 | WORK_SERVER_DIR_PATH / SERVER_PRIVATE_KEY_FILE_NAME, 82 | WORK_SERVER_DIR_PATH / SERVER_CERTIFICATE_FILE_NAME, 83 | ) 84 | 85 | ssl_context = ( 86 | f"{WORK_SERVER_DIR_PATH / SERVER_CERTIFICATE_FILE_NAME}", 87 | f"{WORK_SERVER_DIR_PATH / SERVER_PRIVATE_KEY_FILE_NAME}", 88 | ) 89 | app.register_blueprint(installer_bp, url_prefix="/api/v1/deployment/installer/agent") 90 | app.register_blueprint(certificate_bp) 91 | 92 | server = make_server(ip_address, port, app, ssl_context=ssl_context) 93 | server_thread = threading.Thread(target=server.serve_forever, daemon=True) 94 | server_thread.start() 95 | 96 | logging.info("Server thread started on %s:%d", ip_address, port) 97 | 98 | yield 99 | 100 | logging.info("Shutting down the server") 101 | server.shutdown() 102 | server_thread.join() 103 | -------------------------------------------------------------------------------- /roles/oneagent/tests/test_install_and_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from ansible.config import AnsibleConfigurator 5 | from ansible.runner import AnsibleRunner 6 | from command.platform_command_wrapper import PlatformCommandWrapper 7 | from constants import ( 8 | INSTALLER_SERVER_TOKEN, 9 | UNIX_DOWNLOAD_DIR_PATH, 10 | WINDOWS_DOWNLOAD_DIR_PATH, 11 | ) 12 | from deployment.deployment_operations import ( 13 | check_agent_state, 14 | check_download_directory, 15 | get_oneagentctl_path, 16 | select_by_platform, 17 | perform_operation_on_platforms, 18 | run_deployment, 19 | set_installer_download_params, 20 | ) 21 | from deployment.deployment_platform import ( 22 | DeploymentPlatform, 23 | DeploymentResult, 24 | PlatformCollection, 25 | ) 26 | 27 | CTL_OPTION_GET_HOST_TAGS = "--get-host-tags" 28 | CTL_OPTION_SET_HOST_TAG = "--set-host-tag" 29 | 30 | CTL_OPTION_GET_HOST_PROPERTIES = "--get-host-properties" 31 | CTL_OPTION_SET_HOST_PROPERTY = "--set-host-property" 32 | 33 | 34 | def _assert_oneagentctl_getter( 35 | platform: DeploymentPlatform, 36 | address: str, 37 | wrapper: PlatformCommandWrapper, 38 | ctl_param: str, 39 | expected_values: set[str], 40 | ): 41 | oneagentctl = f"{get_oneagentctl_path(platform)}" 42 | result = wrapper.run_command(platform, address, oneagentctl, ctl_param) 43 | assert result.returncode == 0 44 | assert expected_values == set(result.stdout.strip().splitlines()) 45 | 46 | 47 | def _check_install_args( 48 | platform: DeploymentPlatform, 49 | address: str, 50 | wrapper: PlatformCommandWrapper, 51 | ) -> None: 52 | logging.debug("Platform: %s, IP: %s", platform, address) 53 | 54 | oneagentctl = f"{get_oneagentctl_path(platform)}" 55 | metadata = wrapper.run_command(platform, address, oneagentctl, "--get-deployment-metadata") 56 | assert metadata.returncode == 0 57 | 58 | params = dict(kv.split("=") for kv in metadata.stdout.strip().splitlines()) 59 | 60 | 61 | def _check_config_args( 62 | platform: DeploymentPlatform, 63 | address: str, 64 | wrapper: PlatformCommandWrapper, 65 | expected_tags: set[str], 66 | expected_properties: set[str], 67 | ): 68 | logging.debug("Platform: %s, IP: %s", platform, address) 69 | 70 | _assert_oneagentctl_getter(platform, address, wrapper, CTL_OPTION_GET_HOST_TAGS, expected_tags) 71 | _assert_oneagentctl_getter(platform, address, wrapper, CTL_OPTION_GET_HOST_PROPERTIES, expected_properties) 72 | 73 | 74 | def _check_output_for_secrets(result: DeploymentResult, installer_server_url: str) -> None: 75 | for out in result: 76 | assert INSTALLER_SERVER_TOKEN not in out.stdout 77 | assert installer_server_url not in out.stderr 78 | 79 | 80 | def test_basic_installation( 81 | runner: AnsibleRunner, 82 | configurator: AnsibleConfigurator, 83 | platforms: PlatformCollection, 84 | wrapper: PlatformCommandWrapper, 85 | installer_server_url: str, 86 | ): 87 | dummy_common_tag = "dummy_common_tag" 88 | dummy_platform_tag = "dummy_platform_tag" 89 | dummy_common_property = "dummy_common_key=dummy_common_value" 90 | dummy_platform_property = "dummy_platform_key=dummy_platform_value" 91 | 92 | set_installer_download_params(configurator, installer_server_url) 93 | configurator.set_common_parameter(configurator.VALIDATE_DOWNLOAD_CERTS_KEY, False) 94 | configurator.set_common_parameter(configurator.PRESERVE_INSTALLER_KEY, True) 95 | configurator.set_common_parameter( 96 | configurator.INSTALLER_ARGS_KEY, 97 | [ 98 | f"{CTL_OPTION_SET_HOST_TAG}={dummy_common_tag}", 99 | f"{CTL_OPTION_SET_HOST_PROPERTY}={dummy_common_property}", 100 | ], 101 | ) 102 | 103 | for platform in platforms: 104 | download_dir: Path = select_by_platform(platform, UNIX_DOWNLOAD_DIR_PATH, WINDOWS_DOWNLOAD_DIR_PATH) 105 | configurator.set_platform_parameter(platform, configurator.DOWNLOAD_DIR_KEY, str(download_dir)) 106 | configurator.set_common_parameter( 107 | configurator.INSTALLER_PLATFORM_ARGS_KEY, 108 | [ 109 | f"{CTL_OPTION_SET_HOST_TAG}={dummy_platform_tag}", 110 | f"{CTL_OPTION_SET_HOST_PROPERTY}={dummy_platform_property}", 111 | ], 112 | ) 113 | 114 | result = run_deployment(runner, configurator) 115 | 116 | logging.info("Check if output contains secrets") 117 | _check_output_for_secrets(result, installer_server_url) 118 | 119 | logging.info("Check if agent is installed") 120 | perform_operation_on_platforms(platforms, check_agent_state, wrapper, True) 121 | 122 | logging.info("Check if installer was downloaded to correct place and preserved") 123 | perform_operation_on_platforms( 124 | platforms, 125 | check_download_directory, 126 | wrapper, 127 | True, 128 | UNIX_DOWNLOAD_DIR_PATH, 129 | WINDOWS_DOWNLOAD_DIR_PATH, 130 | ) 131 | 132 | logging.info("Check if config args were applied correctly") 133 | perform_operation_on_platforms( 134 | platforms, 135 | _check_config_args, 136 | wrapper, 137 | {dummy_common_tag, dummy_platform_tag}, 138 | {dummy_common_property, dummy_platform_property}, 139 | ) 140 | 141 | logging.info("Check if installer args were passed correctly") 142 | perform_operation_on_platforms(platforms, _check_install_args, wrapper) 143 | -------------------------------------------------------------------------------- /roles/oneagent/tests/deployment/installer_fetching.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | from pathlib import Path 4 | 5 | import requests 6 | from constants import ( 7 | INSTALLER_CERTIFICATE_FILE_NAME, 8 | INSTALLER_PRIVATE_KEY_FILE_NAME, 9 | INSTALLERS_DIR_PATH, 10 | WORK_INSTALLERS_DIR_PATH, 11 | InstallerVersion, 12 | ) 13 | from deployment.deployment_platform import DeploymentPlatform, PlatformCollection 14 | from deployment.ssl_certificate_generator import ( 15 | SSLCertificateGenerator, 16 | SSLCertificateInfo, 17 | ) 18 | 19 | 20 | def get_file_content(path: Path) -> list[str]: 21 | with path.open("r") as f: 22 | return f.readlines() 23 | 24 | 25 | def replace_tag(source: list[str], old: str, new: str) -> list[str]: 26 | return [line.replace(old, new) for line in source] 27 | 28 | 29 | def sign_installer(installer: list[str]) -> list[str]: 30 | cmd = [ 31 | "openssl", 32 | "cms", 33 | "-sign", 34 | "-signer", 35 | f"{WORK_INSTALLERS_DIR_PATH / INSTALLER_CERTIFICATE_FILE_NAME}", 36 | "-inkey", 37 | f"{WORK_INSTALLERS_DIR_PATH / INSTALLER_PRIVATE_KEY_FILE_NAME}", 38 | ] 39 | 40 | proc = subprocess.run( 41 | cmd, 42 | input=f"{''.join(installer)}", 43 | encoding="utf-8", 44 | stdout=subprocess.PIPE, 45 | stderr=subprocess.STDOUT, 46 | check=False, 47 | ) 48 | if proc.returncode != 0: 49 | logging.error("Failed to sign installer: %s") 50 | return [] 51 | 52 | signed_installer = proc.stdout.splitlines() 53 | delimiter = next(l for l in signed_installer if l.startswith("----")) 54 | index = signed_installer.index(delimiter) 55 | signed_installer = signed_installer[index + 1 :] 56 | 57 | custom_delimiter = "----SIGNED-INSTALLER" 58 | 59 | return [ 60 | f"{l}\n" if not l.startswith(delimiter) else f"{l.replace(delimiter, custom_delimiter)}\n" 61 | for l in signed_installer 62 | ] 63 | 64 | 65 | def generate_installers() -> bool: 66 | uninstall_template = get_file_content(INSTALLERS_DIR_PATH / "uninstall.sh") 67 | uninstall_code = replace_tag(uninstall_template, "$", r"\$") 68 | 69 | oneagentctl_template = get_file_content(INSTALLERS_DIR_PATH / "oneagentctl.sh") 70 | oneagentctl_code = replace_tag(oneagentctl_template, "$", r"\$") 71 | 72 | installer_partial_name = "Dynatrace-OneAgent-Linux" 73 | installer_template = get_file_content(INSTALLERS_DIR_PATH / f"{installer_partial_name}.sh") 74 | installer_template = replace_tag(installer_template, "##UNINSTALL_CODE##", "".join(uninstall_code)) 75 | installer_template = replace_tag(installer_template, "##ONEAGENTCTL_CODE##", "".join(oneagentctl_code)) 76 | 77 | ssl_info = SSLCertificateInfo( 78 | country_name="US", 79 | state_name="California", 80 | locality_name="San Francisco", 81 | organization_name="Dynatrace", 82 | common_name="127.0.0.1", 83 | ) 84 | ssl_generator = SSLCertificateGenerator(ssl_info, validity_days=365) 85 | 86 | ssl_generator.generate_and_save( 87 | WORK_INSTALLERS_DIR_PATH / INSTALLER_PRIVATE_KEY_FILE_NAME, 88 | WORK_INSTALLERS_DIR_PATH / INSTALLER_CERTIFICATE_FILE_NAME, 89 | ) 90 | 91 | # REMOVE INSTALLER VERSION 92 | for version in InstallerVersion: 93 | installer_code = replace_tag(installer_template, "##VERSION##", version.value) 94 | installer_code = sign_installer(installer_code) 95 | if not installer_code: 96 | return False 97 | with open(WORK_INSTALLERS_DIR_PATH / f"{installer_partial_name}-{version.value}.sh", "w") as f: 98 | f.writelines(installer_code) 99 | 100 | return True 101 | 102 | 103 | def get_installers_versions_from_tenant(tenant: str, tenant_token: str, system_family: str) -> list[str]: 104 | url = f"{tenant}/api/v1/deployment/installer/agent/versions/{system_family}/default" 105 | headers = {"accept": "application/json", "Authorization": f"Api-Token {tenant_token}"} 106 | 107 | resp = requests.get(url, headers=headers) 108 | 109 | try: 110 | versions = resp.json()["availableVersions"] 111 | if len(versions) < 2: 112 | logging.error("List of available installers is too short: %s", versions) 113 | else: 114 | return [versions[0], versions[-1]] 115 | except KeyError: 116 | logging.error("Failed to get list of installer versions: %s", resp.content) 117 | return [] 118 | 119 | 120 | def download_and_save(path: Path, url: str, headers: dict[str, str]) -> bool: 121 | resp = requests.get(url, headers=headers) 122 | 123 | if not resp.ok: 124 | logging.error("Failed to download file %s: %s", path, resp.text) 125 | return False 126 | 127 | with path.open("wb") as f: 128 | _unused = f.write(resp.content) 129 | return True 130 | 131 | 132 | def download_signature(url: str) -> bool: 133 | path = WORK_INSTALLERS_DIR_PATH / INSTALLER_CERTIFICATE_FILE_NAME 134 | return download_and_save(path, url, {}) 135 | 136 | 137 | def download_installer(tenant: str, tenant_token: str, version: str, platform: DeploymentPlatform) -> bool: 138 | family = platform.family() 139 | url = f"{tenant}/api/v1/deployment/installer/agent/{family}/default/version/{version}" 140 | headers = {"accept": "application/octet-stream", "Authorization": f"Api-Token {tenant_token}"} 141 | 142 | ext = "exe" if family == "windows" else "sh" 143 | path = WORK_INSTALLERS_DIR_PATH / f"Dynatrace-OneAgent-{family}_{platform.arch()}-{version}.{ext}" 144 | 145 | return download_and_save(path, url, headers) 146 | 147 | 148 | def download_installers(tenant: str, tenant_token: str, platforms: PlatformCollection) -> bool: 149 | for platform in platforms: 150 | versions = get_installers_versions_from_tenant(tenant, tenant_token, platform.family()) 151 | if not versions: 152 | return False 153 | for version in versions: 154 | if not download_installer(tenant, tenant_token, version, platform): 155 | return False 156 | return True 157 | -------------------------------------------------------------------------------- /roles/oneagent/tests/deployment/deployment_operations.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import os 4 | from pathlib import Path 5 | from typing import Any, Callable, TypeVar, cast 6 | 7 | from ansible.config import AnsibleConfigurator 8 | from ansible.runner import AnsibleRunner 9 | from command.platform_command_wrapper import PlatformCommandWrapper 10 | from constants import ( 11 | INSTALLER_CERTIFICATE_FILE_NAME, 12 | INSTALLER_PARTIAL_NAME, 13 | INSTALLER_SERVER_TOKEN, 14 | INSTALLER_SYSTEM_NAME_TYPE_MAP, 15 | SERVER_CERTIFICATE_FILE_NAME, 16 | UNIX_ONEAGENTCTL_PATH, 17 | WINDOWS_ONEAGENTCTL_PATH, 18 | WORK_DIR_PATH, 19 | WORK_INSTALLERS_DIR_PATH, 20 | WORK_SERVER_DIR_PATH, 21 | ) 22 | from deployment.deployment_platform import ( 23 | DeploymentPlatform, 24 | DeploymentResult, 25 | PlatformCollection, 26 | ) 27 | 28 | CallableOperation = Callable[[DeploymentPlatform, str, Any], None] 29 | 30 | 31 | def prepare_test_dirs(test_case_name: str) -> None: 32 | Path(WORK_DIR_PATH / test_case_name).mkdir(parents=True, exist_ok=True) 33 | os.chdir(WORK_DIR_PATH) 34 | 35 | 36 | def get_oneagentctl_path(platform: DeploymentPlatform) -> Path: 37 | return select_by_platform(platform, UNIX_ONEAGENTCTL_PATH, WINDOWS_ONEAGENTCTL_PATH) 38 | 39 | 40 | PlatformSpecificValue = TypeVar("PlatformSpecificValue") 41 | 42 | 43 | def select_by_platform( 44 | platform: DeploymentPlatform, unix_value: PlatformSpecificValue, windows_value: PlatformSpecificValue 45 | ) -> PlatformSpecificValue: 46 | return windows_value if platform == DeploymentPlatform.WINDOWS_X86 else unix_value 47 | 48 | 49 | def _get_platform_by_installer(installer: Path) -> DeploymentPlatform: 50 | name = installer.name.lower() 51 | for platform in DeploymentPlatform: 52 | if platform.arch() in name and platform.system() in name: 53 | return platform 54 | 55 | # Special handling for Linux_x86 and Windows as the installer does not 56 | # contain architecture in its name 57 | if DeploymentPlatform.WINDOWS_X86.system() in name: 58 | return DeploymentPlatform.WINDOWS_X86 59 | return DeploymentPlatform.LINUX_X86 60 | 61 | 62 | def _get_available_installers() -> dict[DeploymentPlatform, list[Path]]: 63 | installers: dict[DeploymentPlatform, list[Path]] = {platform: [] for platform in DeploymentPlatform} 64 | for installer in sorted(WORK_INSTALLERS_DIR_PATH.glob(f"{INSTALLER_PARTIAL_NAME}*")): 65 | platform = _get_platform_by_installer(installer) 66 | installers[platform].append(installer) 67 | return installers 68 | 69 | 70 | def get_installers(system: str, arch: str, version: str = "", include_paths: bool = False) -> list[Path]: 71 | try: 72 | # Special handling for mocking server behavior as URL for Linux 73 | # installers contains "unix" instead of linux 74 | system = INSTALLER_SYSTEM_NAME_TYPE_MAP[system] 75 | platform_installers = _get_available_installers()[DeploymentPlatform.from_system_and_arch(system, arch)] 76 | installers = platform_installers if include_paths else [Path(ins.name) for ins in platform_installers] 77 | if not version: 78 | return installers 79 | if version == "latest": 80 | return [installers[-1]] 81 | return [installer for installer in installers if version in str(installer)] 82 | 83 | except Exception as ex: 84 | logging.error("Failed to get installer for %s_%s in %s version: %s", system, arch, version, ex) 85 | return [] 86 | 87 | 88 | def _get_kwarg_by_name(name: str, **kwargs: object) -> object: 89 | assert name in kwargs, f"No '{name}' parameter in parameters list" 90 | return kwargs[name] 91 | 92 | 93 | def enable_for_system_family(family: str) -> Callable[[Callable[..., None]], Callable[..., None]]: 94 | def func_wrapper(func: Callable[..., None]) -> Callable[..., None]: 95 | @functools.wraps(func) 96 | def params_wrapper(*args: object, **kwargs: object) -> None: 97 | config = cast(AnsibleConfigurator, _get_kwarg_by_name("configurator", **kwargs)) 98 | platforms = cast(PlatformCollection, _get_kwarg_by_name("platforms", **kwargs)) 99 | matching_platforms = [p for p in platforms.keys() if p.family() == family] 100 | if matching_platforms: 101 | config.set_deployment_hosts(family) 102 | logging.debug("Running test for %s platform", family) 103 | func(*args, **kwargs) 104 | else: 105 | logging.info("Skipping test for %s platform", family) 106 | 107 | return params_wrapper 108 | 109 | return func_wrapper 110 | 111 | 112 | def perform_operation_on_platforms(platforms: PlatformCollection, operation: CallableOperation, *args: object) -> None: 113 | for platform, hosts in platforms.items(): 114 | for address in hosts: 115 | operation(platform, address, *args) 116 | 117 | 118 | def set_ca_cert_download_params(config: AnsibleConfigurator, installer_server_url: str) -> None: 119 | config.set_common_parameter( 120 | config.CA_CERT_DOWNLOAD_URL_KEY, f"{installer_server_url}/{INSTALLER_CERTIFICATE_FILE_NAME}" 121 | ) 122 | config.set_common_parameter( 123 | config.CA_CERT_DOWNLOAD_CERT_KEY, f"{WORK_SERVER_DIR_PATH / SERVER_CERTIFICATE_FILE_NAME}" 124 | ) 125 | config.set_common_parameter(config.FORCE_CERT_DOWNLOAD_KEY, True) 126 | 127 | 128 | def set_installer_download_params(config: AnsibleConfigurator, installer_server_url: str) -> None: 129 | config.set_common_parameter(config.ENVIRONMENT_URL_KEY, installer_server_url) 130 | config.set_common_parameter(config.PAAS_TOKEN_KEY, INSTALLER_SERVER_TOKEN) 131 | config.set_common_parameter( 132 | config.INSTALLER_DOWNLOAD_CERT_KEY, f"{WORK_SERVER_DIR_PATH / SERVER_CERTIFICATE_FILE_NAME}" 133 | ) 134 | for platform in DeploymentPlatform: 135 | config.set_platform_parameter(platform, config.INSTALLER_ARCH_KEY, platform.arch()) 136 | 137 | set_ca_cert_download_params(config, installer_server_url) 138 | 139 | 140 | def run_deployment(runner: AnsibleRunner, configurator: AnsibleConfigurator, ignore_errors: bool = False, check_mode: bool = False) -> DeploymentResult: 141 | results = runner.run_deployment(configurator, check_mode=check_mode) 142 | 143 | if not ignore_errors: 144 | logging.info("Check exit codes") 145 | for out in results: 146 | assert out.returncode == 0 147 | return results 148 | 149 | 150 | def check_agent_state( 151 | platform: DeploymentPlatform, address: str, wrapper: PlatformCommandWrapper, installed: bool 152 | ) -> None: 153 | logging.debug("Platform: %s, IP: %s", platform, address) 154 | result = wrapper.file_exists(platform, address, get_oneagentctl_path(platform)) 155 | assert result.returncode == (0 if installed else 1) 156 | 157 | 158 | def check_download_directory( 159 | platform: DeploymentPlatform, 160 | address: str, 161 | wrapper: PlatformCommandWrapper, 162 | exists: bool, 163 | unix_path: Path, 164 | windows_path: Path, 165 | ) -> None: 166 | logging.debug("Platform: %s, IP: %s", platform, address) 167 | download_path: Path = select_by_platform(platform, unix_path, windows_path) 168 | installer_path = download_path / f"{INSTALLER_PARTIAL_NAME}*" 169 | assert wrapper.directory_exists(platform, address, download_path).returncode == 0 170 | assert wrapper.file_exists(platform, address, installer_path).returncode == (0 if exists else 1) 171 | -------------------------------------------------------------------------------- /roles/oneagent/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # PYTEST CONFIGURATION FILE 2 | import logging 3 | import os 4 | import shutil 5 | import time 6 | from collections.abc import Generator 7 | 8 | import pytest 9 | import requests 10 | from pytest import FixtureRequest, Metafunc, Parser 11 | from ansible.config import AnsibleConfigurator 12 | from ansible.runner import AnsibleRunner 13 | from command.platform_command_wrapper import PlatformCommandWrapper 14 | from constants import ( 15 | WORK_DIR_PATH, 16 | WORK_INSTALLERS_DIR_PATH, 17 | WORK_SERVER_DIR_PATH, 18 | ) 19 | from deployment.deployment_operations import ( 20 | check_agent_state, 21 | perform_operation_on_platforms, 22 | prepare_test_dirs, 23 | ) 24 | from deployment.deployment_platform import ( 25 | DeploymentPlatform, 26 | DeploymentResult, 27 | PlatformCollection, 28 | ) 29 | from deployment.installer_fetching import ( 30 | download_installers, 31 | download_signature, 32 | generate_installers, 33 | ) 34 | from installer_server.server import run_server 35 | 36 | # Command line options 37 | USER_KEY = "user" 38 | PASS_KEY = "password" 39 | TENANT_KEY = "tenant" 40 | TENANT_TOKEN_KEY = "tenant_token" 41 | 42 | # Ini file configuration 43 | CA_CERT_URL_KEY = "dynatrace_ca_cert_url" 44 | 45 | RUNNER_KEY = "runner" 46 | WRAPPER_KEY = "wrapper" 47 | CONSTANTS_KEY = "constants" 48 | PLATFORMS_KEY = "platforms" 49 | CONFIGURATOR_KEY = "configurator" 50 | 51 | 52 | def is_local_deployment(platforms: PlatformCollection) -> bool: 53 | return any("localhost" in hosts for hosts in platforms.values()) 54 | 55 | 56 | def parse_platforms_from_options(options: dict[str, list[str]]) -> PlatformCollection: 57 | platforms: PlatformCollection = {} 58 | deployment_platforms = [e.value for e in DeploymentPlatform] 59 | 60 | for key, hosts in options.items(): 61 | if key in deployment_platforms and hosts: 62 | if "localhost" in hosts: 63 | return {DeploymentPlatform.from_str(key): hosts} 64 | platforms[DeploymentPlatform.from_str(key)] = hosts 65 | return platforms 66 | 67 | 68 | def try_connect_to_server(url: str): 69 | try: 70 | response = requests.get(url, verify=False) 71 | logging.info("Installer server started successfully") 72 | return response 73 | except requests.ConnectionError: 74 | time.sleep(0.5) 75 | return None 76 | 77 | 78 | def wait_for_server_or_fail(url: str, max_attempts: int = 10) -> bool: 79 | for _attempt in range(max_attempts): 80 | if try_connect_to_server(url) is not None: 81 | return True 82 | return False 83 | 84 | 85 | @pytest.fixture(scope="session", autouse=True) 86 | def create_test_directories(request: FixtureRequest) -> None: 87 | logging.info("Creating working directories for tests") 88 | os.makedirs(WORK_INSTALLERS_DIR_PATH, exist_ok=True) 89 | os.makedirs(WORK_SERVER_DIR_PATH, exist_ok=True) 90 | os.makedirs(WORK_DIR_PATH, exist_ok=True) 91 | 92 | 93 | @pytest.fixture(scope="session", autouse=True) 94 | def prepare_installers(request: FixtureRequest) -> None: 95 | logging.info("Preparing installers...") 96 | tenant = request.config.getoption(TENANT_KEY) 97 | tenant_token = request.config.getoption(TENANT_TOKEN_KEY) 98 | cert_url = str(request.config.getini(CA_CERT_URL_KEY)) 99 | 100 | if is_local_deployment(request.config.platforms): 101 | logging.info("Generating installers...") 102 | if not generate_installers(): 103 | pytest.exit("Generating installers failed") 104 | elif tenant and tenant_token: 105 | logging.info("Downloading installers and signature...") 106 | if not download_signature(cert_url) or not download_installers(tenant, tenant_token, request.config.platforms): 107 | pytest.exit("Downloading installers and signature failed") 108 | else: 109 | pytest.exit("No tenant or tenant token provided, cannot download installers") 110 | 111 | 112 | @pytest.fixture(scope="session", autouse=True) 113 | def installer_server_url() -> Generator[str, None, None]: 114 | port = 8021 115 | ipaddress = "127.0.0.1" 116 | url = f"https://{ipaddress}:{port}" 117 | 118 | logging.info("Running installer server on %s", url) 119 | 120 | server = run_server(ipaddress, port) 121 | for _unused in server: 122 | break 123 | 124 | if not wait_for_server_or_fail(url): 125 | pytest.exit("Failed to start installer server") 126 | 127 | yield url 128 | 129 | logging.info("Stopping installer server") 130 | for _unused in server: 131 | break 132 | logging.info("Installer server has stopped") 133 | 134 | 135 | @pytest.fixture(autouse=True) 136 | def handle_test_environment( 137 | request, 138 | runner: AnsibleRunner, 139 | configurator: AnsibleConfigurator, 140 | platforms: PlatformCollection, 141 | wrapper: PlatformCommandWrapper, 142 | ) -> Generator[None, None, None]: 143 | logging.info("Preparing test environment") 144 | prepare_test_dirs(WORK_DIR_PATH / request.module.__name__ / request.node.originalname) 145 | 146 | yield 147 | 148 | logging.info("Cleaning up environment") 149 | configurator.set_common_parameter(configurator.PACKAGE_STATE_KEY, "absent") 150 | 151 | results: DeploymentResult = runner.run_deployment(configurator) 152 | for result in results: 153 | if result.returncode != 0: 154 | logging.error("Failed to clean up environment, exit code: %d", result.returncode) 155 | 156 | logging.info("Check if agent is uninstalled") 157 | perform_operation_on_platforms(platforms, check_agent_state, wrapper, False) 158 | 159 | shutil.rmtree("/var/lib/dynatrace", ignore_errors=True) 160 | 161 | 162 | def pytest_addoption(parser: Parser) -> None: 163 | parser.addini(CA_CERT_URL_KEY, type="string", help="Url to CA certificate for downloading installers") 164 | 165 | parser.addoption(f"--{USER_KEY}", type=str, help="Name of the user", required=False) 166 | parser.addoption(f"--{PASS_KEY}", type=str, help="Password of the user", required=False) 167 | parser.addoption( 168 | f"--{TENANT_KEY}", 169 | type=str, 170 | help="Tenant URL for downloading installer", 171 | required=False, 172 | ) 173 | parser.addoption( 174 | f"--{TENANT_TOKEN_KEY}", 175 | type=str, 176 | help="API key for downloading installer", 177 | required=False, 178 | ) 179 | 180 | for platform in DeploymentPlatform: 181 | parser.addoption( 182 | f"--{platform.value}", 183 | type=str, 184 | nargs="+", 185 | default=[], 186 | help="List of IPs for specified platform", 187 | ) 188 | 189 | 190 | def pytest_configure(config) -> None: 191 | config.platforms = parse_platforms_from_options(vars(config.option)) 192 | 193 | 194 | def pytest_generate_tests(metafunc: Metafunc) -> None: 195 | options = vars(metafunc.config.option) 196 | for key in [USER_KEY, PASS_KEY]: 197 | if key in metafunc.fixturenames: 198 | metafunc.parametrize(key, [options[key]]) 199 | 200 | user: str = options[USER_KEY] 201 | password: str = options[PASS_KEY] 202 | 203 | wrapper = PlatformCommandWrapper(user, password) 204 | configurator = AnsibleConfigurator(user, password, metafunc.config.platforms) 205 | runner = AnsibleRunner(WORK_DIR_PATH / metafunc.module.__name__ / metafunc.function.__name__, user, password) 206 | 207 | if CONFIGURATOR_KEY in metafunc.fixturenames: 208 | metafunc.parametrize(CONFIGURATOR_KEY, [configurator]) 209 | 210 | if RUNNER_KEY in metafunc.fixturenames: 211 | metafunc.parametrize(RUNNER_KEY, [runner]) 212 | 213 | if PLATFORMS_KEY in metafunc.fixturenames: 214 | metafunc.parametrize(PLATFORMS_KEY, [metafunc.config.platforms]) 215 | 216 | if WRAPPER_KEY in metafunc.fixturenames: 217 | metafunc.parametrize(WRAPPER_KEY, [wrapper]) 218 | 219 | 220 | @pytest.hookimpl(hookwrapper=True, tryfirst=True) 221 | def pytest_runtest_setup(item): 222 | config = item.config 223 | logging_plugin = config.pluginmanager.get_plugin("logging-plugin") 224 | logging_plugin.set_log_path(WORK_DIR_PATH / item._request.module.__name__ / item._request.node.originalname / "test.log") 225 | 226 | yield 227 | -------------------------------------------------------------------------------- /roles/oneagent/tests/test_resilience.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | import pytest 5 | from ansible.config import AnsibleConfigurator 6 | from ansible.runner import AnsibleRunner 7 | from constants import ( 8 | ERROR_MESSAGES_FILE_PATH, 9 | FAILED_DEPLOYMENT_EXIT_CODE, 10 | TEST_SIGNATURE_FILE, 11 | VARIABLE_PREFIX, 12 | ) 13 | from deployment.deployment_operations import ( 14 | enable_for_system_family, 15 | run_deployment, 16 | set_installer_download_params, 17 | ) 18 | from deployment.deployment_platform import DeploymentResult, PlatformCollection 19 | from resources.file_operations import read_yaml_file 20 | 21 | DOWNLOAD_DIR_CONTAINS_SPACES_KEY = "download_dir_contains_spaces" 22 | DOWNLOAD_FAILED_KEY = "failed_download" 23 | INSTALL_DIR_CONTAINS_SPACES_KEY = "install_dir_contains_spaces" 24 | LOCAL_INSTALLER_NOT_AVAILABLE_KEY = "missing_local_installer" 25 | MISSING_DOWNLOAD_DIRECTORY_KEY = "missing_download_dir" 26 | MISSING_REQUIRED_PARAMETERS_KEY = "missing_mandatory_params" 27 | MULTIPLE_INSTALL_PATH_KEY = "multiple_install_dir" 28 | SIGNATURE_VERIFICATION_FAILED_KEY = "signature_verification_failed" 29 | UNKNOWN_ARCHITECTURE_KEY = "unknown_arch" 30 | VERSION_LOWER_THAN_INSTALLED_KEY = "version_lower_than_installed" 31 | VERSION_PARAMETER_TOO_LOW_KEY = "version_lower_than_minimal" 32 | 33 | 34 | def _parse_error_messages_file() -> dict[str, str]: 35 | return read_yaml_file(ERROR_MESSAGES_FILE_PATH) 36 | 37 | 38 | def _prepare_test_data(data: dict[str, str]) -> dict[str, str]: 39 | parsed_data = {} 40 | for key, value in data.items(): 41 | key = key.strip().removeprefix(VARIABLE_PREFIX) 42 | value = value.strip().strip('"') 43 | value = value.replace("(", "\\(").replace(")", "\\)") 44 | parsed_data[key] = re.sub("%\\w", ".*", value) 45 | return parsed_data 46 | 47 | 48 | @pytest.fixture 49 | def _error_messages() -> dict[str, str]: 50 | return _prepare_test_data(_parse_error_messages_file()) 51 | 52 | 53 | def _check_deployment_failure(results: DeploymentResult, expected_message: str, expected_code: int) -> None: 54 | logging.info("Check if installation failed") 55 | for result in results: 56 | assert result.returncode == expected_code 57 | 58 | logging.info("Check if output contains error message") 59 | for result in results: 60 | combined_output = result.stdout + result.stderr 61 | search_result = re.search(expected_message, combined_output) 62 | if not search_result: 63 | logging.debug("stdout: %s", result.stdout) 64 | logging.debug("stderr: %s", result.stderr) 65 | assert search_result 66 | 67 | 68 | def test_invalid_required_parameters( 69 | _error_messages: dict[str, str], runner: AnsibleRunner, configurator: AnsibleConfigurator, installer_server_url: str 70 | ) -> None: 71 | logging.debug("Removing required parameter - direct download scenario") 72 | set_installer_download_params(configurator, installer_server_url) 73 | configurator.set_common_parameter(configurator.ENVIRONMENT_URL_KEY, "") 74 | 75 | _check_deployment_failure( 76 | run_deployment(runner, configurator, True), 77 | _error_messages[MISSING_REQUIRED_PARAMETERS_KEY], 78 | FAILED_DEPLOYMENT_EXIT_CODE, 79 | ) 80 | 81 | 82 | def test_invalid_architecture( 83 | _error_messages: dict[str, str], runner: AnsibleRunner, configurator: AnsibleConfigurator, installer_server_url: str 84 | ): 85 | set_installer_download_params(configurator, installer_server_url) 86 | configurator.set_common_parameter(configurator.INSTALLER_ARCH_KEY, "unknown_arch") 87 | 88 | _check_deployment_failure( 89 | run_deployment(runner, configurator, True), 90 | _error_messages[UNKNOWN_ARCHITECTURE_KEY], 91 | FAILED_DEPLOYMENT_EXIT_CODE, 92 | ) 93 | 94 | 95 | def test_missing_local_installer( 96 | _error_messages: dict[str, str], runner: AnsibleRunner, configurator: AnsibleConfigurator 97 | ): 98 | configurator.set_common_parameter(configurator.LOCAL_INSTALLER_KEY, "non_existing_installer") 99 | 100 | _check_deployment_failure( 101 | run_deployment(runner, configurator, True), 102 | _error_messages[LOCAL_INSTALLER_NOT_AVAILABLE_KEY], 103 | FAILED_DEPLOYMENT_EXIT_CODE, 104 | ) 105 | 106 | 107 | @enable_for_system_family(family="unix") 108 | def test_install_path_contain_spaces( 109 | _error_messages: dict[str, str], 110 | runner: AnsibleRunner, 111 | configurator: AnsibleConfigurator, 112 | platforms: PlatformCollection, 113 | installer_server_url: str, 114 | ): 115 | set_installer_download_params(configurator, installer_server_url) 116 | installer_args = ["INSTALL_PATH=/path with spaces"] 117 | configurator.set_common_parameter(configurator.INSTALLER_ARGS_KEY, installer_args) 118 | 119 | _check_deployment_failure( 120 | run_deployment(runner, configurator, True), 121 | _error_messages[INSTALL_DIR_CONTAINS_SPACES_KEY], 122 | FAILED_DEPLOYMENT_EXIT_CODE, 123 | ) 124 | 125 | 126 | @enable_for_system_family(family="unix") 127 | def test_download_directory_contain_spaces( 128 | _error_messages: dict[str, str], 129 | runner: AnsibleRunner, 130 | configurator: AnsibleConfigurator, 131 | platforms: PlatformCollection, 132 | installer_server_url: str, 133 | ): 134 | set_installer_download_params(configurator, installer_server_url) 135 | configurator.set_common_parameter(configurator.DOWNLOAD_DIR_KEY, "/path with spaces") 136 | 137 | _check_deployment_failure( 138 | run_deployment(runner, configurator, True), 139 | _error_messages[DOWNLOAD_DIR_CONTAINS_SPACES_KEY], 140 | FAILED_DEPLOYMENT_EXIT_CODE, 141 | ) 142 | 143 | 144 | def test_version_parameter_too_low( 145 | _error_messages: dict[str, str], runner: AnsibleRunner, configurator: AnsibleConfigurator, installer_server_url: str 146 | ): 147 | set_installer_download_params(configurator, installer_server_url) 148 | configurator.set_common_parameter(configurator.INSTALLER_VERSION_KEY, "0.0.0") 149 | 150 | _check_deployment_failure( 151 | run_deployment(runner, configurator, True), 152 | _error_messages[VERSION_PARAMETER_TOO_LOW_KEY], 153 | FAILED_DEPLOYMENT_EXIT_CODE, 154 | ) 155 | 156 | 157 | def test_multiple_install_path_arguments( 158 | _error_messages: dict[str, str], runner: AnsibleRunner, configurator: AnsibleConfigurator, installer_server_url: str 159 | ): 160 | set_installer_download_params(configurator, installer_server_url) 161 | configurator.set_common_parameter(configurator.INSTALLER_ARGS_KEY, ["INSTALL_PATH=/path1"]) 162 | configurator.set_common_parameter(configurator.INSTALLER_PLATFORM_ARGS_KEY, ["INSTALL_PATH=/path2"]) 163 | 164 | _check_deployment_failure( 165 | run_deployment(runner, configurator, True), 166 | _error_messages[MULTIPLE_INSTALL_PATH_KEY], 167 | FAILED_DEPLOYMENT_EXIT_CODE, 168 | ) 169 | 170 | 171 | def test_failed_download( 172 | _error_messages: dict[str, str], runner: AnsibleRunner, configurator: AnsibleConfigurator, installer_server_url: str 173 | ): 174 | set_installer_download_params(configurator, installer_server_url) 175 | configurator.set_common_parameter(configurator.ENVIRONMENT_URL_KEY, "0.0.0.0") 176 | 177 | _check_deployment_failure( 178 | run_deployment(runner, configurator, True), 179 | _error_messages[DOWNLOAD_FAILED_KEY], 180 | FAILED_DEPLOYMENT_EXIT_CODE, 181 | ) 182 | 183 | 184 | @enable_for_system_family(family="unix") 185 | def test_failed_signature_verification( 186 | _error_messages: dict[str, str], 187 | runner: AnsibleRunner, 188 | configurator: AnsibleConfigurator, 189 | platforms: PlatformCollection, 190 | installer_server_url: str, 191 | ): 192 | set_installer_download_params(configurator, installer_server_url) 193 | configurator.set_common_parameter(configurator.FORCE_CERT_DOWNLOAD_KEY, False) 194 | configurator.set_common_parameter(configurator.INSTALLER_VERSION_KEY, "latest") 195 | 196 | with TEST_SIGNATURE_FILE.open("w") as signature: 197 | _unused = signature.write("break signature by writing some text") 198 | 199 | _check_deployment_failure( 200 | run_deployment(runner, configurator, True), _error_messages[SIGNATURE_VERIFICATION_FAILED_KEY], FAILED_DEPLOYMENT_EXIT_CODE 201 | ) 202 | -------------------------------------------------------------------------------- /roles/oneagent/README.md: -------------------------------------------------------------------------------- 1 | # OneAgent role 2 | 3 | ## Requirements 4 | 5 | - OneAgent version 1.199+. 6 | - Script access to the OneAgent installer file. You can either: 7 | - configure the script to download the installer directly from your Dynatrace environment, 8 | - download it yourself and upload it to the primary node. 9 | 10 | ### Direct download from your environment 11 | 12 | The script utilizes [Deployment API] to download a platform-specific installer to the target machine. 13 | You will need to supply the role with information required to authenticate the API call in your environment: 14 | 15 | - The environment URL: 16 | - **SaaS**: `https://{your-environment-id}.live.dynatrace.com` 17 | - **Managed**: `https://{your-domain}/e/{your-environment-id}` 18 | - The [PaaS token] of your environment 19 | 20 | ### Local installer 21 | 22 | Use the Dynatrace UI to download OneAgent and upload it to the primary node. The script copies the installer to target machines during execution. 23 | Note that Windows, Linux, and AIX require their dedicated installers. Original installer names indicate the target platform. If you need to change the installer names, make sure the script can distinguish them. 24 | If you don't specify the installer, the script attempts to use the direct download. 25 | 26 | ### Configure existing installation 27 | 28 | The role is capable of configuring existing installation by utilizing `oneagentctl`. In case no `environment_url`, `paas_token` and `local_installer` parameters are provided, the script runs `oneagentctl` with provided parameters directly. 29 | 30 | For full list of suitable parameters, see [OneAgent configuration via command-line interface]. 31 | 32 | ## Variables 33 | 34 | The following variables are available in `defaults/main/` and can be overridden: 35 | 36 | | Name | Default | Description | 37 | |----------------------------------|---------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| 38 | | `oneagent_environment_url` | `-` | The URL of the target Dynatrace environment (see [Direct download from your environment](#direct-download-from-your-environment)). | 39 | | `oneagent_paas_token` | `-` | The [PaaS Token] retrieved from the "Deploy Dynatrace" installer page. | 40 | | `oneagent_local_installer` | `-` | The Path to OneAgent installer stored on the main node. | 41 | | `oneagent_installer_arch` | `-` | Specifies the OneAgent installer architecture. | 42 | | `oneagent_version` | `latest` | The required version of the OneAgent in the `1.199.247.20200714-111723` format. See [Deployment API - GET available versions of OneAgent] for more details. | 43 | | `oneagent_download_dir` | Linux: `$TEMP` or `/tmp`
Windows: `%TEMP%` or `C:\Windows\Temp` | Installer download directory. For Linux and AIX, the directory must not contain spaces. Will be created if it does not exist. | 44 | | `oneagent_install_args` | `-` | Dynatrace OneAgent installation parameters defined as a list of items. | 45 | | `oneagent_platform_install_args` | `-` | Additional list of platform-specific installation parameters, appended to `oneagent_install_args' when run on a respective platform. | 46 | | `oneagent_preserve_installer` | `false` | Preserve installers on secondary machines after deployment. | 47 | | `oneagent_package_state` | `present` | OneAgent package state; use `present` or `latest` to make sure it's installed, or `absent` in order to uninstall. | 48 | | `oneagent_reboot_host` | `false` | Reboot the secondary machine after OneAgent installation | 49 | | `oneagent_verify_signature` | `true` | Verifies installer's signature (available only on AIX/Linux platforms) | 50 | | `oneagent_reboot_timeout` | `3600` | Set the timeout for rebooting secondary machine in seconds | 51 | | `oneagent_no_log` | `true` | Sets Ansible `no_log` attribute value in tasks which may log sensitive values | 52 | 53 | For more information, see customize OneAgent installation documentation for [Linux], [Windows], and [AIX]. 54 | 55 | ## Examples 56 | 57 | You can find example playbooks in the `examples` directory within the role. The directory contains the following: -`local_installer` - basic configuration with local installers. -`advanced_config` - showing advanced configuration with a custom install path and download directory. -`oneagentctl_config` - showing bare configuration with oneagentctl. 58 | 59 | Additionally, each directory contains inventory file with basic hosts configuration for playbooks. 60 | 61 | **NOTE:** For multi-platform Windows, Linux or AIX deployment, you must specify the `become: true` option for proper machines group in the inventory file. 62 | On Windows, `become: true` option is not supported. 63 | Since Windows paths are different compared to a traditional Linux system, review [Path Formatting for Windows] to avoid issues during install. 64 | 65 | ## Logging 66 | 67 | The logs produced by ansible can be collected into a single file located on the managed node instead of printing them to STDOUT. 68 | Log output will still be printed to the STDOUT. 69 | There are several ways to achieve that using ansible's configuration setting: 70 | 71 | - Set `ANSIBLE_LOG_PATH` environment variable containing path to the log file before executing a playbook with the role. 72 | - Specify `log_path` variable in the `[default]` section of ansible's [configuration settings file]. 73 | 74 | The verbosity of the logs can be controlled with the command line option `-v`. 75 | Repeating the option multiple times gets maximal verbosity up to the connection debugging level: `-vvvv`. 76 | 77 | [PaaS token]: https://www.dynatrace.com/support/help/shortlink/token#paas-token- 78 | [Deployment API]: https://www.dynatrace.com/support/help/shortlink/api-deployment 79 | [Deployment API - GET available versions of OneAgent]: https://www.dynatrace.com/support/help/shortlink/api-deployment-get-versions 80 | [Path Formatting for Windows]: https://docs.ansible.com/ansible/latest/user_guide/windows_usage.html#path-formatting-for-windows 81 | [Windows]: https://www.dynatrace.com/support/help/shortlink/windows-custom-installation 82 | [Linux]: https://www.dynatrace.com/support/help/shortlink/linux-custom-installation 83 | [AIX]: https://www.dynatrace.com/support/help/shortlink/aix-custom-installation 84 | [configuration settings file]: https://docs.ansible.com/ansible/latest/reference_appendices/general_precedence.html#configuration-settings 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------