├── 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.
--------------------------------------------------------------------------------