diff --git a/.config/ansible-lint.yml b/.config/ansible-lint.yml index b3bc42cb..7b8dbb95 100644 --- a/.config/ansible-lint.yml +++ b/.config/ansible-lint.yml @@ -4,4 +4,5 @@ warn_list: - galaxy[version-incorrect] # until collection gets bumped to 1.x.x - name[casing] # https://github.com/ansible/ansible-lint/issues/4035#issuecomment-2116272270 skip_list: + - role-name # Allow underscore prefix in role name for internal role - var-naming[no-role-prefix] # https://github.com/ansible/ansible-lint/pull/3422#issuecomment-1549584988 diff --git a/roles/_common/README.md b/roles/_common/README.md new file mode 100644 index 00000000..1e29b5fc --- /dev/null +++ b/roles/_common/README.md @@ -0,0 +1,3 @@ +--- +# Internal use only +This role is for common tasks shared between roles and should not be used directly diff --git a/roles/_common/handlers/main.yml b/roles/_common/handlers/main.yml new file mode 100644 index 00000000..76ba6b37 --- /dev/null +++ b/roles/_common/handlers/main.yml @@ -0,0 +1,15 @@ +--- +- name: "Restart {{ _common_service_name }}" + # listen: "restart_service" + become: true + ansible.builtin.service: + daemon_reload: true + name: "{{ _common_service_name }}" + state: restarted + +- name: "Reload {{ _common_service_name }}" + # listen: "reload_service" + become: true + ansible.builtin.service: + name: "{{ _common_service_name }}" + state: reloaded diff --git a/roles/_common/meta/argument_specs.yml b/roles/_common/meta/argument_specs.yml new file mode 100644 index 00000000..7642a47c --- /dev/null +++ b/roles/_common/meta/argument_specs.yml @@ -0,0 +1,91 @@ +--- +argument_specs: + configure: + short_description: "Internal only - common configuration tasks" + description: "Internal only - selinux requirements" + author: + - "Prometheus Community" + options: + _common_service_name: + description: + - "Name of the system service (systemd)" + - "Usually matches the role name" + default: "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + _common_config_dir: + description: "Path to directory to install configuration." + default: "" + _common_system_user: + description: "System user for running the service." + default: "" + _common_system_group: + description: "User group for the system user." + default: "" + _common_tls_server_config: + description: "Configuration for TLS authentication." + default: "" + _common_http_server_config: + description: "Configuration for HTTP/2 support." + default: "" + _common_common_basic_auth_users: + description: "Dictionary of users and password for basic authentication. Passwords are automatically hashed with bcrypt." + default: "" + install: + short_description: "Internal only - common installation tasks" + description: "Internal only - selinux requirements" + author: + - "Prometheus Community" + options: + _common_binaries: + description: "List of binaries to install" + default: [] + type: "list" + elements: "str" + _common_binary_install_dir: + description: "Directory to install binaries" + default: "" + _common_binary_name: + description: "Name of main binary" + default: "{{ __common_binary_basename }}" + _common_binary_unarchive_opts: + description: "Extra options to pass to binary unarchive task" + default: [] + type: "list" + elements: "str" + _common_binary_url: + description: "URL of the binaries to install" + default: "" + _common_checksums_url: + description: "URL of the checksums file for the binaries" + default: "" + _common_config_dir: + description: "Path to the configuration dir" + default: "" + _common_local_cache_path: + description: "Local path to stash the archive and its extraction" + default: "" + _common_system_user: + description: "System user for running the service." + default: "" + _common_system_group: + description: "User group for the system user." + default: "" + preflight: + short_description: "Internal only - common preflight tasks" + description: "Internal only - selinux requirements" + author: + - "Prometheus Community" + options: + _common_dependencies: + description: "Package dependencies to install" + default: "{% if (ansible_pkg_mgr == 'apt') %}\ + {{ ('python-apt' if ansible_python_version is version('3', '<') else 'python3-apt') }} + {% else %}\ + {% endif %}" + selinux: + short_description: "Internal only - common selinux configuration tasks" + description: "Internal only - selinux requirements" + author: + - "Prometheus Community" + options: + _common_selinux_port: + description: "Port to allow in SELinux" diff --git a/roles/_common/meta/main.yml b/roles/_common/meta/main.yml new file mode 100644 index 00000000..4c160942 --- /dev/null +++ b/roles/_common/meta/main.yml @@ -0,0 +1,6 @@ +--- +galaxy_info: + author: "Prometheus Community" + description: "Internal role for common tasks shared between roles" + license: "Apache" + min_ansible_version: "2.9" diff --git a/roles/_common/tasks/configure.yml b/roles/_common/tasks/configure.yml new file mode 100644 index 00000000..fe0333e2 --- /dev/null +++ b/roles/_common/tasks/configure.yml @@ -0,0 +1,70 @@ +--- +- name: "Validate invocation of _common role" + ansible.builtin.assert: + that: + - "ansible_parent_role_names is defined" + - "ansible_parent_role_names | default() | length > 0" + fail_msg: "Error: The '_common' role is a internal role and cannot be invoked directly." + tags: + - always + +- name: "Create systemd service unit {{ _common_service_name }}" + ansible.builtin.template: + src: "{{ _common_service_name }}.service.j2" + dest: "/etc/systemd/system/{{ _common_service_name }}.service" + owner: root + group: root + mode: 0644 + become: true + notify: + - "{{ ansible_parent_role_names | first }} : Restart {{ _common_service_name }}" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + +- name: "Create config dir {{ _common_config_dir }}" + ansible.builtin.file: + path: "{{ _common_config_dir }}" + state: directory + owner: "{{ _common_system_user }}" + group: "{{ _common_system_group }}" + mode: u+rwX,g+rwX,o=rX + become: true + notify: + - "{{ ansible_parent_role_names | first }} : Restart {{ _common_service_name }}" + when: (_common_config_dir) + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + +- name: "Install web config for {{ _common_service_name }}" + ansible.builtin.template: + src: "web_config.yml.j2" + dest: "{{ _common_config_dir }}/web_config.yml" + owner: "{{ _common_system_user }}" + group: "{{ _common_system_group }}" + mode: 0644 + become: true + notify: + - "{{ ansible_parent_role_names | first }} : Restart {{ _common_service_name }}" + when: "[_common_tls_server_config, _common_http_server_config, _common_basic_auth_users] | map('length') | select('>', 0) | list is any" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + +# +# - name: "Configure {{ _common_service_name }}" +# ansible.builtin.template: +# # src: "{{ ansible_parent_role_paths | first }}/templates/{{ _common_service_name }}.yml.j2" +# src: "{{ _config_template | default(ansible_parent_role_paths | first ~ '/templates/' ~ _common_service_name ~ '.yml.j2') }}" +# # dest: "/etc/{{ _common_service_name }}.yml" +# dest: "{{ _config_dest | default('/etc/' ~ _common_service_name ~ '.yml') }}" +# owner: "{{ _system_user }}" +# group: "{{ _system_group }}" +# mode: 0644 +# notify: +# - reload_service +# when: (ansible_parent_role_paths | first '/templates/' _common_service_name '.yml.j2') diff --git a/roles/_common/tasks/install.yml b/roles/_common/tasks/install.yml new file mode 100644 index 00000000..99cc293a --- /dev/null +++ b/roles/_common/tasks/install.yml @@ -0,0 +1,108 @@ +--- +- name: "Validate invocation of _common role" + ansible.builtin.assert: + that: + - "ansible_parent_role_names is defined" + - "ansible_parent_role_names | default() | length > 0" + fail_msg: "Error: The '_common' role is a internal role and cannot be invoked directly." + tags: + - always + +- name: "Create system group {{ _common_system_group }}" + ansible.builtin.group: + name: "{{ _common_system_group }}" + system: true + state: present + become: true + when: _common_system_group != "root" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - install + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_install" + +- name: "Create system user {{ _common_system_user }}" + ansible.builtin.user: + name: "{{ _common_system_user }}" + system: true + shell: "/usr/sbin/nologin" + group: "{{ _common_system_group }}" + home: "{{ _common_config_dir | default('/') }}" + create_home: false + become: true + when: _common_system_user != "root" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - install + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_install" + +- name: "Create localhost binary cache path" + ansible.builtin.file: + path: "{{ _common_local_cache_path }}" + state: directory + mode: 0755 + delegate_to: localhost + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - install + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_install" + - download + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_download" + +- name: "Download binary {{ __common_binary_basename }}" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - install + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_install" + - download + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_download" + block: + - name: "Get checksum list for {{ __common_binary_basename }}" + ansible.builtin.set_fact: + __common_binary_checksums: "{{ dict(lookup('url', _common_checksums_url, headers=__common_github_api_headers, wantlist=True) + | map('regex_replace', '^([a-fA-F0-9]+)\\s+', 'sha256:\\1 ') + | map('regex_findall', '^(sha256:[a-fA-F0-9]+)\\s+(.+)$') | map('flatten') | map('reverse')) }}" + run_once: true + when: (_common_checksums_url) + + - name: "Download {{ __common_binary_basename }}" + ansible.builtin.get_url: + url: "{{ _common_binary_url }}" + dest: "{{ _common_local_cache_path }}/{{ _common_binary_name | default(__common_binary_basename) }}" + headers: "{{ __common_github_api_headers }}" + checksum: "{{ __common_binary_checksums[__common_binary_basename] | default(omit) }}" + mode: 0644 + register: __common_download + until: __common_download is succeeded + retries: 5 + delay: 2 + # run_once: true # <-- this can't be set due to multi-arch support + delegate_to: localhost + check_mode: false + + - name: "Unpack binary archive {{ __common_binary_basename }}" + ansible.builtin.unarchive: + src: "{{ _common_local_cache_path }}/{{ __common_binary_basename }}" + dest: "{{ _common_local_cache_path }}" + mode: 0755 + list_files: true + extra_opts: "{{ _common_binary_unarchive_opts | default(omit, true) }}" + register: __common_unpack + delegate_to: localhost + check_mode: false + when: __common_binary_basename is search('\.zip$|\.tar\.gz$') + +- name: "Propagate binaries" + ansible.builtin.copy: + src: "{{ _common_local_cache_path }}/{{ item }}" + dest: "{{ _common_binary_install_dir }}/{{ item }}" + mode: 0755 + owner: root + group: root + loop: "{{ _common_binaries }}" + become: true + notify: + - "{{ ansible_parent_role_names | first }} : Restart {{ _common_service_name }}" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - install + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_install" diff --git a/roles/_common/tasks/preflight.yml b/roles/_common/tasks/preflight.yml new file mode 100644 index 00000000..4a8006e5 --- /dev/null +++ b/roles/_common/tasks/preflight.yml @@ -0,0 +1,76 @@ +--- +- name: "Validate invocation of _common role" + ansible.builtin.assert: + that: + - "ansible_parent_role_names is defined" + - "ansible_parent_role_names | default() | length > 0" + fail_msg: "Error: The '_common' role is a internal role and cannot be invoked directly." + tags: + - always + +- name: "Check for deprecated skip_install variable" + ansible.builtin.assert: + that: + - __common_parent_role_short_name ~ '_skip_install' not in vars + fail_msg: "The variable {{ __common_parent_role_short_name ~ '_skip_install' }} is deprecated. + Please use `--skip-tags {{ __common_parent_role_short_name }}_install` instead to skip the installation." + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + +- name: "Check for deprecated binary_local_dir variable" + ansible.builtin.assert: + that: + - __common_parent_role_short_name ~ '_binary_local_dir' not in vars + fail_msg: "The variable {{ __common_parent_role_short_name ~ '_binary_local_dir' }} is deprecated. + Please use the variable {{ __common_parent_role_short_name ~ '_local_cache_path' }} instead" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + +- name: "Check for deprecated archive_path variable" + ansible.builtin.assert: + that: + - __common_parent_role_short_name ~ '_archive_path' not in vars + fail_msg: "The variable {{ __common_parent_role_short_name ~ '_archive_path' }} is deprecated. + Please use the variable {{ __common_parent_role_short_name ~ '_local_cache_path' }} instead" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + +- name: Assert usage of systemd as an init system + ansible.builtin.assert: + that: ansible_service_mgr == 'systemd' + msg: "This module only works with systemd" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + - install + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_install" + +- name: Install dependencies + become: true + ansible.builtin.package: + name: "{{ _common_dependencies }}" + state: present + when: (_common_dependencies) + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + - install + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_install" + +- name: Gather package facts + ansible.builtin.package_facts: + when: "not 'packages' in ansible_facts" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + - install + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_install" diff --git a/roles/_common/tasks/selinux.yml b/roles/_common/tasks/selinux.yml new file mode 100644 index 00000000..5b7319fc --- /dev/null +++ b/roles/_common/tasks/selinux.yml @@ -0,0 +1,58 @@ +--- +- name: "Validate invocation of _common role" + ansible.builtin.assert: + that: + - "ansible_parent_role_names is defined" + - "ansible_parent_role_names | default() | length > 0" + fail_msg: "Error: The '_common' role is a internal role and cannot be invoked directly." + tags: + - always + +- name: Install selinux python packages [RedHat] + ansible.builtin.package: + name: "{{ ['libselinux-python', 'policycoreutils-python'] + if ansible_python_version is version('3', '<') else + ['python3-libselinux', 'python3-policycoreutils'] }}" + state: present + register: __common_install_selinux_packages + until: __common_install_selinux_packages is success + retries: 5 + delay: 2 + become: true + when: ansible_os_family | lower == "redhat" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + +- name: Install selinux python packages [clearlinux] + ansible.builtin.package: + name: sysadmin-basic + state: present + register: __common_install_selinux_packages + until: __common_install_selinux_packages is success + retries: 5 + delay: 2 + become: true + when: + - ansible_distribution | lower == "clearlinux" + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" + +- name: Allow port in SELinux + community.general.seport: + ports: "{{ _common_selinux_port }}" + proto: tcp + setype: http_port_t + state: present + become: true + when: + - ansible_version.full is version_compare('2.4', '>=') + - ansible_selinux.status == "enabled" + - (_common_selinux_port) + tags: + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}" + - configure + - "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}_configure" diff --git a/roles/_common/templates/web_config.yml.j2 b/roles/_common/templates/web_config.yml.j2 new file mode 100644 index 00000000..af495e50 --- /dev/null +++ b/roles/_common/templates/web_config.yml.j2 @@ -0,0 +1,18 @@ +--- +{{ ansible_managed | comment }} +{% if _common_tls_server_config | length > 0 %} +tls_server_config: +{{ _common_tls_server_config | to_nice_yaml | indent(2, true) }} +{% endif %} + +{% if _common_http_server_config | length > 0 %} +http_server_config: +{{ _common_http_server_config | to_nice_yaml | indent(2, true) }} +{% endif %} + +{% if _common_basic_auth_users | length > 0 %} +basic_auth_users: +{% for k, v in _common_basic_auth_users.items() %} + {{ k }}: {{ v | string | password_hash('bcrypt', ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' | shuffle(seed=inventory_hostname) | join)[:22], rounds=9) }} +{% endfor %} +{% endif %} diff --git a/roles/_common/vars/main.yml b/roles/_common/vars/main.yml new file mode 100644 index 00000000..dbda12bc --- /dev/null +++ b/roles/_common/vars/main.yml @@ -0,0 +1,24 @@ +--- +_common_local_cache_path: "" +_common_binaries: [] +_common_binary_name: "{{ __common_binary_basename }}" +_common_binary_install_dir: +_common_config_dir: "" +_common_binary_url: "" +_common_checksums_url: "" +_common_selinux_port: "" +_common_service_name: "{{ __common_parent_role_short_name }}" +_common_system_user: "" +_common_system_group: "" +_common_dependencies: "{% if (ansible_pkg_mgr == 'apt') %}\ + {{ ('python-apt' if ansible_python_version is version('3', '<') else 'python3-apt') }} + {% else %}\ + {% endif %}" +_common_binary_unarchive_opts: "" +_common_tls_server_config: {} +_common_http_server_config: {} +_common_basic_auth_users: {} +# Variables that should not be overwritten +__common_binary_basename: "{{ _common_binary_url | urlsplit('path') | basename }}" +__common_github_api_headers: "{{ {'GITHUB_TOKEN': lookup('ansible.builtin.env', 'GITHUB_TOKEN')} if (lookup('ansible.builtin.env', 'GITHUB_TOKEN')) else {} }}" +__common_parent_role_short_name: "{{ ansible_parent_role_names | first | regex_replace(ansible_collection_name ~ '.', '') }}"