diff --git a/.ansible-lint b/.ansible-lint index 43cae14..f7c8aa6 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -7,3 +7,6 @@ loop_var_prefix: "^(__|{role}_)" exclude_paths: - .github/ + - .ansible/ + # ansible-lint thinks it's a playbook so gives an error, but its tasks are for include_task. decided to exclude it + - roles/rustdesk/molecule/default/client-binary-existance.yml diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index b90a6ea..0aace81 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -26,6 +26,10 @@ jobs: # run: ansible-galaxy install -r requirements.yml # shell: micromamba-shell {0} + - name: "Install community.general collection" + run: ansible-galaxy collection install community.general + shell: micromamba-shell {0} + - name: Check workflow files themselves with ActionLint run: actionlint shell: micromamba-shell {0} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a7d3a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.ansible +.vscode \ No newline at end of file diff --git a/README.md b/README.md index dc6784a..7217749 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,23 @@ ## Roles +- [alertmanager](roles/alertmanager/README.md) +- [borgmatic](roles/borgmatic/README.md) +- [curl_scheduled](roles/curl_scheduled/README.md) +- [dnsmasq](roles/dnsmasq/README.md) +- [grafana](roles/grafana/README.md) +- [ipmi_exporter](roles/ipmi_exporter/README.md) +- [karma](roles/karma/README.md) +- [loki](roles/loki/README.md) - [mount_device](roles/mount_device/README.md) +- [nginx](roles/nginx/README.md) +- [node_exporter](roles/node_exporter/README.md) +- [prometheus](roles/prometheus/README.md) +- [promtail](roles/promtail/README.md) +- [rclone_yandex](roles/rclone_yandex/README.md) +- [rustdesk](roles/rustdesk/README.md) +- [sftp_share](roles/sftp_share/README.md) +- [smartctl_exporter](roles/smartctl_exporter/README.md) +- [ufw](roles/ufw/README.md) +- [wg_hub](roles/wg_hub/README.md) +- [wg_spoke](roles/wg_spoke/README.md) \ No newline at end of file diff --git a/galaxy.yml b/galaxy.yml index 66c7b7e..e8ba3ce 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,10 +1,11 @@ --- namespace: genlab name: common -version: 0.0.1 +version: 0.20.0 readme: README.md authors: - Alexander Gorelyshev (corvus-migratorius@proton.me) + - Sergey Malyuk (malyuk.ss@genlab.llc) description: >- The genlab.common collection includes roles for performing deploying and configuring common infrastructural components license_file: LICENSE diff --git a/requirements.yml b/requirements.yml index a0cd255..093988a 100644 --- a/requirements.yml +++ b/requirements.yml @@ -1,3 +1,8 @@ --- collections: - name: ansible.posix + - name: community.general + - name: community.grafana + - name: community.docker + - name: community.crypto + - name: maxhoesel.borgbackup diff --git a/roles/alertmanager/README.md b/roles/alertmanager/README.md new file mode 100644 index 0000000..7ef20e9 --- /dev/null +++ b/roles/alertmanager/README.md @@ -0,0 +1,53 @@ +genlab.alertmanager +========= + +The [Alertmanager](https://github.com/prometheus/alertmanager) handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts. + +You'll need to manually specify the paths to your template files in the main Alertmanager configuration file (`alertmanager.yml`) on the target machine. The configuration file must have a `.yml` suffix, and template files must use a `.tmpl` suffix. + +Requirements +------------ + +None + +Role Variables +-------------- + +``` +alertmanager_version: 0.28.1 # version of Alertmanager (AM) +alertmanager_config_dir: "/etc/alertmanager/conf" # where to place AM configs +alertmanager_storage_dir: "/var/lib/alertmanager" # path to AM storage +alertmanager_dir: "/etc/alertmanager" # where to install AM on target +alertmanager_user: "alertmanager" # system user name +alertmanager_group: "alertmanager" # system group name +config_source_dir: "./alertmanager" # path to config files on source + +# Optional +template_source_dir: "./templates" # path to template files on source +``` + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.template + config_source_dir: alertmanager + alertmanager_version: 0.28.1 + +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/alertmanager/defaults/main.yml b/roles/alertmanager/defaults/main.yml new file mode 100644 index 0000000..4e14295 --- /dev/null +++ b/roles/alertmanager/defaults/main.yml @@ -0,0 +1,7 @@ +--- +alertmanager_version: 0.28.1 +alertmanager_config_dir: "/etc/alertmanager/conf" +alertmanager_storage_dir: "/var/lib/alertmanager" +alertmanager_dir: "/etc/alertmanager" +alertmanager_user: alertmanager +alertmanager_group: alertmanager diff --git a/roles/alertmanager/handlers/main.yml b/roles/alertmanager/handlers/main.yml new file mode 100644 index 0000000..4a0e813 --- /dev/null +++ b/roles/alertmanager/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "(Re)start and enable Alertmanager" + ansible.builtin.systemd_service: + name: alertmanager.service + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/alertmanager/meta/main.yml b/roles/alertmanager/meta/main.yml new file mode 100644 index 0000000..0cebb5a --- /dev/null +++ b/roles/alertmanager/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: alertmanager + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/alertmanager/molecule/default/alertmanager/alertmanager.yml b/roles/alertmanager/molecule/default/alertmanager/alertmanager.yml new file mode 100644 index 0000000..78e14ea --- /dev/null +++ b/roles/alertmanager/molecule/default/alertmanager/alertmanager.yml @@ -0,0 +1,36 @@ +global: + # SMTP configuration for email notifications (if needed) + smtp_smarthost: 'mailserver.example.com:587' + smtp_from: 'alertmanager@example.com' + smtp_auth_username: 'alertmanager' + smtp_auth_password: 'your_password' + + # Other global settings like resolve_timeout, http_config, etc. + +route: + # Default receiver for alerts + receiver: 'default-receiver' + + # Labels used for grouping alerts + group_by: ['alertname', 'instance', 'severity'] + + # Timing settings (group_wait, group_interval, repeat_interval) + # You can have nested 'routes' for more complex routing logic + +receivers: + - name: 'default-receiver' + email_configs: + - to: 'ops-team@example.com' + +inhibit_rules: + # Rules to suppress alerts based on other alerts + # Example: + - source_match: + severity: 'critical' + target_match: + severity: 'warning' + # Suppress 'warning' alerts if a 'critical' alert is also firing + +templates: + # Paths to template files for customizing notifications + - '/etc/alertmanager/templates/*.tmpl' diff --git a/roles/alertmanager/molecule/default/converge.yml b/roles/alertmanager/molecule/default/converge.yml new file mode 100644 index 0000000..470c151 --- /dev/null +++ b/roles/alertmanager/molecule/default/converge.yml @@ -0,0 +1,6 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.alertmanager + config_source_dir: alertmanager diff --git a/roles/alertmanager/molecule/default/molecule.yml b/roles/alertmanager/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/alertmanager/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/alertmanager/molecule/default/verify.yml b/roles/alertmanager/molecule/default/verify.yml new file mode 100644 index 0000000..37eb46f --- /dev/null +++ b/roles/alertmanager/molecule/default/verify.yml @@ -0,0 +1,55 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + tasks: + + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + - name: "Check if Alertmanager is installed" + changed_when: false + ansible.builtin.command: "alertmanager --version" + register: alertmanager_installed_version + + - name: "Check Alertmanager version" + ansible.builtin.assert: + that: "alertmanager_installed_version.stdout is regex('{{ alertmanager_version }}')" + success_msg: "Alertmanager version {{ alertmanager_version }} is installed and working" + fail_msg: "Alertmanager version {{ alertmanager_version }} is not installed or not working correctly" + + # kics-scan ignore-block + - name: "Check if Alertmanager is reachable" + ansible.builtin.uri: + url: "http://localhost:9093/-/healthy" + return_content: true + status_code: 200 + method: GET + body_format: json + register: alertmanager_health + + - name: "Assert Alertmanager health status" + ansible.builtin.assert: + that: "alertmanager_health.content == 'OK'" + success_msg: "Alertmanager is healthy" + fail_msg: "Alertmanager is not healthy" + + # kics-scan ignore-block + - name: "Check if Alertmanager is ready" + ansible.builtin.uri: + url: "http://localhost:9093/-/ready" + return_content: true + status_code: 200 + method: GET + body_format: json + register: alertmanager_ready + + - name: "Assert Alertmanager readiness status" + ansible.builtin.assert: + that: "alertmanager_ready.content == 'OK'" + success_msg: "Alertmanager is ready" + fail_msg: "Alertmanager is not ready" diff --git a/roles/alertmanager/tasks/configuration.yml b/roles/alertmanager/tasks/configuration.yml new file mode 100644 index 0000000..f06c996 --- /dev/null +++ b/roles/alertmanager/tasks/configuration.yml @@ -0,0 +1,24 @@ +--- + +- name: "Copy config file" + notify: "(Re)start and enable Alertmanager" + ansible.builtin.template: + src: "{{ item }}" + dest: "{{ alertmanager_config_dir }}/{{ item | basename }}" + owner: "{{ alertmanager_user }}" + group: "{{ alertmanager_group }}" + mode: "0660" + with_fileglob: + - "{{ config_source_dir }}/*.yml" + +- name: "Copy templates if existed" + notify: "(Re)start and enable Alertmanager" + when: template_source_dir is defined + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ alertmanager_config_dir }}/{{ item | basename }}" + owner: "{{ alertmanager_user }}" + group: "{{ alertmanager_group }}" + mode: "0660" + with_fileglob: + - "{{ template_source_dir }}/*.tmpl" diff --git a/roles/alertmanager/tasks/install.yml b/roles/alertmanager/tasks/install.yml new file mode 100644 index 0000000..db0b8c6 --- /dev/null +++ b/roles/alertmanager/tasks/install.yml @@ -0,0 +1,85 @@ +--- +- name: "Create Alertmanager system group" + ansible.builtin.group: + name: "{{ alertmanager_user }}" + system: true + state: present + +- name: "Create Alertmanager system user" + ansible.builtin.user: + name: "{{ alertmanager_user }}" + group: "{{ alertmanager_group }}" + system: true + shell: "/sbin/nologin" + create_home: false + state: present + +- name: "Install Alertmanager from binary" + block: + - name: "Check Alertmanager version" + changed_when: false + ansible.builtin.command: + cmd: "alertmanager --version" + register: alertmanager_ver + + - name: "Assert version correctness" + ansible.builtin.assert: + that: "alertmanager_ver.stdout is regex('{{ alertmanager_version }}')" + success_msg: "alertmanager version {{ alertmanager_version }} is installed and working" + fail_msg: "alertmanager version {{ alertmanager_version }} is not installed or not working correctly" + + rescue: + - name: "Create Alertmanager directories '{{ item }}'" + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ alertmanager_user }}" + group: "{{ alertmanager_group }}" + mode: "0755" + with_items: + - "{{ alertmanager_config_dir }}" + - "{{ alertmanager_dir }}" + - "{{ alertmanager_storage_dir }}" + + - name: "Download Alertmanager binary" + ansible.builtin.get_url: + url: "https://github.com/prometheus/alertmanager/releases/download/v{{ alertmanager_version }}/\ + alertmanager-{{ alertmanager_version }}.linux-amd64.tar.gz" + dest: "/tmp/alertmanager-{{ alertmanager_version }}.linux-amd64.tar.gz" + owner: "{{ alertmanager_user }}" + group: "{{ alertmanager_group }}" + mode: "0644" + + - name: "Unpack Alertmanager binaries" + notify: "(Re)start and enable Alertmanager" + ansible.builtin.unarchive: + src: "/tmp/alertmanager-{{ alertmanager_version }}.linux-amd64.tar.gz" + dest: "{{ alertmanager_dir }}" + creates: "{{ alertmanager_dir }}/alertmanager-{{ alertmanager_version }}.linux-amd64" + remote_src: true + + - name: "Cleanup downloaded file" + ansible.builtin.file: + path: "/tmp/alertmanager-{{ alertmanager_version }}.linux-amd64.tar.gz" + state: absent + + - name: "Move official alertmanager and amtool binaries" + ansible.builtin.copy: + src: "{{ alertmanager_dir }}/alertmanager-{{ alertmanager_version }}.linux-amd64/{{ item }}" + dest: "/usr/local/bin/{{ item }}" + mode: "0755" + owner: "{{ alertmanager_user }}" + group: "{{ alertmanager_group }}" + remote_src: true + with_items: + - alertmanager + - amtool + + +- name: "Create systemd service unit" + ansible.builtin.template: + src: alertmanager.service.j2 + dest: /etc/systemd/system/alertmanager.service + owner: "{{ alertmanager_user }}" + group: "{{ alertmanager_group }}" + mode: "0660" diff --git a/roles/alertmanager/tasks/main.yml b/roles/alertmanager/tasks/main.yml new file mode 100644 index 0000000..6bd62b2 --- /dev/null +++ b/roles/alertmanager/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: "Run installation tasks" + ansible.builtin.include_tasks: install.yml + +- name: "Run configuration tasks" + ansible.builtin.include_tasks: configuration.yml + +- name: "Flush handlers" + ansible.builtin.meta: "flush_handlers" diff --git a/roles/alertmanager/templates/alertmanager.service.j2 b/roles/alertmanager/templates/alertmanager.service.j2 new file mode 100644 index 0000000..a85ae68 --- /dev/null +++ b/roles/alertmanager/templates/alertmanager.service.j2 @@ -0,0 +1,27 @@ +[Unit] +Description=Alertmanager Service +After=network.target +Documentation="https://prometheus.io/docs/alerting/latest/alertmanager/" + +[Service] +User={{ alertmanager_user }} +Group={{ alertmanager_group }} +Type=simple +ExecStart=/usr/local/bin/alertmanager \ + --config.file={{ alertmanager_config_dir }}/alertmanager.yml \ + --storage.path={{ alertmanager_storage_dir }} +Restart=on-failure + +# Security hardening +ReadWritePaths={{ alertmanager_storage_dir }} +ProtectSystem=strict +NoNewPrivileges=true +PrivateTmp=true +ProtectKernelModules=true +ProtectControlGroups=true +ProtectKernelTunables=true +ProtectClock=yes +RestrictSUIDSGID=true + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/roles/alertmanager/vars/main.yml b/roles/alertmanager/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/alertmanager/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/borgmatic/README.md b/roles/borgmatic/README.md new file mode 100644 index 0000000..ce5f940 --- /dev/null +++ b/roles/borgmatic/README.md @@ -0,0 +1,40 @@ +ansible-borgmatic +========= + +This is a wrapper around the `borgmatic` role from the `maxhoesel.borgbackup` collection. + +The wrapper solve the most outstading issue with the current implementation of the original role: inability to install latest (or arbitrary) versions of `borgmatic` and `borg`. + +In the case of Borg we are fetching a release from Github. + +In the case of Borgmatic we are installing it via `pipx`, as recommended by their official documentation found here: https://torsion.org/borgmatic/docs/how-to/set-up-backups/. + +Requirements +------------ + +- `maxhoesel.borgbackup` collection installed (see `requirements.yml`); + +Role Variables +-------------- + +None + +Dependencies +------------ + +None + +Example Playbook +---------------- + +See `molecule/default/converge.yml` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/borgmatic/defaults/main.yml b/roles/borgmatic/defaults/main.yml new file mode 100644 index 0000000..c96f1e8 --- /dev/null +++ b/roles/borgmatic/defaults/main.yml @@ -0,0 +1,18 @@ +--- +borgmatic_version: "1.4.0" +borgmatic_glibc_version: "2.36" +borgmatic_pipx_version: "1.7.1" +borgmatic_binary_url: "\ + https://github.com/borgbackup/borg/releases/download/{{ borgmatic_version }}/borg-linux-glibc{{ borgmatic_glibc_version | replace('.', '') }}.tgz" +borgmatic_pipx_bin_dir: "/opt/borgmatic/bin" +borgmatic_schedule_oncalendar: "daily" +borgmatic_push_pubkey: true +borgmatic_sshkey_path: "/root/borgmatic/id_ed25519" +borgmatic_compression: "lz4" +borgmatic_keep_hourly: 0 +borgmatic_keep_daily: 3 +borgmatic_keep_weekly: 3 +borgmatic_keep_monthly: 1 +borgmatic_keep_yearly: 0 +borgmatic_uptime_kuma: +borgmatic_loki: diff --git a/roles/borgmatic/handlers/main.yml b/roles/borgmatic/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/borgmatic/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/borgmatic/meta/main.yml b/roles/borgmatic/meta/main.yml new file mode 100644 index 0000000..868031b --- /dev/null +++ b/roles/borgmatic/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "borgmatic" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/borgmatic/molecule/default/converge.yml b/roles/borgmatic/molecule/default/converge.yml new file mode 100644 index 0000000..cb3fdb1 --- /dev/null +++ b/roles/borgmatic/molecule/default/converge.yml @@ -0,0 +1,48 @@ +--- +- name: Converge + hosts: all + vars: + repo_path: "/home/borg/test-repo" + + pre_tasks: + - name: "Create a user for borg" + ansible.builtin.user: + name: borg + shell: /bin/bash + create_home: true + + - name: "Generate test data file" + ansible.builtin.copy: + dest: "/tmp/data" + content: "This is a test file!" + owner: "{{ ansible_user_id }}" + group: "{{ ansible_user_id }}" + mode: "0644" + + - name: "Ensure the repo path exists" + ansible.builtin.file: + path: "{{ repo_path }}" + state: directory + owner: "borg" + mode: "0700" + + - name: "Install openssh-server" + ansible.builtin.apt: + name: openssh-server + state: present + update_cache: true + + - name: "Start an SSH openssh-server" + ansible.builtin.systemd: + name: ssh + state: started + + roles: + - role: genlab.common.borgmatic + borgmatic_source_directories: + - "/tmp/data" + borgmatic_repo_path: "ssh://borg@localhost/./test-repo" + borgmatic_repo_label: "test-repo" + borgmatic_encryption_passphrase: "secret" + repo_server_inventory_hostname: ubuntu # in production this should be an Ansible inventory hostname + repo_server_user: borg diff --git a/roles/borgmatic/molecule/default/molecule.yml b/roles/borgmatic/molecule/default/molecule.yml new file mode 100644 index 0000000..2910ca3 --- /dev/null +++ b/roles/borgmatic/molecule/default/molecule.yml @@ -0,0 +1,31 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +scenario: + name: default + test_sequence: + - destroy + - create + - converge + # - idempotence + - verify diff --git a/roles/borgmatic/molecule/default/verify.yml b/roles/borgmatic/molecule/default/verify.yml new file mode 100644 index 0000000..6edcb47 --- /dev/null +++ b/roles/borgmatic/molecule/default/verify.yml @@ -0,0 +1,48 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + vars: + repo_path: "/home/borg/test-repo" + + tasks: + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + - name: "Check if Borg is installed" + changed_when: false + register: borgmatic_installed_version + ansible.builtin.command: "/usr/bin/borg --version" + + - name: "Check Borg version" + ansible.builtin.assert: + that: borgmatic_installed_version.stdout.find(borgmatic_version) + success_msg: "borg version {{ borgmatic_version }} is installed and working" + fail_msg: "borg version {{ borgmatic_version }} is not installed or not working correctly" + + - name: "Check if Borgmatic is installed" + changed_when: false + register: borgmatic_installed_version + ansible.builtin.command: + cmd: "/usr/bin/borgmatic --version" + + - name: "Check that the test repo was created" + register: borgmatic_test_repo_readme + failed_when: borgmatic_test_repo_readme.stat.exists is false + ansible.builtin.stat: + path: "{{ repo_path }}" + + - name: "Check that the systemd timer for Borgmatic is up and running" + register: borgmatic_timer + ansible.builtin.systemd: + name: borgmatic.timer + + - name: "Assert that the timer is running" + ansible.builtin.assert: + that: borgmatic_timer.status.ActiveState == "active" + success_msg: "Timer is running" + fail_msg: "Unexpected timer state: '{{ borgmatic_timer.status.ActiveState }}'" diff --git a/roles/borgmatic/tasks/config.yml b/roles/borgmatic/tasks/config.yml new file mode 100644 index 0000000..78dc67d --- /dev/null +++ b/roles/borgmatic/tasks/config.yml @@ -0,0 +1,34 @@ +--- +- name: "Compose basic configuration for Borgmatic" + ansible.builtin.set_fact: + borgmatic_composite_config: + source_directories: "{{ borgmatic_source_directories }}" + repositories: + - path: "{{ borgmatic_repo_path }}" + label: "{{ borgmatic_repo_label }}" + encryption_passphrase: "{{ borgmatic_encryption_passphrase }}" + compression: "{{ borgmatic_compression }}" + # CLI output configuration + list_details: true + statistics: true + exclude_caches: true + # logging verbosity: + verbosity: 1 + syslog_verbosity: 1 + monitoring_verbosity: 1 + # backup depth + keep_hourly: "{{ borgmatic_keep_hourly }}" + keep_daily: "{{ borgmatic_keep_daily }}" + keep_weekly: "{{ borgmatic_keep_weekly }}" + keep_monthly: "{{ borgmatic_keep_monthly }}" + keep_yearly: "{{ borgmatic_keep_yearly }}" + +- name: "Add Uptime Kuma configuration" + when: borgmatic_uptime_kuma + ansible.builtin.set_fact: + borgmatic_composite_config: "{{ borgmatic_composite_config | combine({'uptime_kuma': borgmatic_uptime_kuma}) }}" + +- name: "Add Loki configuration" + when: borgmatic_loki + ansible.builtin.set_fact: + borgmatic_composite_config: "{{ borgmatic_composite_config | combine({'loki': borgmatic_loki}) }}" diff --git a/roles/borgmatic/tasks/handle-ssh-keys.yml b/roles/borgmatic/tasks/handle-ssh-keys.yml new file mode 100644 index 0000000..07e9c07 --- /dev/null +++ b/roles/borgmatic/tasks/handle-ssh-keys.yml @@ -0,0 +1,25 @@ +--- +- name: "Ensure the path for SSH keys exists" + ansible.builtin.file: + path: "{{ borgmatic_sshkey_path | dirname }}" + state: directory + owner: root + group: root + mode: "0700" + +- name: "Generate an ed25519 SSH key pair with 100 KDF rounds" + register: borgmatic_ssh_key_pair + community.crypto.openssh_keypair: + type: ed25519 + path: "{{ borgmatic_sshkey_path }}" + comment: "Generated by Ansible for Borgmatic" + force: false + mode: '0600' + +- name: "Push the SSH key pair to the Borg repo host" + when: borgmatic_push_pubkey + delegate_to: "{{ repo_server_inventory_hostname }}" + ansible.posix.authorized_key: + user: "{{ repo_server_user }}" + key: "{{ borgmatic_ssh_key_pair.public_key }}" + state: present diff --git a/roles/borgmatic/tasks/install.yml b/roles/borgmatic/tasks/install.yml new file mode 100644 index 0000000..46f5203 --- /dev/null +++ b/roles/borgmatic/tasks/install.yml @@ -0,0 +1,66 @@ +--- +- name: "Ensure that system dependencies are installed" + ansible.builtin.apt: + name: + - openssh-client + - python3-pip + - python3-venv + state: present + update_cache: true + cache_valid_time: 3600 + +- name: "Install pipx" + retries: 3 + delay: 1 + ansible.builtin.pip: + name: "pipx=={{ borgmatic_pipx_version }}" + executable: pip3 + break_system_packages: true + +- name: "Ensure pipx binary is available in PATH" + changed_when: false + ansible.builtin.command: + cmd: pipx ensurepath + +- name: "Install borgmatic via pipx" + retries: 3 + delay: 1 + environment: + PIPX_BIN_DIR: "{{ borgmatic_pipx_bin_dir }}" + community.general.pipx: + name: borgmatic + state: present + install_deps: true + +- name: "Install Borg if the correct version is not available" + block: + # we are looking for Borg installed in a directory that Max Hoesel's role exects to find it + - name: "Get the currently installed version of Borg" + changed_when: false + register: borgmatic_version_installed + ansible.builtin.command: + cmd: /usr/bin/borg --version + + - name: "Check that the correct version of Borg is installed" + ansible.builtin.assert: + that: borgmatic_version_installed.stdout.find(borgmatic_version) + fail_msg: "The expected Borg version was not found: {{ borgmatic_version_installed }}" + success_msg: "Found the expected Borg version ({{ borgmatic_version }})" + + rescue: + - name: "Download Borg from a custom URL: '{{ borgmatic_binary_url }}'" + retries: 3 + delay: 1 + ansible.builtin.unarchive: + src: "{{ borgmatic_binary_url }}" + dest: "/opt/" + remote_src: true + owner: root + group: root + mode: "0755" + + - name: "Create a symbolic link for Borg" + ansible.builtin.file: + state: link + src: "/opt/borg-dir/borg.exe" + dest: "/usr/bin/borg" diff --git a/roles/borgmatic/tasks/integrations.yml b/roles/borgmatic/tasks/integrations.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/borgmatic/tasks/integrations.yml @@ -0,0 +1 @@ +--- diff --git a/roles/borgmatic/tasks/main.yml b/roles/borgmatic/tasks/main.yml new file mode 100644 index 0000000..0207ec6 --- /dev/null +++ b/roles/borgmatic/tasks/main.yml @@ -0,0 +1,15 @@ +--- +- name: "Include tool installation tasks" + ansible.builtin.include_tasks: "install.yml" + +- name: "Include SSH key handling tasks" + ansible.builtin.include_tasks: "handle-ssh-keys.yml" + +- name: "Include configuration tasks" + ansible.builtin.include_tasks: "config.yml" + +- name: "Include tasks for third-party integrations" + ansible.builtin.include_tasks: "integrations.yml" + +- name: "Include tasks for running borgmatic" + ansible.builtin.include_tasks: "run.yml" diff --git a/roles/borgmatic/tasks/run.yml b/roles/borgmatic/tasks/run.yml new file mode 100644 index 0000000..28051f5 --- /dev/null +++ b/roles/borgmatic/tasks/run.yml @@ -0,0 +1,22 @@ +--- +# A workaround for maxhoesel.borgbackup.borgmatic that does not support custom paths +- name: "Create symbolic links for Borgmatic executables" + loop: + - borgmatic + - generate-borgmatic-config + - validate-borgmatic-config + ansible.builtin.file: + state: link + src: "{{ borgmatic_pipx_bin_dir }}/{{ item }}" + dest: /usr/bin/{{ item }} + +- name: "Configure and run Borgmatic" + ansible.builtin.include_role: + name: maxhoesel.borgbackup.borgmatic + vars: + borgmatic_install: false # we handle installation separately to get the recent version + # borgmatic_ssh_key_gen_options: "-t ed25519 -a 100" + borgmatic_ssh_key_path: "{{ borgmatic_sshkey_path }}" + borgmatic_schedule_on: "{{ borgmatic_schedule_oncalendar }}" + borgmatic_schedule_max_random_delay: 600 + borgmatic_config: "{{ borgmatic_composite_config }}" diff --git a/roles/borgmatic/vars/main.yml b/roles/borgmatic/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/borgmatic/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/curl_scheduled/.gitignore b/roles/curl_scheduled/.gitignore new file mode 100644 index 0000000..8cd0df3 --- /dev/null +++ b/roles/curl_scheduled/.gitignore @@ -0,0 +1,2 @@ +.vscode +.idea \ No newline at end of file diff --git a/roles/curl_scheduled/README.md b/roles/curl_scheduled/README.md new file mode 100644 index 0000000..285bbb0 --- /dev/null +++ b/roles/curl_scheduled/README.md @@ -0,0 +1,41 @@ +curl-scheduled +========= + +Configure curl to run on schedule by deploying a systemd service + timer. Useful for sending heartbeats. + +Requirements +------------ + +None + +Role Variables +-------------- + +- `args`: arguments to the curl command + +- `url`: address to be accessed by curl + +- `schedule`: string compatible with systemd timer `OnSchedule` option (default: `minutely`) + + +Dependencies +------------ + +None + +Example Playbook +---------------- + +See `molecule/default/converge.yml`. + +License +------- + +BSD + +Author Information +------------------ + +msayganova@genlab.llc + +corvus-migratorius@proton.me diff --git a/roles/curl_scheduled/defaults/main.yml b/roles/curl_scheduled/defaults/main.yml new file mode 100644 index 0000000..a6f0e51 --- /dev/null +++ b/roles/curl_scheduled/defaults/main.yml @@ -0,0 +1,4 @@ +--- +curl_scheduled_curl_cmd: "/usr/bin/curl" +curl_scheduled_curl_args: "-fsS -m 10" +curl_scheduled_schedule: "minutely" diff --git a/roles/curl_scheduled/handlers/main.yml b/roles/curl_scheduled/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/curl_scheduled/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/curl_scheduled/meta/main.yml b/roles/curl_scheduled/meta/main.yml new file mode 100644 index 0000000..894fc30 --- /dev/null +++ b/roles/curl_scheduled/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: curl_scheduled + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/curl_scheduled/molecule/default/converge.yml b/roles/curl_scheduled/molecule/default/converge.yml new file mode 100644 index 0000000..0445024 --- /dev/null +++ b/roles/curl_scheduled/molecule/default/converge.yml @@ -0,0 +1,45 @@ +--- +- name: Converge + hosts: all + + pre_tasks: + - name: "Create test directory" + ansible.builtin.file: + path: "/test" + state: directory + mode: "0664" + + - name: "Create a test file" + ansible.builtin.lineinfile: + path: "/test/index.html" + create: true + mode: "0664" + line: "OK" + + - name: "Simulate remote HTTP server(s) directly on localhost" + changed_when: false + async: 1 + poll: 0 + args: + chdir: "/test" + loop: + - 8080 + - 8081 + - 8082 + ansible.builtin.shell: + cmd: nohup python3 -m http.server {{ item }} /dev/null 2>&1 & + executable: /bin/bash + + roles: + - role: genlab.common.curl_scheduled + services: + - label: "localhost-test-zero" + url: "http://127.0.0.1:8080" + schedule: minutely + - label: "localhost-test-one" + url: "http://127.0.0.1:8081" + schedule: hourly + - label: "localhost-test-chained-curl" + curl_cmd: '/usr/bin/curl "http://127.0.0.1:8081" && /usr/bin/curl' + url: "http://127.0.0.1:8082" + schedule: minutely diff --git a/roles/curl_scheduled/molecule/default/molecule.yml b/roles/curl_scheduled/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/curl_scheduled/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/curl_scheduled/molecule/default/verify.yml b/roles/curl_scheduled/molecule/default/verify.yml new file mode 100644 index 0000000..5ba80ca --- /dev/null +++ b/roles/curl_scheduled/molecule/default/verify.yml @@ -0,0 +1,81 @@ +--- +- name: Verify + hosts: all + gather_facts: false + + tasks: + # Verify the systemd timers + + - name: "Check the timer status - localhost-test-zero" + register: curl_scheduled_timer_zero + ansible.builtin.systemd: + name: "curl-localhost-test-zero.timer" + + - name: "Assert that the localhost-test-zero timer is running" + ansible.builtin.assert: + that: + - curl_scheduled_timer_zero.status.ActiveState == "active" + success_msg: "Timer is running" + fail_msg: "Unexpected timer state: '{{ curl_scheduled_timer_zero.status.ActiveState }}'" + + - name: "Check the timer status - localhost-test-one" + register: curl_scheduled_timer_one + ansible.builtin.systemd: + name: "curl-localhost-test-one.timer" + + - name: "Assert that the localhost-test-one timer is running" + ansible.builtin.assert: + that: + - curl_scheduled_timer_one.status.ActiveState == "active" + success_msg: "Timer is running" + fail_msg: "Unexpected timer state: '{{ curl_scheduled_timer_one.status.ActiveState }}'" + + - name: "Check the timer status - localhost-test-chained-curl" + register: curl_scheduled_timer_chained_curl + ansible.builtin.systemd: + name: "curl-localhost-test-chained-curl.timer" + + - name: "Assert that the localhost-test-chained-curl timer is running" + ansible.builtin.assert: + that: + - curl_scheduled_timer_chained_curl.status.ActiveState == "active" + success_msg: "Timer is running" + fail_msg: "Unexpected timer state: '{{ curl_scheduled_timer_chained_curl.status.ActiveState }}'" + + ## Verify the systemd services + + - name: "Check the service status - zero" + register: curl_scheduled_service_zero + ansible.builtin.systemd: + name: "curl-localhost-test-zero.service" + + - name: "Assert that the localhost-test-zero service exited with a 0/SUCCESS status" + ansible.builtin.assert: + that: + - 'curl_scheduled_service_zero.status.ExecMainStatus == "0"' + success_msg: "Service has exited with a 0/SUCCESS status" + fail_msg: "Unexpected service status code: '{{ curl_scheduled_service_zero.status.ExecMainStatus }}'" + + - name: "Check the service status - one" + register: curl_scheduled_service_one + ansible.builtin.systemd: + name: "curl-localhost-test-one.service" + + - name: "Assert that the localhost-test-one service exited with a 0/SUCCESS status" + ansible.builtin.assert: + that: + - 'curl_scheduled_service_one.status.ExecMainStatus == "0"' + success_msg: "Service has exited with a 0/SUCCESS status" + fail_msg: "Unexpected service status code: '{{ curl_scheduled_service_one.status.ExecMainStatus }}'" + + - name: "Check the service status - localhost-test-chained-curl" + register: curl_scheduled_service_chained_curl + ansible.builtin.systemd: + name: "curl-localhost-test-chained-curl.service" + + - name: "Assert that the localhost-test-chained-curl service exited with a 0/SUCCESS status" + ansible.builtin.assert: + that: + - 'curl_scheduled_service_chained_curl.status.ExecMainStatus == "0"' + success_msg: "Service has exited with a 0/SUCCESS status" + fail_msg: "Unexpected service status code: '{{ curl_scheduled_service_chained_curl.status.ExecMainStatus }}'" diff --git a/roles/curl_scheduled/tasks/deploy-service.yml b/roles/curl_scheduled/tasks/deploy-service.yml new file mode 100644 index 0000000..66e4239 --- /dev/null +++ b/roles/curl_scheduled/tasks/deploy-service.yml @@ -0,0 +1,33 @@ +--- +- name: "Deploy-Service | Create systemd service file: '{{ job.label }}'" + vars: + label: "{{ job.label }}" + url: "{{ job.url }}" + curl_cmd: "{{ job.curl_cmd | default(curl_scheduled_curl_cmd) }}" + curl_args: "{{ job.curl_args | default(curl_scheduled_curl_args) }}" + register: curl_scheduled_service + ansible.builtin.template: + src: "placeholder.service" + dest: "/etc/systemd/system/curl-{{ job.label }}.service" + mode: "0660" + validate: systemd-analyze verify %s + +- name: "Deploy-Service | Create systemd timer file: '{{ job.label }}'" + vars: + label: "{{ job.label }}" + schedule: "{{ job.schedule | default(curl_scheduled_schedule) }}" + register: curl_scheduled_timer + ansible.builtin.template: + src: "placeholder.timer" + dest: "/etc/systemd/system/curl-{{ job.label }}.timer" + mode: "0660" + validate: systemd-analyze verify %s + +- name: "Deploy-Service | Enable and start the timer: '{{ job.label }}'" # noqa: no-handler + become: true + when: curl_scheduled_service.changed or curl_scheduled_timer.changed + ansible.builtin.systemd: + name: "curl-{{ job.label }}.timer" + state: started + enabled: true + daemon_reload: true diff --git a/roles/curl_scheduled/tasks/main.yml b/roles/curl_scheduled/tasks/main.yml new file mode 100644 index 0000000..b0a19de --- /dev/null +++ b/roles/curl_scheduled/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: "Install curl (Debian derivatives)" + when: ansible_os_family == "Debian" + ansible.builtin.apt: + name: curl + state: present + update_cache: true + +- name: "Install curl (RHEL derivatives)" + when: ansible_os_family == "RedHat" + ansible.builtin.dnf: + name: curl + state: present + +- name: "Configure and deploy systemd service" + loop: "{{ services }}" + loop_control: + loop_var: "job" + ansible.builtin.include_tasks: "deploy-service.yml" diff --git a/roles/curl_scheduled/templates/placeholder.service b/roles/curl_scheduled/templates/placeholder.service new file mode 100644 index 0000000..e9222cf --- /dev/null +++ b/roles/curl_scheduled/templates/placeholder.service @@ -0,0 +1,21 @@ +[Unit] +Description=Run an HTTP request via curl designated '{{ label }}' +After=network.target + +[Service] +Type=oneshot +ExecStart={{ curl_cmd }} {{ curl_args }} "{{ url }}" +Restart=no + +# Security hardening +ProtectSystem=strict +NoNewPrivileges=true +PrivateTmp=true +ProtectKernelModules=true +ProtectControlGroups=true +ProtectKernelTunables=true +ProtectClock=yes +RestrictSUIDSGID=true + +[Install] +WantedBy=multi-user.targer diff --git a/roles/curl_scheduled/templates/placeholder.timer b/roles/curl_scheduled/templates/placeholder.timer new file mode 100644 index 0000000..0118d10 --- /dev/null +++ b/roles/curl_scheduled/templates/placeholder.timer @@ -0,0 +1,12 @@ +[Unit] +Description=Trigger a curl command designated '{{ label }}' +Requires=curl-{{ label }}.service + +[Timer] +Unit=curl-{{ label }}.service +OnCalendar={{ schedule }} +AccuracySec=1m +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/roles/curl_scheduled/vars/main.yml b/roles/curl_scheduled/vars/main.yml new file mode 100644 index 0000000..01952d4 --- /dev/null +++ b/roles/curl_scheduled/vars/main.yml @@ -0,0 +1,2 @@ +--- +curl_scheduled_kuma_port: "3001" diff --git a/roles/dnsmasq/README.md b/roles/dnsmasq/README.md new file mode 100644 index 0000000..2e1d6b4 --- /dev/null +++ b/roles/dnsmasq/README.md @@ -0,0 +1,37 @@ +ansible-dnsmasq +========= + +Deploy dnsmasq on the target node. For now, supports only DNS functionality (DHCP and TFTP are not configuratble). + +Requirements +------------ + +Take care to open the port you choose for dnsmasq to serve queries on. This role does not handle firewall configuration. + +Role Variables +-------------- + +None + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.dnsmasq +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/dnsmasq/defaults/main.yml b/roles/dnsmasq/defaults/main.yml new file mode 100644 index 0000000..24e3d56 --- /dev/null +++ b/roles/dnsmasq/defaults/main.yml @@ -0,0 +1,3 @@ +--- +dnsmasq_cache_size: 100 +dnsmasq_dns_port: 5300 diff --git a/roles/dnsmasq/handlers/main.yml b/roles/dnsmasq/handlers/main.yml new file mode 100644 index 0000000..c30be1c --- /dev/null +++ b/roles/dnsmasq/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "Restart dnsmasq" + ansible.builtin.systemd_service: + name: dnsmasq + state: restarted + daemon_reload: true + enabled: true diff --git a/roles/dnsmasq/meta/main.yml b/roles/dnsmasq/meta/main.yml new file mode 100644 index 0000000..03ddef7 --- /dev/null +++ b/roles/dnsmasq/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: dnsmasq + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/dnsmasq/molecule/default/converge.yml b/roles/dnsmasq/molecule/default/converge.yml new file mode 100644 index 0000000..6a8f31d --- /dev/null +++ b/roles/dnsmasq/molecule/default/converge.yml @@ -0,0 +1,21 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.ufw + disable_ipv6: true + rules: + - rule: allow + # proto: udp + port: 5300 + interface: lo + direction: in + comment: "Allow dnsmasq to serve DNS queries on the given interface" + + - role: genlab.common.dnsmasq + dnsmasq_iface: lo + dnsmasq_domain: adm.local + dnsmasq_dns_port: 5300 + dnsmasq_nodes: + - name: hub + ip: 127.0.0.1 diff --git a/roles/dnsmasq/molecule/default/molecule.yml b/roles/dnsmasq/molecule/default/molecule.yml new file mode 100644 index 0000000..fd2f06d --- /dev/null +++ b/roles/dnsmasq/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/dnsmasq/molecule/default/verify.yml b/roles/dnsmasq/molecule/default/verify.yml new file mode 100644 index 0000000..9a45b9e --- /dev/null +++ b/roles/dnsmasq/molecule/default/verify.yml @@ -0,0 +1,19 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + pre_tasks: + - name: "Install a package providing the `dig` tool" + ansible.builtin.apt: + name: dnsutils + state: present + + tasks: + - name: "Test the output of the `dig` command" + changed_when: false + register: dnsmasq_dig + failed_when: 'dnsmasq_dig.stdout != "127.0.0.1"' + ansible.builtin.command: + cmd: "dig @127.0.0.1 -p 5300 hub.adm.local +short" diff --git a/roles/dnsmasq/tasks/main.yml b/roles/dnsmasq/tasks/main.yml new file mode 100644 index 0000000..ff29987 --- /dev/null +++ b/roles/dnsmasq/tasks/main.yml @@ -0,0 +1,38 @@ +--- +- name: "Install dnsmasq" + ansible.builtin.apt: + name: dnsmasq + state: present + update_cache: true + + +- name: "Create interface-specific configuration file" + notify: "Restart dnsmasq" + ansible.builtin.blockinfile: + path: /etc/dnsmasq.d/{{ dnsmasq_domain }}.conf + create: true + owner: root + group: root + mode: "0660" + block: | + interface="{{ dnsmasq_iface }}" + port="{{ dnsmasq_dns_port }}" + cache-size="{{ dnsmasq_cache_size }}" + log-queries + server=1.1.1.1 # Cloudflare DNS + server=8.8.8.8 # Google DNS + server=8.8.4.4 # Google DNS (secondary) + + +- name: "Add dnsmasq_nodes to the configuration file" + notify: "Restart dnsmasq" + loop: "{{ dnsmasq_nodes }}" + ansible.builtin.blockinfile: + path: /etc/dnsmasq.d/{{ dnsmasq_domain }}.conf + marker: "# {mark} ANSIBLE MANAGED: {{ item.name }}.{{ dnsmasq_domain }}" + block: | + address=/{{ item.name }}.{{ dnsmasq_domain }}/{{ item.ip }} + + +- name: "Flush handlers" + ansible.builtin.meta: flush_handlers diff --git a/roles/dnsmasq/vars/main.yml b/roles/dnsmasq/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/dnsmasq/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/grafana/README.md b/roles/grafana/README.md new file mode 100644 index 0000000..a700863 --- /dev/null +++ b/roles/grafana/README.md @@ -0,0 +1,107 @@ +genlab.grafana +========= + +This ansible role installs [Grafana](https://github.com/grafana/grafana) - the open-source platform for monitoring and observability. It can produce charts, graphs, and alerts for the web when connected to supported data sources. + +This role installs and configures Grafana from a binary distribution. It also: + +- Uploads custom dashboards + +- Installs plugins + +- Imports public dashboards and data sources + +- Allows admin password changes + +Supports user creation + Grafana service requires an environment file at startup, where you can set custom paths for logs (`grafana_log_dir`), data (`grafana_data_dir`), and plugins (`grafana_plugins_dir`). This allows flexible control over Grafana's data storage. + + Requirements +------------ + +You need `community.grafana` module. + +Role Variables +-------------- + +``` +grafana_user: "grafana" # user name +grafana_group: "grafana" # group name +grafana_version: 11.5.0 # version +grafana_port: 3000 # port + +# Directory paths +grafana_dashboard_dir: "/tmp/grafana/dashboards" # where to copy dashboards from source +grafana_plugins_dir: "/var/lib/grafana/plugins" # where to store plugins on target +grafana_datasource_dir: "/etc/grafana/provisioning/datasources" # where to store data sources on target +grafana_log_dir: "/var/log/grafana" # where to write logs +grafana_data_dir: "/var/lib/grafana" # where to store Grafana DB + +# Optional configurations +grafana_users: [] # array of user names, passwords, and statuses +grafana_plugins: [] # array of plugins to install +grafana_public_dashboards: [] # array of public dashboards to import + +# Admin credentials +admin_api_username: "secret" # Grafana admin username +admin_api_password: "secret" # Grafana admin password + +# Source paths +dashboard_source_path: "mydir/dashboards" # path to dashboards on source server +datasource_source_path: "mydir/datasources" # path to data sources on source server +``` + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +--- +- name: Converge + hosts: all + vars: + grafana_users: + - name: "test" + user_login: "test" + user_password: "test" + user_email: "test@mail.ru" + is_admin: false + grafana_plugins: + version: 2.1.8 + - name: aceiot-svg-panel + version: 0.1.5 + grafana_public_dashboards: + - name: Node Full Exporter + id: 1860 + revision: 36 + vars_files: + - secrets/admin_cred.yml + + roles: + - role: genlab.grafana + grafana_version: 11.5.0 + admin_api_username: "{{ grafana.admin_api_username }}" + admin_api_password: "{{ grafana.admin_api_password }}" + users: "{{ grafana_users }}" + plugins: "{{ grafana_plugins }}" + public_dashboards: "{{ grafana_public_dashboards }}" + dashboard_source_path: "molecule/default/dashboards" + datasource_source_path: "molecule/default/datasources" + grafana_log_dir: "/opt/grafana/data" + grafana_data_dir: "/opt/grafana/lib" + +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/grafana/defaults/main.yml b/roles/grafana/defaults/main.yml new file mode 100644 index 0000000..535854f --- /dev/null +++ b/roles/grafana/defaults/main.yml @@ -0,0 +1,10 @@ +--- +grafana_user: "grafana" +grafana_group: "grafana" +grafana_version: 11.5.0 +grafana_port: 3000 +grafana_dashboard_dir: "/tmp/grafana/dashboards" +grafana_plugins_dir: "/var/lib/grafana/plugins" +grafana_datasource_dir: "/etc/grafana/provisioning/datasources" +grafana_log_dir: "/var/log/grafana" +grafana_data_dir: "/var/lib/grafana" diff --git a/roles/grafana/handlers/main.yml b/roles/grafana/handlers/main.yml new file mode 100644 index 0000000..0b67d92 --- /dev/null +++ b/roles/grafana/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "(Re)start and enable Grafana" + ansible.builtin.systemd_service: + name: grafana-server + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/grafana/meta/main.yml b/roles/grafana/meta/main.yml new file mode 100644 index 0000000..cec0dd3 --- /dev/null +++ b/roles/grafana/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "grafana" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/grafana/molecule/default/converge.yml b/roles/grafana/molecule/default/converge.yml new file mode 100644 index 0000000..a73b0da --- /dev/null +++ b/roles/grafana/molecule/default/converge.yml @@ -0,0 +1,44 @@ +--- +- name: Converge + hosts: all + vars: + grafana_users: + - name: "test" + user_login: "test" + # kics-scan ignore-line + user_password: "test" + user_email: "test@mail.ru" + is_admin: false + - name: "test2" + user_login: "test2" + # kics-scan ignore-line + user_password: "test2" + user_email: "test2@mail.ru" + is_admin: true + grafana_plugins: + - name: grafana-metricsdrilldown-app + version: 1.0.0 + - name: grafana-clock-panel + version: 2.1.8 + - name: aceiot-svg-panel + version: 0.1.5 + grafana_public_dashboards: + - name: Node Full Exporter + id: 1860 + revision: 36 + vars_files: + - secrets/admin_cred.yml + + roles: + # kics-scan ignore-block + - role: genlab.common.grafana + grafana_version: 11.5.0 + admin_api_username: "{{ grafana.admin_api_username }}" + admin_api_password: "{{ grafana.admin_api_password }}" + users: "{{ grafana_users }}" + plugins: "{{ grafana_plugins }}" + public_dashboards: "{{ grafana_public_dashboards }}" + dashboard_source_path: "molecule/default/dashboards" + datasource_source_path: "molecule/default/datasources" + grafana_log_dir: "/opt/grafana/data" + grafana_data_dir: "/opt/grafana/lib" diff --git a/roles/grafana/molecule/default/dashboards/test_dashboard.json b/roles/grafana/molecule/default/dashboards/test_dashboard.json new file mode 100644 index 0000000..595932e --- /dev/null +++ b/roles/grafana/molecule/default/dashboards/test_dashboard.json @@ -0,0 +1,700 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "panel", + "id": "bargauge", + "name": "Bar gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "7.1.5" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 29, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "decimals": 0, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 15, + "x": 0, + "y": 0 + }, + "id": 4, + "options": { + "displayMode": "gradient", + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "valueMode": "color" + }, + "pluginVersion": "10.1.5", + "targets": [ + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", namespace=~\"$namespace\", status=~\"fail|error\" } > 0) by (namespace)", + "instant": true, + "interval": "", + "legendFormat": "{{namespace}}", + "refId": "A" + } + ], + "title": "Failing Policies by Namespace", + "type": "bargauge" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "decimals": 0, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 3 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 15, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "center", + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.1.5", + "targets": [ + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=~\"fail|error\" } > 0) by (status)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ status }}", + "refId": "A" + } + ], + "title": "Failing ClusterPolicies", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 8 + }, + "hiddenSeries": false, + "id": 11, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "10.1.5", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=~\"fail|error\" } > 0) by (policy)", + "interval": "", + "legendFormat": "{{ policy }}", + "refId": "A" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", namespace=~\"$namespace\", status=~\"fail|error\" } > 0) by (policy)", + "interval": "", + "legendFormat": "{{ policy }}", + "refId": "B" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Failing Policies Graph", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 7, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.1.5", + "targets": [ + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", namespace=~\"$namespace\", status=~\"fail|error\" }) by (namespace,policy,rule,kind,name,status,category,severity,source)", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "{{namespace}}: {{ policy }}", + "refId": "A" + } + ], + "title": "Failing PolicyRules", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true + }, + "indexByName": { + "category": 1, + "kind": 4, + "name": 5, + "namespace": 3, + "policy": 6, + "rule": 7, + "severity": 2, + "source": 0, + "status": 8 + }, + "renameByName": { + "namespace": "namespace" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 9, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.1.5", + "targets": [ + { + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(cluster_policy_report_result{policy=~\"$policy\", category=~\"$category\", severity=~\"$severity\", source=~\"$source\", kind=~\"$kind\", status=~\"fail|error\" }) by (policy,rule,kind,name,status,category,severity,source)", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "{{ kind }}: {{ name }} - {{ policy }}", + "refId": "A" + } + ], + "title": "Failing ClusterPolicyRules", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true, + "__name__": true, + "container": true, + "endpoint": true, + "instance": true, + "job": true, + "namespace": true, + "pod": true, + "report": true, + "service": true + }, + "indexByName": { + "category": 1, + "kind": 3, + "name": 4, + "policy": 5, + "rule": 6, + "severity": 2, + "source": 0, + "status": 7 + }, + "renameByName": {} + } + } + ], + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 38, + "style": "dark", + "tags": [ + "Policy Reporter" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, policy)", + "hide": 0, + "includeAll": true, + "label": "Policy", + "multi": true, + "name": "policy", + "options": [], + "query": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, policy)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, category)", + "hide": 0, + "includeAll": true, + "label": "Category", + "multi": true, + "name": "category", + "options": [], + "query": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, category)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, severity)", + "hide": 0, + "includeAll": true, + "label": "Severity", + "multi": true, + "name": "severity", + "options": [], + "query": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, severity)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values({__name__= \"policy_report_result\", status=~\"fail|error\"}, namespace)", + "hide": 0, + "includeAll": true, + "label": "Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": "label_values({__name__= \"policy_report_result\", status=~\"fail|error\"}, namespace)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, kind)", + "hide": 0, + "includeAll": true, + "label": "Kind", + "multi": true, + "name": "kind", + "options": [], + "query": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, kind)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, source)", + "hide": 0, + "includeAll": true, + "label": "Source", + "multi": true, + "name": "source", + "options": [], + "query": "label_values({__name__=~ \"policy_report_result|cluster_policy_report_result\", status=~\"fail|error\"}, source)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "PolicyReports", + "uid": "ZkwXrUMnk", + "version": 1, + "gnetId": 13968 +} \ No newline at end of file diff --git a/roles/grafana/molecule/default/dashboards/test_dashboard2.json b/roles/grafana/molecule/default/dashboards/test_dashboard2.json new file mode 100644 index 0000000..a100af0 --- /dev/null +++ b/roles/grafana/molecule/default/dashboards/test_dashboard2.json @@ -0,0 +1,2081 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "7.5.6" + }, + { + "type": "panel", + "id": "piechart", + "name": "Pie chart v2", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "A dashboard for the CoreDNS DNS server with updated metrics for version 1.7.0+. Based on the CoreDNS 1.7.0+ dashboard by ejkinger", + "editable": true, + "gnetId": 14981, + "graphTooltip": 0, + "id": null, + "iteration": 1632672605392, + "links": [ + { + "$$hashKey": "object:94", + "icon": "external link", + "tags": [], + "targetBlank": true, + "title": "CoreDNS.io", + "type": "link", + "url": "https://coredns.io" + } + ], + "panels": [ + { + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 44, + "title": "Global stats", + "type": "row" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 39, + "links": [], + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "table", + "placement": "right", + "values": [ + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "text": {} + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_dns_requests_total[5m])) by (instance)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{instance}}", + "refId": "A", + "step": 60 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Requests (by instance)", + "type": "piechart" + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 41, + "panels": [], + "repeat": "instance", + "title": "Health: $instance", + "type": "row" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "super-light-blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 10 + }, + "id": 42, + "links": [], + "maxPerRow": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "name" + }, + "pluginVersion": "7.5.6", + "repeat": null, + "repeatDirection": "v", + "targets": [ + { + "exemplar": true, + "expr": "coredns_build_info{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{version}}", + "refId": "A", + "step": 60 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Version", + "type": "stat" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 6, + "y": 10 + }, + "id": 35, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_forward_healthcheck_broken_total{instance=~\"$instance\"}[5m]))", + "interval": "", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Upstream Health Check Fails", + "type": "stat" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 9, + "y": 10 + }, + "id": 36, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_forward_max_concurrent_rejects_total{instance=~\"$instance\"}[5m]))", + "interval": "", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "title": "Upstream Rejected Queries", + "type": "stat" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 12, + "y": 10 + }, + "id": 81, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_panics_total{instance=~\"$instance\"}[5m]))", + "interval": "", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panics", + "type": "stat" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 15, + "y": 10 + }, + "id": 92, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_reload_failed_total{instance=~\"$instance\"}[5m]))", + "interval": "", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Failed Reloads", + "type": "stat" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 0.03, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 85 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 18, + "y": 10 + }, + "id": 119, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "text": {} + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(process_cpu_seconds_total{instance=~\"$instance\"}[5m]))", + "interval": "", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU Time", + "type": "gauge" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "super-light-blue", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 21, + "y": 10 + }, + "id": 134, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value" + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "go_memstats_alloc_bytes{instance=~\"$instance\"}", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Memory Usage", + "type": "stat" + }, + { + "collapsed": false, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 26, + "panels": [], + "repeat": null, + "title": "Local", + "type": "row" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 2, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_dns_requests_total{instance=~\"$instance\"}[5m])) by (server)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{server}}", + "refId": "A", + "step": 60 + }, + { + "exemplar": true, + "expr": "sum(rate(coredns_cache_requests_total{instance=~\"$instance\"}[5m]))", + "hide": false, + "interval": "", + "legendFormat": "cache", + "refId": "B" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Requests (total)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 6, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_dns_requests_total{instance=~\"$instance\"}[5m])) by (zone)", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{zone}}", + "refId": "A", + "step": 60 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Requests (by zone)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 38 + }, + "id": 32, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "histogram_quantile(0.99, sum(rate(coredns_dns_request_duration_seconds_bucket{instance=~\"$instance\", zone=\".\"}[5m])) by (le))", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "99%", + "refId": "A", + "step": 40 + }, + { + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(coredns_dns_request_duration_seconds_bucket{instance=~\"$instance\", zone=\".\"}[5m])) by (le))", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "90%", + "refId": "B", + "step": 40 + }, + { + "exemplar": true, + "expr": "histogram_quantile(0.50, sum(rate(coredns_dns_request_duration_seconds_bucket{instance=~\"$instance\", zone=\".\"}[5m])) by (le))", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "50%", + "refId": "C", + "step": 40 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Responses (latency, internet zone)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 38 + }, + "id": 4, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_dns_requests_total{instance=~\"$instance\"}[5m])) by (type)", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{type}}", + "refId": "A", + "step": 60 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Requests (by type)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 45 + }, + "id": 24, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_cache_hits_total{instance=~\"$instance\", type=\"success\"}[5m])) / sum(rate(coredns_cache_requests_total{instance=~\"$instance\"}[5m]))", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "hits: success", + "refId": "A", + "step": 40 + }, + { + "exemplar": true, + "expr": "sum(rate(coredns_cache_hits_total{instance=~\"$instance\", type=\"denial\"}[5m])) / sum(rate(coredns_cache_requests_total{instance=~\"$instance\"}[5m]))", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "hits: denial", + "refId": "B", + "step": 40 + }, + { + "exemplar": true, + "expr": "(sum(rate(coredns_cache_requests_total{instance=~\"$instance\"}[5m])) - sum(rate(coredns_cache_hits_total{instance=~\"$instance\", type=\"success\"}[5m]))) / sum(rate(coredns_cache_requests_total{instance=~\"$instance\"}[5m]))", + "hide": false, + "interval": "", + "legendFormat": "misses", + "refId": "C" + }, + { + "exemplar": true, + "expr": "sum(rate(coredns_dnssec_cache_hits_total{instance=~\"$instance\"}[5m])) / sum(rate(coredns_cache_requests_total{instance=~\"$instance\"}[5m]))", + "hide": false, + "interval": "", + "legendFormat": "hits: DNSSEC", + "refId": "D" + }, + { + "exemplar": true, + "expr": "(sum(rate(coredns_cache_requests_total{instance=~\"$instance\"}[5m])) - sum(rate(coredns_dnssec_cache_hits_total{instance=~\"$instance\"}[5m]))) / sum(rate(coredns_cache_requests_total{instance=~\"$instance\"}[5m]))", + "hide": false, + "interval": "", + "legendFormat": "misses: DNSSEC", + "refId": "E" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Cache (hitrate)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 45 + }, + "id": 8, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_dns_do_requests_total{instance=~\"$instance\"}[5m])) by (zone)", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{zone}}", + "refId": "A", + "step": 40 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Requests (DNSSEC by zone)", + "type": "timeseries" + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 52 + }, + "id": 14, + "interval": null, + "links": [], + "options": { + "displayLabels": [], + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "values": [ + "value", + "percent" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "text": {} + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_dns_responses_total{instance=~\"$instance\"}[5m])) by (rcode)", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{rcode}}", + "refId": "A", + "step": 40 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Responses (by code)", + "type": "piechart" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 52 + }, + "id": 18, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "histogram_quantile(0.99, sum(rate(coredns_dns_request_size_bytes_bucket{instance=~\"$instance\", zone=\".\"}[5m])) by (le))", + "interval": "", + "intervalFactor": 2, + "legendFormat": "99%", + "refId": "A", + "step": 40 + }, + { + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(coredns_dns_request_size_bytes_bucket{instance=~\"$instance\", zone=\".\"}[5m])) by (le))", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "90%", + "refId": "B", + "step": 40 + }, + { + "exemplar": true, + "expr": "histogram_quantile(0.50, sum(rate(coredns_dns_request_size_bytes_bucket{instance=~\"$instance\", zone=\".\"}[5m])) by (le))", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "50%", + "metric": "", + "refId": "C", + "step": 40 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Requests (size, internet zone)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 52 + }, + "id": 33, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "histogram_quantile(0.99, sum(rate(coredns_dns_response_size_bytes_bucket{instance=~\"$instance\", zone=\".\"}[5m])) by (le))", + "interval": "", + "intervalFactor": 2, + "legendFormat": "99%", + "refId": "A", + "step": 40 + }, + { + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(coredns_dns_response_size_bytes_bucket{instance=~\"$instance\", zone=\".\"}[5m])) by (le))", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "90%", + "refId": "B", + "step": 40 + }, + { + "exemplar": true, + "expr": "histogram_quantile(0.50, sum(rate(coredns_dns_response_size_bytes_bucket{instance=~\"$instance\", zone=\".\"}[5m])) by (le))", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "50%", + "metric": "", + "refId": "C", + "step": 40 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Responses (size, internet zone)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 59 + }, + "id": 22, + "links": [], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(coredns_cache_entries{instance=~\"$instance\"}) by (type)", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{type}}", + "refId": "A", + "step": 40 + }, + { + "exemplar": true, + "expr": "sum(coredns_dnssec_cache_entries{instance=~\"$instance\"})", + "hide": false, + "interval": "", + "legendFormat": "DNSSEC", + "refId": "B" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Cache (size)", + "type": "stat" + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 66 + }, + "id": 63, + "panels": [], + "title": "Upstream", + "type": "row" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 67 + }, + "id": 72, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_forward_requests_total{instance=~\"$instance\"}[5m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "upstream", + "refId": "A", + "step": 60 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Requests (total)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 67 + }, + "id": 38, + "links": [], + "maxPerRow": 6, + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "repeat": null, + "repeatDirection": "h", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_forward_conn_cache_hits_total{instance=~\"$instance\"}[5m])) / sum(rate(coredns_forward_requests_total{instance=~\"$instance\"}[5m]))", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "hits", + "refId": "A", + "step": 40 + }, + { + "exemplar": true, + "expr": "(sum(rate(coredns_forward_requests_total{instance=~\"$instance\"}[5m])) - sum(rate(coredns_forward_conn_cache_hits_total{instance=~\"$instance\"}[5m]))) / sum(rate(coredns_forward_requests_total{instance=~\"$instance\"}[5m]))", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "misses", + "refId": "B", + "step": 40 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Cache (hitrate)", + "type": "timeseries" + }, + { + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 74 + }, + "id": 37, + "links": [], + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "histogram_quantile(0.99, sum(rate(coredns_forward_request_duration_seconds_bucket{instance=~\"$instance\"}[5m])) by (le))", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "99%", + "refId": "A", + "step": 40 + }, + { + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(coredns_forward_request_duration_seconds_bucket{instance=~\"$instance\"}[5m])) by (le))", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "90%", + "refId": "B", + "step": 40 + }, + { + "exemplar": true, + "expr": "histogram_quantile(0.50, sum(rate(coredns_forward_request_duration_seconds_bucket{instance=~\"$instance\"}[5m])) by (le))", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "50%", + "refId": "C", + "step": 40 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Responses (latency)", + "type": "timeseries" + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 74 + }, + "id": 105, + "interval": null, + "links": [], + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "values": [ + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "text": {} + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_forward_requests_total{instance=~\"$instance\"}[5m])) by (to)", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{to}}", + "refId": "A", + "step": 40 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Requests (by upstream)", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "Time", + "1.0.0.1:853", + "1.1.1.1:853", + "8.8.4.4:853", + "8.8.8.8:853" + ] + } + } + } + ], + "type": "piechart" + }, + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 74 + }, + "id": 53, + "interval": null, + "links": [], + "options": { + "displayLabels": [], + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "values": [ + "value", + "percent" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "text": {} + }, + "pluginVersion": "7.5.6", + "targets": [ + { + "exemplar": true, + "expr": "sum(rate(coredns_forward_responses_total{instance=~\"$instance\"}[5m])) by (rcode)", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{rcode}}", + "refId": "A", + "step": 40 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Responses (by code)", + "type": "piechart" + } + ], + "refresh": "5s", + "schemaVersion": 27, + "style": "dark", + "tags": [ + "dns", + "coredns" + ], + "templating": { + "list": [ + { + "allValue": ".*", + "current": {}, + "datasource": "${DS_PROMETHEUS}", + "definition": "label_values(up{job=\"coredns\"}, instance)", + "description": null, + "error": null, + "hide": 0, + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(up{job=\"coredns\"}, instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 3, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "CoreDNS", + "uid": "wY4blRMGz", + "version": 134 +} \ No newline at end of file diff --git a/roles/grafana/molecule/default/datasources/test_datasource_loki.yaml b/roles/grafana/molecule/default/datasources/test_datasource_loki.yaml new file mode 100644 index 0000000..9c3ede0 --- /dev/null +++ b/roles/grafana/molecule/default/datasources/test_datasource_loki.yaml @@ -0,0 +1,16 @@ +apiVersion: 1 + +datasources: + - name: Loki + type: loki + access: proxy + basicAuth: false + basicAuthPassword: false + url: http://localhost:3100 + jsonData: + maxLines: 1000 + httpHeaderName1: "Authorization" + secureJsonData: + httpHeaderValue1: "Bearer your_token_here" + version: 1 + editable: false diff --git a/roles/grafana/molecule/default/datasources/test_datasource_prometheus.yaml b/roles/grafana/molecule/default/datasources/test_datasource_prometheus.yaml new file mode 100644 index 0000000..56ec0c6 --- /dev/null +++ b/roles/grafana/molecule/default/datasources/test_datasource_prometheus.yaml @@ -0,0 +1,50 @@ +# config file version +apiVersion: 1 + +# list of datasources that should be deleted from the database +# deleteDatasources: +# - name: Prometheus +# orgId: 1 + +# list of datasources to insert/update depending +# whats available in the database +datasources: + # name of the datasource. Required + - name: Prometheus + # datasource type. Required + type: prometheus + # access mode. direct or proxy. Required + access: proxy + # org id. will default to orgId 1 if not specified + orgId: 1 + # url + url: http://localhost:9090 + # database password, if used + password: + # database user, if used + user: + # database name, if used + database: + # enable/disable basic auth + basicAuth: false + # basic auth username, if used + basicAuthUser: + # basic auth password, if used + basicAuthPassword: + # enable/disable with credentials headers + withCredentials: + # mark as default datasource. Max one per org + isDefault: true + # fields that will be converted to json and stored in json_data + jsonData: + graphiteVersion: "1.1" + tlsAuth: false + tlsAuthWithCACert: false + # json object of data that will be encrypted. + secureJsonData: + tlsCACert: "..." + tlsClientCert: "..." + tlsClientKey: "..." + version: 1 + # allow users to edit datasources from the UI. + editable: true diff --git a/roles/grafana/molecule/default/molecule.yml b/roles/grafana/molecule/default/molecule.yml new file mode 100644 index 0000000..1e73d79 --- /dev/null +++ b/roles/grafana/molecule/default/molecule.yml @@ -0,0 +1,29 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + published_ports: + - 127.0.0.1:3001:3000 + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/grafana/molecule/default/secrets/admin_cred.yml b/roles/grafana/molecule/default/secrets/admin_cred.yml new file mode 100644 index 0000000..7d48371 --- /dev/null +++ b/roles/grafana/molecule/default/secrets/admin_cred.yml @@ -0,0 +1,3 @@ +grafana: + admin_api_username: "admin" + admin_api_password: "changme23" diff --git a/roles/grafana/molecule/default/verify.yml b/roles/grafana/molecule/default/verify.yml new file mode 100644 index 0000000..fd0a907 --- /dev/null +++ b/roles/grafana/molecule/default/verify.yml @@ -0,0 +1,41 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + vars_files: + - admin_cred.yml + + tasks: + + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + - name: "Check if Grafana is installed" + changed_when: false + ansible.builtin.command: "grafana-server -v" + register: grafana_installed_version + + - name: "Check Grafana version" + ansible.builtin.assert: + that: "grafana_installed_version.stdout is regex('{{ grafana_version }}')" + success_msg: "grafana version {{ grafana_version }} is installed and working" + fail_msg: "grafana version {{ grafana_version }} is not installed or not working correctly" + + # kics-scan ignore-block + - name: "Check if Grafana login page is reachable" + ansible.builtin.uri: + url: "http://localhost:{{ grafana_port }}/api/health" + return_content: true + status_code: 200 + method: GET + body_format: json + register: grafana_health + + - name: "Debug Grafana health status" + ansible.builtin.assert: + that: grafana_health.json.database == "ok" + success_msg: "Grafana is healthy" + fail_msg: "Grafana is not healthy" diff --git a/roles/grafana/tasks/dashboards.yml b/roles/grafana/tasks/dashboards.yml new file mode 100644 index 0000000..79c0604 --- /dev/null +++ b/roles/grafana/tasks/dashboards.yml @@ -0,0 +1,40 @@ +--- +- name: "Create dashboard directory" + ansible.builtin.file: + path: "{{ grafana_dashboard_dir }}" + state: directory + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0755" + +- name: "Copy dashboard files from source to target" + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ grafana_dashboard_dir }}/{{ item | basename }}" + mode: "0644" + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + with_fileglob: + - "{{ dashboard_source_path }}/*.json" + +- name: "Import Grafana dashboards to Grafana" + community.grafana.grafana_dashboard: + grafana_url: "http://127.0.0.1:{{ grafana_port }}" + url_username: "{{ admin_api_username }}" + url_password: "{{ admin_api_password }}" + state: present + commit_message: Updated by ansible + overwrite: false + path: "{{ grafana_dashboard_dir }}/{{ item | basename }}" + with_fileglob: + - "{{ dashboard_source_path }}/*.json" + +- name: "Run | reload Grafana provisioned dashboard configurations" + ansible.builtin.uri: + # kics-scan ignore-line + url: "http://127.0.0.1:{{ grafana_port }}/api/admin/provisioning/dashboards/reload" + method: POST + force_basic_auth: true + user: "{{ admin_api_username }}" + password: "{{ admin_api_password }}" + status_code: 200 diff --git a/roles/grafana/tasks/datasources.yml b/roles/grafana/tasks/datasources.yml new file mode 100644 index 0000000..94025c0 --- /dev/null +++ b/roles/grafana/tasks/datasources.yml @@ -0,0 +1,28 @@ +--- +- name: "Create datasource directory" + ansible.builtin.file: + path: "{{ grafana_datasource_dir }}" + state: directory + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0755" + +- name: "Configure | provision datasources for Grafana" + ansible.builtin.copy: + src: "{{ datasource_source_path }}/{{ item | basename }}" + dest: "{{ grafana_datasource_dir }}/{{ item | basename }}" + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0660" + with_fileglob: + "{{ datasource_source_path }}/*.y*ml" + +- name: "Run | reload Grafana datasource provisioned configurations" + ansible.builtin.uri: + # kics-scan ignore-line + url: "http://127.0.0.1:{{ grafana_port }}/api/admin/provisioning/datasources/reload" + method: POST + force_basic_auth: true + user: "{{ admin_api_username }}" + password: "{{ admin_api_password }}" + status_code: 200 diff --git a/roles/grafana/tasks/import_pub_dashboard.yml b/roles/grafana/tasks/import_pub_dashboard.yml new file mode 100644 index 0000000..13aead0 --- /dev/null +++ b/roles/grafana/tasks/import_pub_dashboard.yml @@ -0,0 +1,11 @@ +--- +- name: "Import public dashboard - '{{ public_dashboard.name }}''" + community.grafana.grafana_dashboard: + grafana_url: "http://127.0.0.1:{{ grafana_port }}" + state: "{{ public_dashboard.state | default('present') }}" + overwrite: false + dashboard_id: "{{ public_dashboard.id }}" + dashboard_revision: "{{ public_dashboard.revision }}" + commit_message: "Add public dashboard '{{ public_dashboard.name }}''" + url_username: "{{ admin_api_username }}" + url_password: "{{ admin_api_password }}" diff --git a/roles/grafana/tasks/install.yml b/roles/grafana/tasks/install.yml new file mode 100644 index 0000000..0f7d33d --- /dev/null +++ b/roles/grafana/tasks/install.yml @@ -0,0 +1,78 @@ +--- +- name: "Create Grafana system group" + ansible.builtin.group: + name: grafana + system: true + state: present + +- name: "Create Grafana system user" + ansible.builtin.user: + name: grafana + group: grafana + system: true + shell: "/sbin/nologin" + create_home: false + state: present + +- name: "Install Grafana deb package" + block: + - name: "Check Grafana version" + changed_when: false + ansible.builtin.command: + cmd: "grafana-server --version" + register: grafana_ver + + - name: "Assert version correctness" + ansible.builtin.assert: + that: "grafana_ver.stdout is regex('{{ grafana_version }}')" + success_msg: "grafana version {{ grafana_version }} is installed and working" + fail_msg: "grafana version {{ grafana_version }} is not installed or not working correctly" + + rescue: + - name: "Ensure that directories exist" + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: '0775' + with_items: + - "{{ grafana_log_dir }}" + - "{{ grafana_data_dir }}" + - "{{ grafana_plugins_dir }}" + + - name: "Download Grafana DEB package" + ansible.builtin.get_url: + url: "https://dl.grafana.com/oss/release/grafana_{{ grafana_version }}_amd64.deb" + dest: "/tmp/grafana-{{ grafana_version }}_amd64.deb" + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0644" + register: grafana_package_tmp + + - name: "Install DEB package" + notify: "(Re)start and enable Grafana" + ansible.builtin.apt: + deb: "{{ grafana_package_tmp.dest }}" + state: present + update_cache: true + + - name: "Cleanup downloaded file" + ansible.builtin.file: + path: "/tmp/grafana-{{ grafana_version }}_amd64.deb" + state: absent + + - name: "Create env file for systemd service unit" + notify: "(Re)start and enable Grafana" + ansible.builtin.template: + src: grafana-server.env.j2 + dest: "{{ item }}" + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0660" + with_items: + - "/etc/default/grafana-server" + - "/etc/default/grafana" + +- name: "Flush handlers" + ansible.builtin.meta: "flush_handlers" diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml new file mode 100644 index 0000000..75cba65 --- /dev/null +++ b/roles/grafana/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: "Include grafana installation tasks" + ansible.builtin.include_tasks: install.yml + +- name: "Wait for the Grafana server to become available" + ansible.builtin.wait_for: + host: "127.0.0.1" + port: "{{ grafana_port }}" + state: started + delay: 10 + +- name: "Include user creation tasks" + when: users is defined + loop: "{{ users }}" + loop_control: + loop_var: user + ansible.builtin.include_tasks: user.yml + +- name: "Configure custom dashboards" + when: dashboard_source_path is defined + ansible.builtin.include_tasks: dashboards.yml + +- name: "Configure public dashboards" + when: public_dashboards is defined + loop: "{{ public_dashboards }}" + loop_control: + loop_var: public_dashboard + ansible.builtin.include_tasks: import_pub_dashboard.yml + +- name: "Configure plugins" + when: plugins is defined + loop: "{{ plugins }}" + loop_control: + loop_var: plugin + ansible.builtin.include_tasks: plugins.yml + +- name: "Configure datasources" + when: datasource_source_path is defined + ansible.builtin.include_tasks: datasources.yml diff --git a/roles/grafana/tasks/plugins.yml b/roles/grafana/tasks/plugins.yml new file mode 100644 index 0000000..96af00c --- /dev/null +++ b/roles/grafana/tasks/plugins.yml @@ -0,0 +1,16 @@ +--- +- name: "Create plugin directory" + ansible.builtin.file: + path: "{{ grafana_plugins_dir }}" + state: directory + owner: "{{ grafana_user }}" + group: "{{ grafana_group }}" + mode: "0755" + +- name: "Install Grafana plugins" + community.grafana.grafana_plugin: + name: "{{ plugin.name }}" + version: "{{ plugin.version }}" + grafana_plugins_dir: "{{ grafana_plugins_dir }}" + state: "{{ plugin.state | default('present') }}" + notify: "(Re)start and enable Grafana" diff --git a/roles/grafana/tasks/user.yml b/roles/grafana/tasks/user.yml new file mode 100644 index 0000000..1998fc9 --- /dev/null +++ b/roles/grafana/tasks/user.yml @@ -0,0 +1,19 @@ +--- + +- name: "Reset default admin password" + ansible.builtin.command: > + grafana-cli admin reset-admin-password "{{ admin_api_password }}" + no_log: true + changed_when: false + +- name: "Create | update a Grafana user" + community.grafana.grafana_user: + url: "http://127.0.0.1:{{ grafana_port }}" + url_username: "{{ admin_api_username }}" + url_password: "{{ admin_api_password }}" + name: "{{ user.name }}" + email: "{{ user.user_email }}" + login: "{{ user.user_login }}" + password: "{{ user.user_password }}" + is_admin: "{{ user.is_admin | default(false) }}" + state: present diff --git a/roles/grafana/templates/grafana-server.env.j2 b/roles/grafana/templates/grafana-server.env.j2 new file mode 100644 index 0000000..0abd646 --- /dev/null +++ b/roles/grafana/templates/grafana-server.env.j2 @@ -0,0 +1,24 @@ +GRAFANA_USER=grafana + +GRAFANA_GROUP=grafana + +GRAFANA_HOME=/usr/share/grafana + +LOG_DIR={{ grafana_log_dir }} + +DATA_DIR={{ grafana_data_dir }} + +MAX_OPEN_FILES=10000 + +CONF_DIR=/etc/grafana + +CONF_FILE=/etc/grafana/grafana.ini + +RESTART_ON_UPGRADE=true + +PLUGINS_DIR={{ grafana_plugins_dir }} + +PROVISIONING_CFG_DIR=/etc/grafana/provisioning + +# Only used on systemd systems +PID_FILE_DIR=/run/grafana \ No newline at end of file diff --git a/roles/grafana/vars/main.yml b/roles/grafana/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/grafana/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/ipmi_exporter/README.md b/roles/ipmi_exporter/README.md new file mode 100644 index 0000000..06d3620 --- /dev/null +++ b/roles/ipmi_exporter/README.md @@ -0,0 +1,61 @@ +genlab.ipmi_exporter +========= + +This Ansible role installs ipmi_exporter on target host. This is a Prometheus exporter for Intelligent Platform Management Interface [metrics](https://github.com/prometheus-community/ipmi_exporter/blob/master/docs/metrics.md) + +Requirements +------------ + +By default, the exporter relies on tools from the FreeIPMI suite for the actual IPMI implementation. + +Role Variables +-------------- +Configuration files must have names ```web_conf.yaml``` and ```ipmi_local.conf```. If ipmi_exp_source_dir is specified, the role searches for ```web_conf.yaml``` and ```ipmi_local.conf``` in that directory and copy to target host in ```ipmi_exp_config_dir```. If the source directory is not specified, the role skips this step. In ipmi_local.conf user can describe what modules to use for metric collection. + +ipmi_up{collector=""} is 1 if the data for this collector could successfully be retrieved from the remote host, 0 otherwise. The following collectors are available and can be enabled or disabled in the config: +- ipmi: collects IPMI sensor data. If it fails, sensor metrics (see below) will not be available +- dcmi: collects DCMI data, currently only power consumption. If it fails, power consumption metrics (see below) will not be available +- bmc: collects BMC details. If it fails, BMC info metrics (see below) will not be available +- bmc-watchdog: collects status of the watchdog. If it fails, BMC watchdog metrics (see below) will not be available +- chassis: collects the current chassis power state (on/off). If it fails, the chassis power state metric (see below) will not be available +- sel: collects system event log (SEL) details. If it fails, SEL metrics (see below) will not be available +- sel-events: collects metrics for user-defined events in system event log (SEL). If it fails, SEL entries metrics (see below) will not be available +- sm-lan-mode: collects the "LAN mode" setting in the current BMC config. If it fails, the LAN mode metric (see below) will not be available + +```yaml +ipmi_exp_version: 1.10.1 +ipmi_exp_dir: "/etc/exporters" +ipmi_exp_config_dir: "/etc/exporters/config" +ipmi_exp_args: "" # --[no-]native-ipmi Use native IPMI implementation instead of FreeIPMI (EXPERIMENTAL) + # --[no-]web.systemd-socket Use systemd socket activation listeners instead of port listeners (Linux only). +ipmi_exp_log_level: "info" # Only log messages with the given severity or above. One of: [debug, info, warn, error] +ipmi_exp_log_format: "logfmt" # Output format of log messages. One of: [logfmt, json] +ipmi_exp_web_listen_address: "localhost:9290" # Addresses on which to expose metrics and web interface. Repeatable for multiple addresses. Examples: `:9100` or `[::1]:9100` for http, vsock://:9100` for vsock +ipmi_exp_source_dir: ipmi_local.conf # Path to configuration file. See: https://github.com/prometheus-community/ipmi_exporter/blob/master/docs/configuration.md +ipmi_exp_web_source_dir: web_conf.yaml # Path to configuration file that can enable TLS or authentication. See: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md +``` + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml + roles: + - role: genlab.ipmi_exporter + ipmi_exp_version: "1.10.1" + ipmi_exp_source_dir: "molecule/default/" +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/ipmi_exporter/defaults/main.yml b/roles/ipmi_exporter/defaults/main.yml new file mode 100644 index 0000000..c67b875 --- /dev/null +++ b/roles/ipmi_exporter/defaults/main.yml @@ -0,0 +1,8 @@ +--- +ipmi_exporter_version: 1.10.1 +ipmi_exporter_dir: "/etc/exporters" +ipmi_exporter_config_dir: "/etc/exporters/config" +ipmi_exporter_log_level: "info" +ipmi_exporter_log_format: "logfmt" +ipmi_exporter_web_listen_address: "localhost:9290" +ipmi_exporter_args: "" diff --git a/roles/ipmi_exporter/handlers/main.yml b/roles/ipmi_exporter/handlers/main.yml new file mode 100644 index 0000000..57f5d52 --- /dev/null +++ b/roles/ipmi_exporter/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "(Re)start and enable ipmi_exporter" + ansible.builtin.systemd_service: + name: ipmi_exporter.service + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/ipmi_exporter/meta/main.yml b/roles/ipmi_exporter/meta/main.yml new file mode 100644 index 0000000..3c37e0c --- /dev/null +++ b/roles/ipmi_exporter/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "ipmi_exporter" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/ipmi_exporter/molecule/default/converge.yml b/roles/ipmi_exporter/molecule/default/converge.yml new file mode 100644 index 0000000..40122c5 --- /dev/null +++ b/roles/ipmi_exporter/molecule/default/converge.yml @@ -0,0 +1,6 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.ipmi_exporter + ipmi_exp_source_dir: "molecule/default/" diff --git a/roles/ipmi_exporter/molecule/default/ipmi_local.conf b/roles/ipmi_exporter/molecule/default/ipmi_local.conf new file mode 100644 index 0000000..db488bf --- /dev/null +++ b/roles/ipmi_exporter/molecule/default/ipmi_local.conf @@ -0,0 +1,12 @@ +modules: + default: + # Available collectors are bmc, bmc-watchdog, ipmi, chassis, dcmi, sel, sel-events and sm-lan-mode + collectors: + - bmc + - bmc-watchdog + - ipmi + - dcmi + - chassis + - sel + - sel-events + - sm-lan-mode \ No newline at end of file diff --git a/roles/ipmi_exporter/molecule/default/molecule.yml b/roles/ipmi_exporter/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/ipmi_exporter/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/ipmi_exporter/molecule/default/verify.yml b/roles/ipmi_exporter/molecule/default/verify.yml new file mode 100644 index 0000000..95aeb62 --- /dev/null +++ b/roles/ipmi_exporter/molecule/default/verify.yml @@ -0,0 +1,37 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + tasks: + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + - name: "Check if ipmi_exporter is installed" + changed_when: false + ansible.builtin.command: "ipmi_exporter --version" + register: ipmi_exporter_installed_version + + - name: "Check ipmi_exporter version" + ansible.builtin.assert: + that: "ipmi_exporter_installed_version.stdout is regex('{{ ipmi_exporter_version }}')" + success_msg: "ipmi_exporter version {{ ipmi_exporter_version }} is installed and working" + fail_msg: "ipmi_exporter version {{ ipmi_exporter_version }} is not installed or not working correctly" + + # kics-scan ignore-block + - name: Check if /metrics endpoint is reachable + ansible.builtin.uri: + url: "http://{{ ipmi_exporter_web_listen_address }}/metrics" + return_content: true + status_code: 200 + timeout: 60 + register: ipmi_exporter_metrics_check + + - name: "Fail if /metrics doesn't contain ipmi_exporter_build_info line" + ansible.builtin.assert: + that: "'ipmi_exporter_build_info' in ipmi_exporter_metrics_check.content" + fail_msg: "ipmi_exporter /metrics endpoint doesn't contain ipmi_exporter_build_info line!" + success_msg: "ipmi_exporter /metrics endpoint contains ipmi_exporter_build_info line!" diff --git a/roles/ipmi_exporter/tasks/configure.yml b/roles/ipmi_exporter/tasks/configure.yml new file mode 100644 index 0000000..04af8f5 --- /dev/null +++ b/roles/ipmi_exporter/tasks/configure.yml @@ -0,0 +1,20 @@ +--- +- name: "Upload ipmi_exporter local configure file" + notify: "(Re)start and enable ipmi_exporter" + when: ipmi_exp_source_dir is defined and ipmi_exp_source_dir | length > 0 + ansible.builtin.template: + src: "{{ ipmi_exp_source_dir }}/ipmi_local.conf" + dest: "{{ ipmi_exporter_config_dir }}/ipmi_local.conf" + owner: root + group: root + mode: '0640' + +- name: "Upload ipmi_exporter web configure file" + notify: "(Re)start and enable ipmi_exporter" + when: ipmi_exp_web_source_dir is defined and ipmi_exp_web_source_dir | length > 0 + ansible.builtin.template: + src: "{{ ipmi_exp_web_source_dir }}/web_conf.yaml" + dest: "{{ ipmi_exporter_config_dir }}/web_conf.yaml" + owner: root + group: root + mode: '0640' diff --git a/roles/ipmi_exporter/tasks/install.yml b/roles/ipmi_exporter/tasks/install.yml new file mode 100644 index 0000000..4d9ec03 --- /dev/null +++ b/roles/ipmi_exporter/tasks/install.yml @@ -0,0 +1,51 @@ +--- +- name: "Install ipmi_exporter from binary" + block: + - name: "Check ipmi_exporter version" + changed_when: false + ansible.builtin.command: + cmd: "ipmi_exporter --version" + register: ipmi_exporter_ver + + - name: "Assert version correctness" + ansible.builtin.assert: + that: "ipmi_exporter_ver.stdout is regex('{{ ipmi_exporter_version }}')" + success_msg: "ipmi_exporter version {{ ipmi_exporter_version }} is installed and working" + fail_msg: "ipmi_exporter version {{ ipmi_exporter_version }} is not installed or not working correctly" + + rescue: + - name: "Create ipmi_exporter directories" + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + with_items: + - "{{ ipmi_exporter_dir }}" + - "{{ ipmi_exporter_config_dir }}" + + - name: "Unarchive ipmi_exporter tar file" + notify: "(Re)start and enable ipmi_exporter" + ansible.builtin.unarchive: + src: "https://github.com/prometheus-community/ipmi_exporter/releases/\ + download/v{{ ipmi_exporter_version }}/ipmi_exporter-{{ ipmi_exporter_version }}.linux-amd64.tar.gz" + dest: "{{ ipmi_exporter_dir }}" + remote_src: true + + - name: "Move ipmi_exporter binary" + ansible.builtin.copy: + src: "{{ ipmi_exporter_dir }}/ipmi_exporter-{{ ipmi_exporter_version }}.linux-amd64/ipmi_exporter" + dest: "/usr/local/bin/ipmi_exporter" + mode: "0755" + owner: root + group: root + remote_src: true + +- name: Create systemd service file + ansible.builtin.template: + src: ipmi_exporter.service.j2 + dest: /etc/systemd/system/ipmi_exporter.service + owner: root + group: root + mode: '0644' diff --git a/roles/ipmi_exporter/tasks/main.yml b/roles/ipmi_exporter/tasks/main.yml new file mode 100644 index 0000000..b926b2c --- /dev/null +++ b/roles/ipmi_exporter/tasks/main.yml @@ -0,0 +1,7 @@ +--- + +- name: "Include installation tasks" + ansible.builtin.include_tasks: "install.yml" + +- name: "Including configuration tasks" + ansible.builtin.include_tasks: "configure.yml" diff --git a/roles/ipmi_exporter/templates/ipmi_exporter.service.j2 b/roles/ipmi_exporter/templates/ipmi_exporter.service.j2 new file mode 100644 index 0000000..6b41d18 --- /dev/null +++ b/roles/ipmi_exporter/templates/ipmi_exporter.service.j2 @@ -0,0 +1,38 @@ +[Unit] +Description=IPMI exporter +Documentation=https://github.com/prometheus-community/ipmi_exporter +After=network.target +StartLimitIntervalSec=120 +StartLimitBurst=5 + +[Service] +Type=simple +ExecStart=/usr/local/bin/ipmi_exporter \ + --web.listen-address={{ ipmi_exporter_web_listen_address }} \ + --log.level={{ ipmi_exporter_log_level }} \ + --log.format={{ ipmi_exporter_log_format }} \ +{% if ipmi_exporter_args is defined and ipmi_exporter_args | length > 0 %} + {{ ipmi_exporter_args }} \ +{% endif %} +{% if ipmi_exporter_source_dir is defined and ipmi_exporter_source_dir | length > 0 %} + --config.file={{ ipmi_exporter_config_dir }}/ipmi_local.conf \ +{% endif %} +{% if ipmi_exporter_web_source_dir is defined and ipmi_exporter_web_source_dir | length > 0 %} + --web.config.file={{ ipmi_exporter_config_dir }}/web_conf.yaml +{% endif %} + +SyslogIdentifier=ipmi_exporter +Restart=on-failure +RestartSec=5 + +ProtectHome=yes +NoNewPrivileges=yes +ProtectSystem=strict +ProtectControlGroups=true +ProtectKernelModules=true +ProtectKernelTunables=yes +PrivateTmp=true +ProtectSystem=full + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/roles/ipmi_exporter/vars/main.yml b/roles/ipmi_exporter/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/ipmi_exporter/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/karma/.gitignore b/roles/karma/.gitignore new file mode 100644 index 0000000..8cd0df3 --- /dev/null +++ b/roles/karma/.gitignore @@ -0,0 +1,2 @@ +.vscode +.idea \ No newline at end of file diff --git a/roles/karma/README.md b/roles/karma/README.md new file mode 100644 index 0000000..3282de9 --- /dev/null +++ b/roles/karma/README.md @@ -0,0 +1,53 @@ +genlab.karma +========= +This is the ansible role to install and configure Karma - alert dashboard for Prometheus Alertmanager (https://github.com/prymitive/karma) + +------------ +⚠️ Do not forget to update: + +- `meta/main.yml` +- Conda/Mamba manifests +- this README =) including *the name at the top* and *maintainers*. + +Requirements +------------ + +None + +Role Variables +-------------- +```yaml +karma_version: "0.121" - karma version +karma_user: karma - name of karma system user +karma_group: karma - name of karma system group +karma_dir: /etc/karma - path where to unpack karma +karma_config_dir: /etc/karma/conf - where to copy configuration file +config_source_dir: source - path to source dir with karma config on localhost +``` + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.karma + karma_version: "0.121" + config_source_dir: "karma/" + karma_dir: "/etc/karma" + karma_config_dir: "/etc/karma/config" +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/karma/defaults/main.yml b/roles/karma/defaults/main.yml new file mode 100644 index 0000000..f6c4844 --- /dev/null +++ b/roles/karma/defaults/main.yml @@ -0,0 +1,7 @@ +--- +# Default variables for Karma role +karma_version: "0.121" +karma_user: karma +karma_group: karma +karma_dir: /etc/karma +karma_config_dir: /etc/karma/conf diff --git a/roles/karma/handlers/main.yml b/roles/karma/handlers/main.yml new file mode 100644 index 0000000..d7748d6 --- /dev/null +++ b/roles/karma/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "(Re)start and enable karma" + ansible.builtin.systemd_service: + name: karma.service + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/karma/meta/main.yml b/roles/karma/meta/main.yml new file mode 100644 index 0000000..87eb400 --- /dev/null +++ b/roles/karma/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "karma" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [monitoring] + +dependencies: [] diff --git a/roles/karma/molecule/default/converge.yml b/roles/karma/molecule/default/converge.yml new file mode 100644 index 0000000..6f4bd54 --- /dev/null +++ b/roles/karma/molecule/default/converge.yml @@ -0,0 +1,9 @@ +--- +- name: Converge + hosts: all + gather_facts: true + become: true + roles: + - role: genlab.common.karma + karma_version: "0.121" + config_source_dir: molecule/default/ diff --git a/roles/karma/molecule/default/karma.conf b/roles/karma/molecule/default/karma.conf new file mode 100644 index 0000000..8956068 --- /dev/null +++ b/roles/karma/molecule/default/karma.conf @@ -0,0 +1,17 @@ +alertmanager: + interval: 1m + servers: + - name: production + uri: http://localhost:9093 + timeout: 20s + proxy: false + readonly: true +listen: + address: "0.0.0.0" + port: 8080 + prefix: / + tls: + cert: "" + key: "" + cors: + allowedOrigins: [] \ No newline at end of file diff --git a/roles/karma/molecule/default/molecule.yml b/roles/karma/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/karma/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/karma/molecule/default/verify.yml b/roles/karma/molecule/default/verify.yml new file mode 100644 index 0000000..d38c99c --- /dev/null +++ b/roles/karma/molecule/default/verify.yml @@ -0,0 +1,35 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + vars: + karma_version: "0.121" + + tasks: + - name: "Check if Karma is installed" + changed_when: false + ansible.builtin.command: "karma --version" + register: karma_installed_version + + - name: "Check Karma version" + ansible.builtin.assert: + that: "karma_installed_version.stdout is regex('{{ karma_version }}')" + success_msg: "Karma version {{ karma_version }} is installed and working" + fail_msg: "Karma version {{ karma_version }} is not installed or not working correctly" + + # kics-scan ignore-block + - name: "Check if Karma is reachable" + ansible.builtin.uri: + url: "http://localhost:8080/health" + return_content: true + status_code: 200 + method: GET + body_format: json + register: karma_health + + - name: "Assert Karma health status" + ansible.builtin.assert: + that: "karma_health.content.strip() == 'Pong'" + success_msg: "Karma is healthy" + fail_msg: "Karma is not healthy" diff --git a/roles/karma/tasks/configuration.yml b/roles/karma/tasks/configuration.yml new file mode 100644 index 0000000..e0a7400 --- /dev/null +++ b/roles/karma/tasks/configuration.yml @@ -0,0 +1,10 @@ +--- + +- name: Create karma configuration file + notify: "(Re)start and enable karma" + ansible.builtin.template: + src: "{{ config_source_dir }}/karma.conf" + dest: "{{ karma_config_dir }}/karma.conf" + owner: "{{ karma_user }}" + group: "{{ karma_group }}" + mode: '0640' diff --git a/roles/karma/tasks/install.yml b/roles/karma/tasks/install.yml new file mode 100644 index 0000000..3cc4e4c --- /dev/null +++ b/roles/karma/tasks/install.yml @@ -0,0 +1,66 @@ +--- + +- name: "Create Karma system group" + ansible.builtin.group: + name: "{{ karma_user }}" + system: true + state: present + +- name: "Create karma system user" + ansible.builtin.user: + name: "{{ karma_user }}" + group: "{{ karma_group }}" + system: true + shell: "/sbin/nologin" + create_home: false + state: present + +- name: "Install karma from binary" + block: + - name: "Check karma version" + changed_when: false + ansible.builtin.command: + cmd: "{{ karma_dir }}/karma-linux-amd64 --version" + register: karma_ver + + - name: "Assert version correctness" + ansible.builtin.assert: + that: "karma_ver.stdout is regex('{{ karma_version }}')" + success_msg: "karma version {{ karma_version }} is installed and working" + fail_msg: "karma version {{ karma_version }} is not installed or not working correctly" + + rescue: + - name: "Create karma directories" + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ karma_user }}" + group: "{{ karma_group }}" + mode: "0755" + with_items: + - "{{ karma_dir }}" + - "{{ karma_config_dir }}" + + - name: "Unarchive karma tar file" + notify: "(Re)start and enable karma" + ansible.builtin.unarchive: + src: "https://github.com/prymitive/karma/releases/download/v{{ karma_version }}/karma-linux-amd64.tar.gz" + dest: "{{ karma_dir }}" + remote_src: true + + - name: "Move karma binary" + ansible.builtin.copy: + src: "{{ karma_dir }}/karma-linux-amd64" + dest: "/usr/local/bin/karma" + mode: "0755" + owner: "{{ karma_user }}" + group: "{{ karma_group }}" + remote_src: true + +- name: Create systemd service file + ansible.builtin.template: + src: karma.service.j2 + dest: /etc/systemd/system/karma.service + owner: "{{ karma_user }}" + group: "{{ karma_group }}" + mode: '0644' diff --git a/roles/karma/tasks/main.yml b/roles/karma/tasks/main.yml new file mode 100644 index 0000000..6bd62b2 --- /dev/null +++ b/roles/karma/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: "Run installation tasks" + ansible.builtin.include_tasks: install.yml + +- name: "Run configuration tasks" + ansible.builtin.include_tasks: configuration.yml + +- name: "Flush handlers" + ansible.builtin.meta: "flush_handlers" diff --git a/roles/karma/templates/karma.service.j2 b/roles/karma/templates/karma.service.j2 new file mode 100644 index 0000000..312fac8 --- /dev/null +++ b/roles/karma/templates/karma.service.j2 @@ -0,0 +1,26 @@ +[Unit] +Description=Karma - Alertmanager dashboard +After=network.target +Documentation=https://github.com/prymitive/karma + +[Service] +Type=simple +User={{ karma_user }} +Group={{ karma_group }} +ExecStart=karma --config.file={{ karma_config_dir }}/karma.conf +Restart=always +RestartSec=5 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=karma +ProtectSystem=strict +NoNewPrivileges=true +PrivateTmp=true +ProtectKernelModules=true +ProtectControlGroups=true +ProtectKernelTunables=true +ProtectClock=yes +RestrictSUIDSGID=true + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/roles/karma/vars/main.yml b/roles/karma/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/karma/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/loki/README.md b/roles/loki/README.md new file mode 100644 index 0000000..8a05ac1 --- /dev/null +++ b/roles/loki/README.md @@ -0,0 +1,43 @@ +loki +========= + +Installs Loki as a `systemd` service. + +Requirements +------------ + +Role Variables +-------------- + +```yaml +loki_version: Loki version to be deployed +loki_storage_path: where to put Loki's files, including log data and positions (default: /data/loki) +loki_port: Loki will listen on this port (default: 3000) +``` + +Dependencies +------------ + +No + +Example Playbook +---------------- + +```yaml +roles: + - role: loki + loki_version: 2.7.3 +``` + + +License +------- + +MIT + +Author Information +------------------ + +Alexander Gorelyshev (corvus-migratorius@proton.me) and Danila Danilkin + +Genlab LLC diff --git a/roles/loki/defaults/main.yml b/roles/loki/defaults/main.yml new file mode 100644 index 0000000..08a7293 --- /dev/null +++ b/roles/loki/defaults/main.yml @@ -0,0 +1,4 @@ +--- +loki_version: 3.4.2 +loki_storage_path: /data/loki +loki_port: 3100 diff --git a/roles/loki/handlers/main.yml b/roles/loki/handlers/main.yml new file mode 100644 index 0000000..e6d7f41 --- /dev/null +++ b/roles/loki/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: "Restart the Loki daemon" + ansible.builtin.systemd: + name: loki + state: restarted + + +- name: "Reload the Loki daemon configuration" + ansible.builtin.systemd: + daemon_reload: true diff --git a/roles/loki/meta/main.yml b/roles/loki/meta/main.yml new file mode 100644 index 0000000..2ca0728 --- /dev/null +++ b/roles/loki/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: loki + namespace: genlab + author: "Danila Danilkin" + company: "Genlab, LLC" + description: "Deploy Loki as a systemd service" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/loki/molecule/default/converge.yml b/roles/loki/molecule/default/converge.yml new file mode 100644 index 0000000..b95fe26 --- /dev/null +++ b/roles/loki/molecule/default/converge.yml @@ -0,0 +1,5 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.loki diff --git a/roles/loki/molecule/default/molecule.yml b/roles/loki/molecule/default/molecule.yml new file mode 100644 index 0000000..de54042 --- /dev/null +++ b/roles/loki/molecule/default/molecule.yml @@ -0,0 +1,22 @@ +--- +dependency: + name: galaxy +driver: + name: docker +platforms: + - name: ubuntu + image: geerlingguy/docker-ubuntu2204-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true +provisioner: + name: ansible +verifier: + name: ansible +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/loki/molecule/default/verify.yml b/roles/loki/molecule/default/verify.yml new file mode 100644 index 0000000..c18de88 --- /dev/null +++ b/roles/loki/molecule/default/verify.yml @@ -0,0 +1,34 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + tasks: + # https://github.com/ansible/molecule/issues/3587#issuecomment-1158650179 + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + - name: "Sanity check the Loki daemon" + retries: 5 + delay: 1 + register: loki_this + failed_when: "loki_version not in loki_this.content" + ansible.builtin.uri: + # kics-scan ignore-line - kics wants https on localhost but i dont + url: "http://localhost:{{ loki_port }}/metrics" + return_content: true + + - name: "Ensure that locgli is installed and able to run" + register: loki_logcli + failed_when: loki_logcli.rc != 0 + changed_when: false + ansible.builtin.command: + cmd: logcli --version + + - name: "Check logcli version" + ansible.builtin.assert: + that: "'version {{ loki_version }}' in loki_logcli.stderr" + success_msg: "logcli version {{ loki_version }} is installed and working" + fail_msg: "logcli version {{ loki_version }} is not installed or not working correctly" diff --git a/roles/loki/tasks/main.yml b/roles/loki/tasks/main.yml new file mode 100644 index 0000000..266744e --- /dev/null +++ b/roles/loki/tasks/main.yml @@ -0,0 +1,101 @@ +--- +- name: "Install unzip package" + ansible.builtin.apt: + name: unzip + state: present + cache_valid_time: 3600 + + +- name: "Create Loki directories" + with_items: + - {path: '/opt/loki', mode: '0755'} + - {path: '/etc/loki', mode: '0644'} + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + owner: root + group: root + mode: "{{ item.mode }}" + + +- name: "Template Loki config file" + notify: "Restart the Loki daemon" + ansible.builtin.template: + src: "loki.yml.j2" + dest: "/etc/loki/loki.yml" + owner: root + group: root + mode: "0644" + + +- name: "Install requested Loki version" + block: + - name: "Check Loki version" + ansible.builtin.command: /opt/loki/loki -version + register: loki_version_check + changed_when: false + + - name: "Assert version correctness" + notify: "Restart the Loki daemon" + ansible.builtin.assert: + that: "loki_version in loki_version_check.stdout" + success_msg: "Expected Loki version available ({{ loki_version }})" + fail_msg: "Expected version '{{ loki_version }}'; available is '{{ loki_version_check.stdout }}'" + + rescue: + - name: "Install Loki if it is not present ({{ loki_version }})" + ansible.builtin.unarchive: + src: "https://github.com/grafana/loki/releases/download/v\ + {{ loki_version }}/loki-linux-amd64.zip" + dest: /opt/loki/ + remote_src: true + + - name: "Rename and set permissions for the Loki binary" + ansible.builtin.copy: + src: "/opt/loki/loki-linux-amd64" + dest: /opt/loki/loki + owner: root + group: root + mode: "0755" + remote_src: true + + - name: "Cleanup downloaded file" + ansible.builtin.file: + path: /opt/loki/loki-linux-amd64 + state: absent + + - name: "Install logcli" + ansible.builtin.unarchive: + src: "https://github.com/grafana/loki/releases/download/v\ + {{ loki_version }}/logcli-linux-amd64.zip" + dest: /usr/local/bin + remote_src: true + + - name: "Rename and set permissions for the Logcli binary" + ansible.builtin.copy: + src: "/usr/local/bin/logcli-linux-amd64" + dest: "/usr/local/bin/logcli" + owner: root + group: root + mode: "0755" + remote_src: true + + - name: "Cleanup downloaded file" + ansible.builtin.file: + path: /usr/local/bin/logcli-linux-amd64 + state: absent + +- name: "Template the systemd unit file" + notify: "Reload the Loki daemon configuration" + ansible.builtin.template: + src: templates/loki.service.j2 + dest: /etc/systemd/system/loki.service + owner: root + group: root + mode: "0755" + +- name: "Start and enable the Loki daemon" + ansible.builtin.systemd: + name: loki + state: started + enabled: true diff --git a/roles/loki/templates/loki.service.j2 b/roles/loki/templates/loki.service.j2 new file mode 100644 index 0000000..6cc23bc --- /dev/null +++ b/roles/loki/templates/loki.service.j2 @@ -0,0 +1,11 @@ +[Unit] +Description=Loki service +After=network.target + +[Service] +Type=simple +User=root +ExecStart=/opt/loki/loki -config.file /etc/loki/loki.yml + +[Install] +WantedBy=multi-user.target diff --git a/roles/loki/templates/loki.yml.j2 b/roles/loki/templates/loki.yml.j2 new file mode 100644 index 0000000..8236f15 --- /dev/null +++ b/roles/loki/templates/loki.yml.j2 @@ -0,0 +1,83 @@ +auth_enabled: false + +server: + http_listen_port: {{ loki_port }} + # grpc_listen_port: 9096 + # log_level: debug + # grpc_server_max_concurrent_streams: 1000 + +common: + instance_addr: 127.0.0.1 + path_prefix: {{ loki_storage_path }} + storage: + filesystem: + chunks_directory: {{ loki_storage_path }}/chunks + rules_directory: {{ loki_storage_path }}/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +# ingester: +# lifecycler: +# address: 127.0.0.1 +# ring: +# kvstore: +# store: inmemory +# replication_factor: 1 +# final_sleep: 0s +# chunk_idle_period: 5m +# chunk_retain_period: 30s +# max_transfer_retries: 0 + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +pattern_ingester: + enabled: true + metric_aggregation: + loki_address: localhost:3100 + +# storage_config: +# boltdb: +# directory: {{ loki_storage_path }}/index + +# filesystem: +# directory: {{ loki_storage_path }}/chunks + +# ruler: +# alertmanager_url: http://localhost:9093 + +query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + +frontend: + encoding: protobuf + +# querier: +# engine: +# enable_multi_variant_queries: true + +limits_config: + metric_aggregation_enabled: true + +# chunk_store_config: +# max_look_back_period: 0s + +# table_manager: +# retention_deletes_enabled: false +# retention_period: 0s + +analytics: + reporting_enabled: false diff --git a/roles/loki/templates/loki_test.yml.j2 b/roles/loki/templates/loki_test.yml.j2 new file mode 100644 index 0000000..954f214 --- /dev/null +++ b/roles/loki/templates/loki_test.yml.j2 @@ -0,0 +1,45 @@ +auth_enabled: false + +server: + http_listen_port: 3200 + +ingester: + lifecycler: + address: 127.0.0.1 + ring: + kvstore: + store: inmemory + replication_factor: 1 + final_sleep: 0s + chunk_idle_period: 5m + chunk_retain_period: 30s + max_transfer_retries: 0 + +schema_config: + configs: + - from: 2018-04-15 + store: boltdb + object_store: filesystem + schema: v11 + index: + prefix: index_ + period: 168h + +storage_config: + boltdb: + directory: /data/loki/index + + filesystem: + directory: /data/loki/chunks + +limits_config: + enforce_metric_name: false + reject_old_samples: true + reject_old_samples_max_age: 168h + +chunk_store_config: + max_look_back_period: 0s + +table_manager: + retention_deletes_enabled: false + retention_period: 0s diff --git a/roles/loki/vars/main.yml b/roles/loki/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/loki/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/nginx/.gitignore b/roles/nginx/.gitignore new file mode 100644 index 0000000..8cd0df3 --- /dev/null +++ b/roles/nginx/.gitignore @@ -0,0 +1,2 @@ +.vscode +.idea \ No newline at end of file diff --git a/roles/nginx/README.md b/roles/nginx/README.md new file mode 100644 index 0000000..9ce29f0 --- /dev/null +++ b/roles/nginx/README.md @@ -0,0 +1,37 @@ +ansible-nginx +========= + +Deploy NGINX with a minimal configuration. + +Requirements +------------ + +None + +Role Variables +-------------- + +None + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.nginx +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/nginx/defaults/main.yml b/roles/nginx/defaults/main.yml new file mode 100644 index 0000000..b9f6c1f --- /dev/null +++ b/roles/nginx/defaults/main.yml @@ -0,0 +1,2 @@ +--- +nginx_version: "1.26.3" diff --git a/roles/nginx/handlers/main.yml b/roles/nginx/handlers/main.yml new file mode 100644 index 0000000..aaf83c0 --- /dev/null +++ b/roles/nginx/handlers/main.yml @@ -0,0 +1,16 @@ +--- +- name: "Update apt cache" + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + +- name: "Start nginx" + ansible.builtin.service: + name: nginx + state: started + enabled: true + +- name: "Reload nginx" + ansible.builtin.service: + name: nginx + state: reloaded diff --git a/roles/nginx/meta/main.yml b/roles/nginx/meta/main.yml new file mode 100644 index 0000000..b6e9df6 --- /dev/null +++ b/roles/nginx/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "nginx" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/nginx/molecule/default/converge.yml b/roles/nginx/molecule/default/converge.yml new file mode 100644 index 0000000..30c3718 --- /dev/null +++ b/roles/nginx/molecule/default/converge.yml @@ -0,0 +1,36 @@ +--- +- name: Converge + hosts: all + post_tasks: + - name: "Create a test site root" + ansible.builtin.file: + path: /var/www/html + state: directory + owner: nginx + group: nginx + mode: "0755" + + - name: "Push a test index file" + ansible.builtin.copy: + src: index.html + dest: /var/www/html + owner: nginx + group: nginx + mode: "0644" + + roles: + # using default NGINX verison + - role: genlab.common.nginx + site: + confname: test-var + content: | + server { + listen 80; + server_name example.com www.example.com; + + location / { + root /var/www/html; + index index.html; + } + } + dir_sites: '{{ lookup("env", "MOLECULE_PROJECT_DIRECTORY") }}/molecule/default/site-configs/' diff --git a/roles/nginx/molecule/default/files/index.html b/roles/nginx/molecule/default/files/index.html new file mode 100644 index 0000000..19d993f --- /dev/null +++ b/roles/nginx/molecule/default/files/index.html @@ -0,0 +1 @@ +Hello from ansible-nginx! diff --git a/roles/nginx/molecule/default/molecule.yml b/roles/nginx/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/nginx/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/nginx/molecule/default/site-configs/test-dir.conf b/roles/nginx/molecule/default/site-configs/test-dir.conf new file mode 100644 index 0000000..0830bee --- /dev/null +++ b/roles/nginx/molecule/default/site-configs/test-dir.conf @@ -0,0 +1,9 @@ +server { + listen 8080; + server_name test.com; + + location / { + root /var/www/html; + index index.html; + } +} diff --git a/roles/nginx/molecule/default/verify.yml b/roles/nginx/molecule/default/verify.yml new file mode 100644 index 0000000..23748b8 --- /dev/null +++ b/roles/nginx/molecule/default/verify.yml @@ -0,0 +1,73 @@ +# kics-scan disable=2e8d4922-8362-4606-8c14-aa10466a1ce3 +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + pre_tasks: + - name: "Include default vars" + ansible.builtin.include_vars: + dir: '{{ lookup("env", "MOLECULE_PROJECT_DIRECTORY") }}/defaults/' + extensions: + - 'yml' + + tasks: + - name: "Verify NGINX version" + register: nginx_version_output + changed_when: false + failed_when: nginx_version_output.rc != 0 + ansible.builtin.command: /usr/sbin/nginx -v + + # version is displayed as e.g. 'nginx version: nginx/1.26.3' + - name: "Extract NGINX version" + ansible.builtin.set_fact: + nginx_version: "{{ nginx_version_output.stderr.split('/')[1] }}" + + - name: "Check expected NGINX version" + ansible.builtin.assert: + that: nginx_version.startswith(nginx_version) + fail_msg: "Unexpected NGINX version found: '{{ nginx_version }}'" + + - name: "Gather service facts" + ansible.builtin.service_facts: + + - name: "Assert NGINX service is running" + ansible.builtin.assert: + that: ansible_facts.services['nginx.service'].state == 'running' + fail_msg: "NGINX service is not running." + + - name: "Assert NGINX service is enabled" + ansible.builtin.assert: + that: ansible_facts.services['nginx.service'].status == 'enabled' + fail_msg: "NGINX service is not enabled." + + - name: "Get an HTTP response from the test site (single site deployment from a variable)" + register: nginx_response + ansible.builtin.uri: + url: http://localhost:80 + status_code: 200 + return_content: true + + - name: "Check the response correctness on port 80" + vars: + message: "{{ nginx_response.content | trim }}" + ansible.builtin.assert: + that: message == 'Hello from ansible-nginx!' + success_msg: "{{ message }}" + fail_msg: "Unexpected response from the test site: '{{ message }}'" + + - name: "Get an HTTP response from the test site (site deployment from a dictionary)" + register: nginx_response + ansible.builtin.uri: + url: http://localhost:8080 + status_code: 200 + return_content: true + + - name: "Check the response correctness on port 8080" + vars: + message: "{{ nginx_response.content | trim }}" + ansible.builtin.assert: + that: message == 'Hello from ansible-nginx!' + success_msg: "{{ message }}" + fail_msg: "Unexpected response from the test site: '{{ message }}'" diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml new file mode 100644 index 0000000..b0bfb81 --- /dev/null +++ b/roles/nginx/tasks/main.yml @@ -0,0 +1,53 @@ +--- +- name: "Install gpg-agent" + ansible.builtin.apt: + name: gpg-agent + state: present + update_cache: true + cache_valid_time: 3600 + +- name: "Add NGINX signing key" + ansible.builtin.apt_key: + url: https://nginx.org/keys/nginx_signing.key + state: present + +- name: "Add NGINX repository" + notify: "Update apt cache" + ansible.builtin.apt_repository: + repo: "deb http://nginx.org/packages/{{ ansible_distribution | lower }}/ {{ ansible_distribution_release }} nginx" + state: present + +- name: "Install specific NGINX version" + notify: "Start nginx" + ansible.builtin.apt: + name: "nginx={{ nginx_version }}*" + state: present + +- name: "Remove default site configuration if it exists" + notify: "Reload nginx" + ansible.builtin.file: + path: /etc/nginx/conf.d/default.conf + state: absent + +- name: "Deploy a single site configuration" + when: site is defined + notify: "Reload nginx" + ansible.builtin.copy: + dest: "/etc/nginx/conf.d/{{ site.confname }}.conf" + owner: root + group: root + mode: "0644" + content: "{{ site.content }}" + +- name: "Deploy custom site configurations" + when: dir_sites is defined + with_fileglob: + - "{{ dir_sites }}/*.conf" + notify: "Reload nginx" + ansible.builtin.copy: + src: "{{ item }}" + dest: "/etc/nginx/conf.d/{{ item | basename }}" + owner: root + group: root + mode: '0644' + # validate: '/usr/sbin/nginx -t -c %s' diff --git a/roles/nginx/vars/main.yml b/roles/nginx/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/nginx/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/node_exporter/README.md b/roles/node_exporter/README.md new file mode 100644 index 0000000..0ff7a86 --- /dev/null +++ b/roles/node_exporter/README.md @@ -0,0 +1,34 @@ +Role Name +========= + +Deploy `prometheus/node_exporter` binary as a systemd unit. + +Requirements +------------ + +None + +Role Variables +-------------- + +`node_exporter_bin_path`: where to install the binary (default: `/usr/bin`). + +Dependencies +------------ + +None + +Example Playbook +---------------- + +See: [converge.yml](molecule/default/converge.yml) + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/node_exporter/defaults/main.yml b/roles/node_exporter/defaults/main.yml new file mode 100644 index 0000000..daf4778 --- /dev/null +++ b/roles/node_exporter/defaults/main.yml @@ -0,0 +1,2 @@ +--- +node_exporter_bin_path: /usr/bin diff --git a/roles/node_exporter/handlers/main.yml b/roles/node_exporter/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/node_exporter/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/node_exporter/meta/main.yml b/roles/node_exporter/meta/main.yml new file mode 100644 index 0000000..885b94c --- /dev/null +++ b/roles/node_exporter/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: node_exporter + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "Deploy prometheus/node_exporter binary as a systemd unit" + license: "GPL-2.0-or-later" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy"] + + galaxy_tags: ["prometheus"] + +dependencies: [] diff --git a/roles/node_exporter/molecule/default/converge.yml b/roles/node_exporter/molecule/default/converge.yml new file mode 100644 index 0000000..e0158f0 --- /dev/null +++ b/roles/node_exporter/molecule/default/converge.yml @@ -0,0 +1,6 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.node_exporter + node_exporter_ver: "1.10.1" diff --git a/roles/node_exporter/molecule/default/molecule.yml b/roles/node_exporter/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/node_exporter/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/node_exporter/molecule/default/verify.yml b/roles/node_exporter/molecule/default/verify.yml new file mode 100644 index 0000000..3f1bb0b --- /dev/null +++ b/roles/node_exporter/molecule/default/verify.yml @@ -0,0 +1,16 @@ +# kics-scan disable=2e8d4922-8362-4606-8c14-aa10466a1ce3 +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + tasks: + - name: "Sanity check the node exporter" + retries: 5 + delay: 1 + register: node_exporter_check + failed_when: '"node_exporter_build_info" not in node_exporter_check.content' + ansible.builtin.uri: + url: "http://localhost:9100/metrics" + return_content: true diff --git a/roles/node_exporter/tasks/install.yml b/roles/node_exporter/tasks/install.yml new file mode 100644 index 0000000..aaff138 --- /dev/null +++ b/roles/node_exporter/tasks/install.yml @@ -0,0 +1,37 @@ +--- +- name: "Install | Check if node_exporter is accessible from $PATH" + changed_when: false + failed_when: false + register: node_exporter_cmd + ansible.builtin.command: + cmd: node_exporter --version + +- name: "Install | Build the exporter name" + when: "node_exporter_ver not in node_exporter_cmd.stdout" + ansible.builtin.set_fact: + node_exporter_name: "/node_exporter-{{ node_exporter_ver }}\ + .{{ ansible_system | lower }}-\ + {{ (ansible_architecture == 'x86_64') | ternary('amd64', ansible_architecture) }}" + +- name: "Install | Download the node_exporter binary (release {{ node_exporter_ver }})" + when: "node_exporter_ver not in node_exporter_cmd.stdout" + ansible.builtin.unarchive: + src: "\ + https://github.com/prometheus/node_exporter/releases/download/\ + v{{ node_exporter_ver }}\ + {{ node_exporter_name }}\ + .tar.gz" + dest: /tmp + remote_src: true + +- name: "Install | Install the downloaded binary to '{{ node_exporter_bin_path }}'" + when: "node_exporter_ver not in node_exporter_cmd.stdout" + changed_when: false + ansible.builtin.command: + cmd: install /tmp/{{ node_exporter_name }}/node_exporter -t {{ node_exporter_bin_path }} + +- name: "Install | Push the service file template" + ansible.builtin.template: + src: node_exporter.service.j2 + dest: /usr/lib/systemd/system/node_exporter.service + mode: "0660" diff --git a/roles/node_exporter/tasks/main.yml b/roles/node_exporter/tasks/main.yml new file mode 100644 index 0000000..b63f5a3 --- /dev/null +++ b/roles/node_exporter/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: "Include installation tasks" + ansible.builtin.include_tasks: "install.yml" + +- name: "Including run tasks" + ansible.builtin.include_tasks: "run.yml" diff --git a/roles/node_exporter/tasks/run.yml b/roles/node_exporter/tasks/run.yml new file mode 100644 index 0000000..2d71c2e --- /dev/null +++ b/roles/node_exporter/tasks/run.yml @@ -0,0 +1,8 @@ +--- +- name: "Enable and start the node_exporter service" + changed_when: false + ansible.builtin.systemd: + name: node_exporter + enabled: true + daemon_reload: true + state: restarted diff --git a/roles/node_exporter/templates/node_exporter.service.j2 b/roles/node_exporter/templates/node_exporter.service.j2 new file mode 100644 index 0000000..414c5c6 --- /dev/null +++ b/roles/node_exporter/templates/node_exporter.service.j2 @@ -0,0 +1,12 @@ +[Unit] +Description=Node Exporter +After=network.target + +[Service] +Type=simple +ExecStart={{ node_exporter_bin_path }}/node_exporter \ + --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/) \ + --collector.systemd + +[Install] +WantedBy=multi-user.target diff --git a/roles/node_exporter/vars/main.yml b/roles/node_exporter/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/node_exporter/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/prometheus/README.md b/roles/prometheus/README.md new file mode 100644 index 0000000..971b023 --- /dev/null +++ b/roles/prometheus/README.md @@ -0,0 +1,50 @@ +genlab.prometheus +========= + +This ansible role installs [Prometheus](https://github.com/prometheus/prometheus) from binary distribution. Prometheus is a systems and service monitoring system. It collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts when specified conditions are observed. + +Prometheus configuration files and rules should be placed in separate directories in the source. They must have a `*.yml` suffix. You'll need to manually specify the paths to your rule files in the main Prometheus configuration file (prometheus.yml) on the target machine + +Requirements +------------ + +None + +Role Variables +-------------- +``` +prometheus_version: 3.2.1 # prometheus version +prometheus_dir: "/etc/prometheus" # where to install prometheus +prometheus_user: prometheus # user name +prometheus_group: prometheus # groups name +prometheus_config_dir: "/etc/prometheus/conf" # where to place prometheus config +prometheus_db_dir: "/var/lib/prometheus" # where to store prometheus db +prometheus_config_source_dir: prometheus # path to config files on source +prometheus_alertrules_source_dir: prometheus/rules # path to rule files on source +``` + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.prometheus + prometheus_config_source_dir: mydir/configs + prometheus_alertrules_source_dir: mydir/configs/rules + prometheus_version: 3.2.1 +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/prometheus/defaults/main.yml b/roles/prometheus/defaults/main.yml new file mode 100644 index 0000000..5ecc61c --- /dev/null +++ b/roles/prometheus/defaults/main.yml @@ -0,0 +1,7 @@ +--- +prometheus_version: 3.2.1 +prometheus_dir: "/etc/prometheus" +prometheus_user: prometheus +prometheus_group: prometheus +prometheus_config_dir: "/etc/prometheus/conf" +prometheus_db_dir: "/var/lib/prometheus" diff --git a/roles/prometheus/handlers/main.yml b/roles/prometheus/handlers/main.yml new file mode 100644 index 0000000..29ac016 --- /dev/null +++ b/roles/prometheus/handlers/main.yml @@ -0,0 +1,8 @@ +--- +- name: "(Re)start Prometheus service" + become: true + ansible.builtin.systemd: + name: prometheus.service + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/prometheus/meta/main.yml b/roles/prometheus/meta/main.yml new file mode 100644 index 0000000..b4335ec --- /dev/null +++ b/roles/prometheus/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "prometheus" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/prometheus/molecule/default/converge.yml b/roles/prometheus/molecule/default/converge.yml new file mode 100644 index 0000000..a9b0439 --- /dev/null +++ b/roles/prometheus/molecule/default/converge.yml @@ -0,0 +1,8 @@ +--- +- name: Converge + hosts: all + + roles: + - role: genlab.common.prometheus + prometheus_config_source_dir: prometheus + alertrules_source_dir: prometheus/rules diff --git a/roles/prometheus/molecule/default/molecule.yml b/roles/prometheus/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/prometheus/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/prometheus/molecule/default/prometheus/prometheus.yml b/roles/prometheus/molecule/default/prometheus/prometheus.yml new file mode 100644 index 0000000..31854f4 --- /dev/null +++ b/roles/prometheus/molecule/default/prometheus/prometheus.yml @@ -0,0 +1,14 @@ +--- +global: + scrape_interval: 15s + evaluation_interval: 15s + +rule_files: + - /etc/prometheus/conf/alertules.yml + # - "first.rules" + # - "second.rules" + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ['localhost:9090'] diff --git a/roles/prometheus/molecule/default/prometheus/rules/alertrules.yml b/roles/prometheus/molecule/default/prometheus/rules/alertrules.yml new file mode 100644 index 0000000..dc3c47f --- /dev/null +++ b/roles/prometheus/molecule/default/prometheus/rules/alertrules.yml @@ -0,0 +1,13 @@ +groups: + - name: example + labels: + team: myteam + rules: + - alert: HighRequestLatency + expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5 + for: 10m + keep_firing_for: 5m + labels: + severity: page + annotations: + summary: High request latency diff --git a/roles/prometheus/molecule/default/verify.yml b/roles/prometheus/molecule/default/verify.yml new file mode 100644 index 0000000..c085dd1 --- /dev/null +++ b/roles/prometheus/molecule/default/verify.yml @@ -0,0 +1,55 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + tasks: + + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + - name: "Check if Prometheus is installed" + changed_when: false + ansible.builtin.command: "prometheus --version" + register: prometheus_installed_version + + - name: "Check Prometheus version" + ansible.builtin.assert: + that: "prometheus_installed_version.stdout is regex('{{ prometheus_version }}')" + success_msg: "Prometheus version {{ prometheus_version }} is installed and working" + fail_msg: "Prometheus version {{ prometheus_version }} is not installed or not working correctly" + + # kics-scan ignore-block + - name: "Check if Prometheus is reachable" + ansible.builtin.uri: + url: "http://localhost:9090/-/healthy" + return_content: true + status_code: 200 + method: GET + body_format: json + register: prometheus_health + + - name: "Debug Prometheus health status" + ansible.builtin.assert: + that: "prometheus_health.content | trim == 'Prometheus Server is Healthy.'" + success_msg: "Prometheus is healthy" + fail_msg: "Prometheus is not healthy" + + # kics-scan ignore-block + - name: "Check if Prometheus is ready" + ansible.builtin.uri: + url: "http://localhost:9090/-/ready" + return_content: true + status_code: 200 + method: GET + body_format: json + register: prometheus_ready + + - name: "Debug Prometheus readiness status" + ansible.builtin.assert: + that: "prometheus_ready.content | trim == 'Prometheus Server is Ready.'" + success_msg: "Prometheus is ready" + fail_msg: "Prometheus is not ready" diff --git a/roles/prometheus/tasks/configure.yml b/roles/prometheus/tasks/configure.yml new file mode 100644 index 0000000..cbad1ad --- /dev/null +++ b/roles/prometheus/tasks/configure.yml @@ -0,0 +1,23 @@ +--- +- name: "Copy config file" + notify: "(Re)start Prometheus service" + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ prometheus_config_dir }}/{{ item | basename }}" + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + mode: "0660" + with_fileglob: + - "{{ prometheus_config_source_dir }}/*.yml" + +- name: "Copy alert rules" + notify: "(Re)start Prometheus service" + when: alertrules_source_dir is defined + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ prometheus_config_dir }}/{{ item | basename }}" + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + mode: "0660" + with_fileglob: + - "{{ alertrules_source_dir }}/*.yml" diff --git a/roles/prometheus/tasks/install.yml b/roles/prometheus/tasks/install.yml new file mode 100644 index 0000000..3118047 --- /dev/null +++ b/roles/prometheus/tasks/install.yml @@ -0,0 +1,84 @@ +--- +- name: "Create Prometheus system group" + ansible.builtin.group: + name: "{{ prometheus_group }}" + system: true + state: present + +- name: "Create Prometheus system user" + ansible.builtin.user: + name: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + system: true + shell: "/sbin/nologin" + create_home: false + state: present + +- name: "Install prometheus from binary" + block: + - name: "Check Prometheus version" + changed_when: false + ansible.builtin.command: + cmd: "prometheus --version" + register: prometheus_ver + + - name: "Assert version correctness" + ansible.builtin.assert: + that: "prometheus_ver.stdout is regex('{{ prometheus_version }}')" + success_msg: "prometheus version {{ prometheus_version }} is installed and working" + fail_msg: "prometheus version {{ prometheus_version }} is not installed or not working correctly" + + rescue: + - name: "Create prometheus directories {{ item }}" + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + mode: "0755" + with_items: + - "{{ prometheus_config_dir }}" + - "{{ prometheus_dir }}" + - "{{ prometheus_db_dir }}" + + - name: "Download Prometheus binary" + ansible.builtin.get_url: + url: "https://github.com/prometheus/prometheus/releases/download/v{{ prometheus_version }}/prometheus-{{ prometheus_version }}.linux-amd64.tar.gz" + dest: "/tmp/prometheus-{{ prometheus_version }}.linux-amd64.tar.gz" + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + mode: "0644" + + - name: "Unpack Prometheus binaries" + notify: "(Re)start Prometheus service" + ansible.builtin.unarchive: + src: "/tmp/prometheus-{{ prometheus_version }}.linux-amd64.tar.gz" + dest: "{{ prometheus_dir }}" + creates: "{{ prometheus_dir }}/prometheus-{{ prometheus_version }}.linux-amd64" + remote_src: true + + - name: "Cleanup downloaded file" + ansible.builtin.file: + path: "/tmp/prometheus-{{ prometheus_version }}.linux-amd64.tar.gz" + state: absent + + - name: "Move official prometheus and promtool binaries" + ansible.builtin.copy: + src: "{{ prometheus_dir }}/prometheus-{{ prometheus_version }}.linux-amd64/{{ item }}" + dest: "/usr/local/bin/{{ item }}" + mode: "0755" + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + remote_src: true + with_items: + - prometheus + - promtool + +- name: "Create systemd service unit" + notify: "(Re)start Prometheus service" + ansible.builtin.template: + src: prometheus.service.j2 + dest: /etc/systemd/system/prometheus.service + owner: "{{ prometheus_user }}" + group: "{{ prometheus_group }}" + mode: "0660" diff --git a/roles/prometheus/tasks/main.yml b/roles/prometheus/tasks/main.yml new file mode 100644 index 0000000..43bcfba --- /dev/null +++ b/roles/prometheus/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: "Install Prometheus from binary" + ansible.builtin.include_tasks: install.yml + +- name: "Configure and add alert rules" + ansible.builtin.include_tasks: configure.yml + +- name: "Flush handlers" + ansible.builtin.meta: "flush_handlers" diff --git a/roles/prometheus/templates/prometheus.service.j2 b/roles/prometheus/templates/prometheus.service.j2 new file mode 100644 index 0000000..2f17191 --- /dev/null +++ b/roles/prometheus/templates/prometheus.service.j2 @@ -0,0 +1,28 @@ +[Unit] +Description=Prometheus +Wants=network-online.target +After=network-online.target +Documentation="https://prometheus.io/" + +[Service] +User=prometheus +Group=prometheus +Type=simple +ExecStart=/usr/local/bin/prometheus \ + --config.file {{ prometheus_config_dir }}/prometheus.yml \ + --storage.tsdb.path {{ prometheus_db_dir }} \ + --storage.tsdb.retention.time=30d + +# Security hardening +ReadWritePaths={{ prometheus_db_dir }} +ProtectSystem=strict +NoNewPrivileges=true +PrivateTmp=true +ProtectKernelModules=true +ProtectControlGroups=true +ProtectKernelTunables=true +ProtectClock=yes +RestrictSUIDSGID=true + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/roles/prometheus/vars/main.yml b/roles/prometheus/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/prometheus/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/promtail/README.md b/roles/promtail/README.md new file mode 100644 index 0000000..7dac0b4 --- /dev/null +++ b/roles/promtail/README.md @@ -0,0 +1,44 @@ +promtail +========= + +Installs promtail as systemd service. + +Requirements +------------ + +Role Variables +-------------- + +(all optional) +`promtail_version`: version to install +`promtail_loki_server`: set loki server +`promtail_loki_port`: set loki port +`custom_server_config`: path to custom config witch replace all config with your own +`custom_scrape_configs`: path to custom scrape configs + +Dependencies +------------ + +No + +Example Playbook +---------------- + + - hosts: servers + roles: + - role: promtail + promtail_version: 2.7.3 + +License +------- + +MIT + +Author Information +------------------ + +Alexander Gorelyshev and Danilkin Danila (MIPT) + +Genlab LLC + +corvus-migratorius@proton.me diff --git a/roles/promtail/defaults/main.yml b/roles/promtail/defaults/main.yml new file mode 100644 index 0000000..8a8fa39 --- /dev/null +++ b/roles/promtail/defaults/main.yml @@ -0,0 +1,7 @@ +--- +promtail_version: 2.7.3 +promtail_positions_path: /home/promtail/positions.yaml +promtail_http_port: 9080 +promtail_loki_server: localhost +promtail_loki_port: 3100 +promtail_add_var_logs: true diff --git a/roles/promtail/handlers/main.yml b/roles/promtail/handlers/main.yml new file mode 100644 index 0000000..d1c06ac --- /dev/null +++ b/roles/promtail/handlers/main.yml @@ -0,0 +1,11 @@ +--- + +- name: "Restart the Promtail daemon" + ansible.builtin.systemd: + name: promtail + state: restarted + + +- name: "Reload the Promtail daemon configuration" + ansible.builtin.systemd: + daemon_reload: true diff --git a/roles/promtail/meta/main.yml b/roles/promtail/meta/main.yml new file mode 100644 index 0000000..79d6be3 --- /dev/null +++ b/roles/promtail/meta/main.yml @@ -0,0 +1,14 @@ +galaxy_info: + role_name: promtail + namespace: genlab + author: Danilkin Danila + description: Installs Promtail + + company: Genlab LLC + license: MIT + + min_ansible_version: "2.1" + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/promtail/molecule/default/converge.yml b/roles/promtail/molecule/default/converge.yml new file mode 100644 index 0000000..ea072dd --- /dev/null +++ b/roles/promtail/molecule/default/converge.yml @@ -0,0 +1,5 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.promtail diff --git a/roles/promtail/molecule/default/molecule.yml b/roles/promtail/molecule/default/molecule.yml new file mode 100644 index 0000000..8d39b18 --- /dev/null +++ b/roles/promtail/molecule/default/molecule.yml @@ -0,0 +1,24 @@ +--- +dependency: + name: galaxy +driver: + name: docker +platforms: + - name: ubuntu + image: geerlingguy/docker-ubuntu2204-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true +provisioner: + name: ansible + playbooks: + converge: converge.yml +verifier: + name: ansible +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/promtail/molecule/default/verify.yml b/roles/promtail/molecule/default/verify.yml new file mode 100644 index 0000000..7e078c2 --- /dev/null +++ b/roles/promtail/molecule/default/verify.yml @@ -0,0 +1,20 @@ +--- +- name: Verify + hosts: all + gather_facts: false + tasks: + # https://github.com/ansible/molecule/issues/3587#issuecomment-1158650179 + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + # kics-scan ignore-block - kics doesn't like http but it's localhost so it's not important + - name: "Sanity check the Promtail daemon" + retries: 3 + delay: 1 + register: promtail_this + failed_when: "promtail_this.content != 'Ready'" + ansible.builtin.uri: + url: "http://localhost:{{ promtail_http_port }}/ready" + return_content: true diff --git a/roles/promtail/tasks/main.yml b/roles/promtail/tasks/main.yml new file mode 100644 index 0000000..127c10c --- /dev/null +++ b/roles/promtail/tasks/main.yml @@ -0,0 +1,107 @@ +--- +- name: "Install packages" + ansible.builtin.apt: + name: [acl, unzip] + state: present + update_cache: true + cache_valid_time: 3600 + + +- name: "Create the 'promtail' group" + ansible.builtin.group: + name: promtail + state: present + + +- name: "Create the 'promtail' user" + ansible.builtin.user: + name: promtail + groups: + - promtail + - systemd-journal + - adm + state: present + system: true + + +- name: "Set facl to allow promtail user to read '/var/log' contents" + ansible.posix.acl: + path: /var/log + entity: promtail + etype: user + permissions: rX + state: present + + +- name: "Make promtail user owner of '{{ promtail_positions_path }}'" + changed_when: false + ansible.builtin.file: + path: "{{ promtail_positions_path }}" + owner: promtail + group: promtail + state: touch + mode: "0750" + + +- name: "Install the requested Promtail version" + block: + - name: "Check Promtail version" + ansible.builtin.command: /usr/bin/promtail --version + register: promtail_version_check + changed_when: false + + - name: "Assert version correctness" + notify: "Restart the Promtail daemon" + ansible.builtin.assert: + that: "promtail_version in promtail_version_check.stdout" + success_msg: "Expected Promtail version available ({{ promtail_version }})" + fail_msg: "Expected version '{{ promtail_version }}'; available is '{{ promtail_version_check.stdout }}'" + + rescue: + - name: "Install Promtail if its not present" + ansible.builtin.unarchive: + src: "https://github.com/grafana/loki/releases/download/v\ + {{ promtail_version }}/promtail-linux-amd64.zip" + dest: /usr/bin/ + remote_src: true + + - name: "Rename and set permissions for the Promtail binary" + ansible.builtin.copy: + src: "/usr/bin/promtail-linux-amd64" + dest: "/usr/bin/promtail" + owner: root + group: root + mode: "0755" + remote_src: true + + - name: "Cleanup the downloaded file" + ansible.builtin.file: + path: "/usr/bin/promtail-linux-amd64" + state: absent + + +- name: "Template the systemd unit file" + notify: "Reload the Promtail daemon configuration" + ansible.builtin.template: + src: promtail.service.j2 + dest: /etc/systemd/system/promtail.service + owner: root + group: root + mode: "0644" + + +- name: "Template Promtail config file" + notify: "Restart the Promtail daemon" + ansible.builtin.template: + src: templates/promtail.yml.j2 + dest: /usr/local/bin/config-promtail.yml + owner: root + group: root + mode: "0644" + + +- name: "Enable and start Promtail daemon" + ansible.builtin.systemd: + name: promtail + state: started + enabled: true diff --git a/roles/promtail/templates/promtail.service.j2 b/roles/promtail/templates/promtail.service.j2 new file mode 100644 index 0000000..3b7b517 --- /dev/null +++ b/roles/promtail/templates/promtail.service.j2 @@ -0,0 +1,11 @@ +[Unit] +Description=Promtail service +After=network.target + +[Service] +Type=simple +User=promtail +ExecStart=/usr/bin/promtail -config.file /usr/local/bin/config-promtail.yml + +[Install] +WantedBy=multi-user.target diff --git a/roles/promtail/templates/promtail.yml.j2 b/roles/promtail/templates/promtail.yml.j2 new file mode 100644 index 0000000..21dea49 --- /dev/null +++ b/roles/promtail/templates/promtail.yml.j2 @@ -0,0 +1,28 @@ +{% if custom_server_config is defined %} +{{ custom_server_config }} +{% else %} +server: + http_listen_port: {{ promtail_http_port }} + grpc_listen_port: 0 +{% endif %} +positions: + filename: {{ promtail_positions_path }} + +clients: + - url: "http://{{ promtail_loki_server }}:{{ promtail_loki_port }}/loki/api/v1/push" + +scrape_configs: +{% if promtail_add_var_logs %} + - job_name: system + static_configs: + - targets: + - localhost + labels: + job: varlogs + __path__: /var/log/*log + host: {{ ansible_hostname }} +{% endif %} + +{% if custom_scrape_configs is defined %} +{{ custom_scrape_configs }} +{% endif %} diff --git a/roles/promtail/vars/main.yml b/roles/promtail/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/promtail/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/rclone_yandex/.gitignore b/roles/rclone_yandex/.gitignore new file mode 100644 index 0000000..8cd0df3 --- /dev/null +++ b/roles/rclone_yandex/.gitignore @@ -0,0 +1,2 @@ +.vscode +.idea \ No newline at end of file diff --git a/roles/rclone_yandex/README.md b/roles/rclone_yandex/README.md new file mode 100644 index 0000000..8e6f453 --- /dev/null +++ b/roles/rclone_yandex/README.md @@ -0,0 +1,41 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +`rclone_config_path`: where to keep the configuration file (default: `/root/rclone.conf`) +`rclone_mountpoint`: where to mount the Yandex Disk share +`rclone_group`: how to name the group that will own the mountpoint + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +```yaml +roles: + - role: rclone_yandex + rclone_mountpoint: /mnt/yandex-disk + rclone_group: yandex +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me \ No newline at end of file diff --git a/roles/rclone_yandex/defaults/main.yml b/roles/rclone_yandex/defaults/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/rclone_yandex/defaults/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/rclone_yandex/handlers/main.yml b/roles/rclone_yandex/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/rclone_yandex/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/rclone_yandex/meta/main.yml b/roles/rclone_yandex/meta/main.yml new file mode 100644 index 0000000..2bb3f62 --- /dev/null +++ b/roles/rclone_yandex/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "rclone_yandex" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "Deploy rclone as a systemd unit and mount a Yandex Disk storage" + license: "GPL-2.0-or-later" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/rclone_yandex/molecule/default/converge.yml b/roles/rclone_yandex/molecule/default/converge.yml new file mode 100644 index 0000000..7e6452e --- /dev/null +++ b/roles/rclone_yandex/molecule/default/converge.yml @@ -0,0 +1,19 @@ +--- +- name: Converge + hosts: all + + pre_tasks: + - name: "Install basic tools" + ansible.builtin.apt: + name: + - curl + update_cache: true + cache_valid_time: 3600 + autoremove: true + autoclean: true + + roles: + - role: genlab.common.rclone_yandex + rclone_mountpoint: /mnt/yandex-disk + rclone_group: yandex + rclone_secrets: secrets/token.yml diff --git a/roles/rclone_yandex/molecule/default/molecule.yml b/roles/rclone_yandex/molecule/default/molecule.yml new file mode 100644 index 0000000..fd2f06d --- /dev/null +++ b/roles/rclone_yandex/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/rclone_yandex/molecule/default/secrets/token.yml b/roles/rclone_yandex/molecule/default/secrets/token.yml new file mode 100644 index 0000000..0b0550a --- /dev/null +++ b/roles/rclone_yandex/molecule/default/secrets/token.yml @@ -0,0 +1,5 @@ +rclone: + access_token: "y0_Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..." + token_type: "OAuth" + refresh_token: "1:SPUlxxxxxxxxxxxxxxxxxxxxxxxx..." + expiry: "2024-05-16T05:26:26.319196346Z" diff --git a/roles/rclone_yandex/molecule/default/verify.yml b/roles/rclone_yandex/molecule/default/verify.yml new file mode 100644 index 0000000..4868f6d --- /dev/null +++ b/roles/rclone_yandex/molecule/default/verify.yml @@ -0,0 +1,18 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + become: true + + tasks: + - name: "Gather facts about the service" + ansible.builtin.service_facts: + + - name: "Check if the `rclone-yandex` service is running" + ansible.builtin.assert: + that: + - "'rclone-yandex.service' in ansible_facts.services" + - "ansible_facts.services['rclone-yandex.service'].state == 'running'" + fail_msg: "the 'rclone-yandex' service is not running!" + success_msg: "the 'rclone-yandex' service is up and running" diff --git a/roles/rclone_yandex/tasks/configure.yaml b/roles/rclone_yandex/tasks/configure.yaml new file mode 100644 index 0000000..60715f7 --- /dev/null +++ b/roles/rclone_yandex/tasks/configure.yaml @@ -0,0 +1,58 @@ +--- +- name: "Configure | create a group - '{{ rclone_group }}'" + ansible.builtin.group: + name: "{{ rclone_group }}" + state: present + +- name: "Configure | get GID for '{{ rclone_group }}'" + register: rclone_yandex_group_gid + changed_when: false + failed_when: not rclone_yandex_group_gid.stdout + ansible.builtin.shell: + cmd: 'set -o pipefail && getent group "{{ rclone_group }}" | cut -d: -f3' + executable: /bin/bash + +- name: "Configure | display rclone mount ownership group" + ansible.builtin.debug: + msg: "'{{ rclone_group }}' ({{ rclone_yandex_group_gid.stdout }})" + +- name: "Configure | set up directories" + loop: + - {path: "/tmp/rclone", group: "root"} + - {path: "/mnt/yandex-disk", group: "{{ rclone_group }}"} + ansible.builtin.file: + state: directory + path: "{{ item.path }}" + owner: root + group: "{{ item.group }}" + mode: "0770" + +- name: "Configure | create the [yandex] section" + ansible.builtin.lineinfile: + create: true + path: "{{ rclone_yandex_config_path }}" + line: "[yandex]" + mode: "0660" + +- name: "Configure | read in the YAMLified Yandex share credentials" + ansible.builtin.include_vars: + file: "{{ rclone_secrets }}" + +- name: "Configure | insert/update the section for the [yandex] share" + ansible.builtin.blockinfile: + path: "{{ rclone_yandex_config_path }}" + insertafter: "^[yandex]" + block: | + type = yandex + token = {{ rclone | to_json }} + +- name: "Configure | template a systemd service file" + ansible.builtin.template: + src: templates/rclone-yandex.service.j2 + dest: /lib/systemd/system/rclone-yandex.service + mode: "0660" + +# validate attribute doesn't seem to work here, unfortunately, so command to the rescue +- name: "Configure | validate the service file" + ansible.builtin.command: systemd-analyze verify rclone-yandex.service + changed_when: false diff --git a/roles/rclone_yandex/tasks/install.yaml b/roles/rclone_yandex/tasks/install.yaml new file mode 100644 index 0000000..71cba8b --- /dev/null +++ b/roles/rclone_yandex/tasks/install.yaml @@ -0,0 +1,39 @@ +--- +- name: "Install | unzip (RHEL)" + ansible.builtin.dnf: + name: [unzip] + state: installed + when: ansible_os_family == 'RHEL' + +- name: "Install | unzip (Debian)" + ansible.builtin.apt: + update_cache: true + name: [unzip] + state: present + when: ansible_os_family == 'Debian' + +- name: "Install | rclone_yandex" + block: + - name: "Install | check if rclone binary is available in PATH" + changed_when: false + ansible.builtin.command: + cmd: "rclone --version" + rescue: + - name: "Install | fetch and install the rclone binary" # noqa: no-changed-when + register: rclone_yandex_result + failed_when: rclone_yandex_result.rc != 0 + ansible.builtin.shell: + cmd: "set -o pipefail && curl https://rclone.org/install.sh | sudo bash" + executable: /bin/bash + +- name: "Install | fuse (RHEL)" + ansible.builtin.dnf: + name: [fuse] + state: installed + when: ansible_os_family == 'RHEL' + +- name: "Install | fuse (Debian)" + ansible.builtin.apt: + name: [fuse3] + state: present + when: ansible_os_family == 'Debian' diff --git a/roles/rclone_yandex/tasks/main.yml b/roles/rclone_yandex/tasks/main.yml new file mode 100644 index 0000000..5adbe57 --- /dev/null +++ b/roles/rclone_yandex/tasks/main.yml @@ -0,0 +1,11 @@ +# tasks file +--- + +- name: "Include installation tasks" + ansible.builtin.include_tasks: "install.yaml" + +- name: "Include configuration tasks" + ansible.builtin.include_tasks: "configure.yaml" + +- name: "Include run tasks" + ansible.builtin.include_tasks: "run.yaml" diff --git a/roles/rclone_yandex/tasks/run.yaml b/roles/rclone_yandex/tasks/run.yaml new file mode 100644 index 0000000..306b5ed --- /dev/null +++ b/roles/rclone_yandex/tasks/run.yaml @@ -0,0 +1,6 @@ +--- +- name: "Run | start the 'rclone-yandex' service" + ansible.builtin.systemd: + name: rclone-yandex + state: started + daemon_reload: true diff --git a/roles/rclone_yandex/templates/rclone-yandex.service.j2 b/roles/rclone_yandex/templates/rclone-yandex.service.j2 new file mode 100644 index 0000000..c8df724 --- /dev/null +++ b/roles/rclone_yandex/templates/rclone-yandex.service.j2 @@ -0,0 +1,26 @@ +[Unit] +Description=rclone: Remote FUSE filesystem for cloud storage config %i +Documentation=man:rclone(1) +After=network-online.target +Wants=network-online.target + +[Service] +Type=notify +ExecStart= \ + /usr/bin/rclone mount \ + --gid={{ rclone_yandex_group_gid.stdout }} \ + --dir-perms=0770 \ + --file-perms=0660 \ + --umask=007 \ + --default-permissions \ + --allow-other \ + --config "{{ rclone_yandex_config_path }}" \ + --log-file="/tmp/rclone/yandex.log" \ + --log-level=INFO \ + --vfs-cache-mode=off \ + yandex: "{{ rclone_mountpoint }}" + +ExecStop=/bin/fusermount -u "{{ rclone_mountpoint }}" + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/roles/rclone_yandex/vars/main.yml b/roles/rclone_yandex/vars/main.yml new file mode 100644 index 0000000..ce372de --- /dev/null +++ b/roles/rclone_yandex/vars/main.yml @@ -0,0 +1,4 @@ +--- +# vars file for rclone_yandex + +rclone_yandex_config_path: "/root/rclone.conf" diff --git a/roles/rustdesk/README.md b/roles/rustdesk/README.md new file mode 100644 index 0000000..390a16c --- /dev/null +++ b/roles/rustdesk/README.md @@ -0,0 +1,42 @@ +Rustdesk Server +========= + +Rustdesk server role for ansible + +Requirements +------------ + +Ports: +``` +tcp 21115, 21116, 21117, 21118, 21119 +udp 21116 +``` + +Role Variables +-------------- + +``` rustdesk_server_version ``` — contains rustdesk-server version to be installed +``` rustdesk_client_version ``` — contains rustdesk-client version for executable to be configured + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.rustdesk_server +``` + +License +------- + +BSD + +Author Information +------------------ + +malyuk.ss@genlab.llc diff --git a/roles/rustdesk/defaults/main.yml b/roles/rustdesk/defaults/main.yml new file mode 100644 index 0000000..315c715 --- /dev/null +++ b/roles/rustdesk/defaults/main.yml @@ -0,0 +1,3 @@ +--- +rustdesk_server_version: "1.1.14" +rustdesk_client_version: "1.4.1" diff --git a/roles/rustdesk/files/generate-rustdesk-exe.sh b/roles/rustdesk/files/generate-rustdesk-exe.sh new file mode 100644 index 0000000..2fb454d --- /dev/null +++ b/roles/rustdesk/files/generate-rustdesk-exe.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# shellcheck disable=SC2155 +set -eu -o pipefail + +readonly RUSTDESK_SERVER_IP="$(curl -s ifconfig.me)" +readonly RUSTDESK_SERVER_KEY="$(cat /opt/rustdesk-server/lib/id_ed25519.pub)" + +cp /opt/rustdesk-server/lib/rustdesk-client.exe \ + /opt/rustdesk-server/lib/rustdesk-host="$RUSTDESK_SERVER_IP",key="$RUSTDESK_SERVER_KEY".exe diff --git a/roles/rustdesk/files/rustdesk-hbbr.service b/roles/rustdesk/files/rustdesk-hbbr.service new file mode 100644 index 0000000..6b0f672 --- /dev/null +++ b/roles/rustdesk/files/rustdesk-hbbr.service @@ -0,0 +1,32 @@ +[Unit] +Description=Rustdesk Relay Server + +[Service] +Type=simple +LimitNOFILE=10000 +Environment="RUST_LOG_LEVEL=trace" +WorkingDirectory=/opt/rustdesk-server/lib/ + +ExecStart=/opt/rustdesk-server/amd64/hbbr + +Restart=on-failure +RestartSec=10 +StartLimitIntervalSec=60 +StartLimitBurst=2 + +User=rustdesk +Group=rustdesk + +# Security +ProtectSystem=strict +ReadWritePaths=/opt/rustdesk-server/lib/ +NoNewPrivileges=true +PrivateTmp=true +ProtectKernelModules=true +ProtectControlGroups=true +ProtectKernelTunables=true +ProtectClock=yes +RestrictSUIDSGID=true + +[Install] +WantedBy=multi-user.target diff --git a/roles/rustdesk/files/rustdesk-hbbs.service b/roles/rustdesk/files/rustdesk-hbbs.service new file mode 100644 index 0000000..d84f44e --- /dev/null +++ b/roles/rustdesk/files/rustdesk-hbbs.service @@ -0,0 +1,35 @@ +[Unit] +Description=Rustdesk Signal Server + +[Service] +Type=simple +LimitNOFILE=10000 +Environment="RUST_LOG_LEVEL=trace" +WorkingDirectory=/opt/rustdesk-server/lib/ + +ExecStartPre=/bin/sh -c 'rm -rf /opt/rustdesk-server/lib/id_ed25519*' +ExecStartPre=/bin/sh -c 'rm -rf /opt/rustdesk-server/lib/rustdesk-host=*' +ExecStart=/opt/rustdesk-server/amd64/hbbs +ExecStartPost=/opt/rustdesk-server/generate-rustdesk-exe.sh + +Restart=on-failure +RestartSec=10 +StartLimitIntervalSec=60 +StartLimitBurst=2 + +User=rustdesk +Group=rustdesk + +# Security +ProtectSystem=strict +ReadWritePaths=/opt/rustdesk-server/lib/ +NoNewPrivileges=true +PrivateTmp=true +ProtectKernelModules=true +ProtectControlGroups=true +ProtectKernelTunables=true +ProtectClock=yes +RestrictSUIDSGID=true + +[Install] +WantedBy=multi-user.target diff --git a/roles/rustdesk/handlers/main.yml b/roles/rustdesk/handlers/main.yml new file mode 100644 index 0000000..976fccb --- /dev/null +++ b/roles/rustdesk/handlers/main.yml @@ -0,0 +1,15 @@ +--- + +- name: "Start and enable rustdesk-hbbr.service" + ansible.builtin.service: + name: "rustdesk-hbbr" + state: started + enabled: true + daemon_reload: true + +- name: "Start and enable rustdesk-hbbs.service" + ansible.builtin.service: + name: "rustdesk-hbbs" + state: started + enabled: true + daemon_reload: true diff --git a/roles/rustdesk/meta/main.yml b/roles/rustdesk/meta/main.yml new file mode 100644 index 0000000..d396088 --- /dev/null +++ b/roles/rustdesk/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "rustdesk" + namespace: genlab + author: "Sergey Malyuk" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/rustdesk/molecule/default/client-binary-existance.yml b/roles/rustdesk/molecule/default/client-binary-existance.yml new file mode 100644 index 0000000..f1f76f8 --- /dev/null +++ b/roles/rustdesk/molecule/default/client-binary-existance.yml @@ -0,0 +1,28 @@ +--- +- name: "Get public IP" + register: rustdesk_public_ip + changed_when: false + ansible.builtin.uri: + url: https://ifconfig.me + return_content: true + headers: + Accept: text/plain + User-Agent: curl/7.68.0 + +- name: "Get public key" + register: rustdesk_public_key + changed_when: false + ansible.builtin.command: "cat /opt/rustdesk-server/lib/id_ed25519.pub" + +- name: "Get client rustdesk binary information" + changed_when: false + register: rustdesk_bin + ansible.builtin.stat: + path: "/opt/rustdesk-server/lib/rustdesk-host={{ rustdesk_public_ip.content }},key={{ rustdesk_public_key.stdout }}.exe" + +- name: "Check if client binary exists" + changed_when: false + ansible.builtin.assert: + that: "rustdesk_bin.stat.exists" + success_msg: "Rustdesk binary exists and named correctly" + fail_msg: "Rustdesk binary not found" diff --git a/roles/rustdesk/molecule/default/converge.yml b/roles/rustdesk/molecule/default/converge.yml new file mode 100644 index 0000000..66aeafe --- /dev/null +++ b/roles/rustdesk/molecule/default/converge.yml @@ -0,0 +1,5 @@ +--- +- name: Converge + hosts: all + roles: + - role: "genlab.common.rustdesk" diff --git a/roles/rustdesk/molecule/default/molecule.yml b/roles/rustdesk/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/rustdesk/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/rustdesk/molecule/default/verify.yml b/roles/rustdesk/molecule/default/verify.yml new file mode 100644 index 0000000..f4832ef --- /dev/null +++ b/roles/rustdesk/molecule/default/verify.yml @@ -0,0 +1,27 @@ +--- + +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + vars_files: + - ../../defaults/main.yml + + tasks: + - name: "Gather service facts" + ansible.builtin.service_facts: + + - name: "Check if rustdesk-hbbr is running" + ansible.builtin.assert: + that: "ansible_facts.services['rustdesk-hbbr.service'].state == 'running'" + success_msg: "rustdesk-hbbr is running" + fail_msg: "rustdesk-hbbr is NOT running" + + - name: "Check if rustdesk-hbbs is running" + ansible.builtin.assert: + that: "ansible_facts.services['rustdesk-hbbs.service'].state == 'running'" + success_msg: "rustdesk-hbbs is running" + fail_msg: "rustdesk-hbbs is NOT running" + + - name: "Check client binary existance" + ansible.builtin.include_tasks: client-binary-existance.yml diff --git a/roles/rustdesk/tasks/check-rustdesk-version.yml b/roles/rustdesk/tasks/check-rustdesk-version.yml new file mode 100644 index 0000000..776b702 --- /dev/null +++ b/roles/rustdesk/tasks/check-rustdesk-version.yml @@ -0,0 +1,25 @@ +--- + +- name: "Check hbbr version" + register: rustdesk_hbbr_output + changed_when: false + ansible.builtin.command: + cmd: "/opt/rustdesk-server/amd64/hbbr --version" + +- name: "Check if the version of hbbr is the expected one" + ansible.builtin.assert: + that: "rustdesk_hbbr_output.stdout == 'hbbr {{ rustdesk_server_version }}'" + success_msg: "hbbr is installed and is the correct version" + fail_msg: "hbbr is either not installed or does not match the target version" + +- name: "Check hbbs version" + register: rustdesk_hbbs_output + changed_when: false + ansible.builtin.command: + cmd: "/opt/rustdesk-server/amd64/hbbs --version" + +- name: "Check if the version of hbbs is the expected one" + ansible.builtin.assert: + that: "rustdesk_hbbs_output.stdout == 'hbbs {{ rustdesk_server_version }}'" + success_msg: "hbbs is installed and is the correct version" + fail_msg: "hbbs is either not installed or does not match the target version" diff --git a/roles/rustdesk/tasks/install-curl.yml b/roles/rustdesk/tasks/install-curl.yml new file mode 100644 index 0000000..7729e18 --- /dev/null +++ b/roles/rustdesk/tasks/install-curl.yml @@ -0,0 +1,30 @@ +--- + +- name: "Install curl for generate-rustdesk-bat.sh script on Debian family" + when: ansible_os_family == "Debian" + ansible.builtin.apt: + name: curl + state: present + update_cache: true + +- name: "Install curl for generate-rustdesk-bat.sh script on RedHat family" + when: ansible_os_family == "RedHat" + ansible.builtin.dnf: + name: curl + state: present + +- name: "Install curl for generate-rustdesk-bat.sh script on FreeBSD family" + when: ansible_os_family == "FreeBSD" + community.general.pkgng: + name: curl + state: present + +- name: "Install curl for generate-rustdesk-bat.sh script on other OS" + when: + - ansible_os_family != "Debian" + - and ansible_os_family != "RedHat" + - and ansible_os_family != "FreeBSD" + ansible.builtin.package: + name: curl + state: present + update_cache: true diff --git a/roles/rustdesk/tasks/install-unzip.yml b/roles/rustdesk/tasks/install-unzip.yml new file mode 100644 index 0000000..efff4b1 --- /dev/null +++ b/roles/rustdesk/tasks/install-unzip.yml @@ -0,0 +1,30 @@ +--- + +- name: "Install unzip for unarchiving the binaries archive on Debian family" + when: ansible_os_family == "Debian" + ansible.builtin.apt: + name: unzip + state: present + update_cache: true + +- name: "Install unzip for unarchiving the binaries archive on RedHat family" + when: ansible_os_family == "RedHat" + ansible.builtin.dnf: + name: unzip + state: present + +- name: "Install unzip for unarchiving the binaries archive on FreeBSD family" + when: ansible_os_family == "FreeBSD" + community.general.pkgng: + name: unzip + state: present + +- name: "Install unzip for unarchiving the binaries archive on other OS" + when: + - ansible_os_family != "Debian" + - and ansible_os_family != "RedHat" + - and ansible_os_family != "FreeBSD" + ansible.builtin.package: + name: unzip + state: present + update_cache: true diff --git a/roles/rustdesk/tasks/main.yml b/roles/rustdesk/tasks/main.yml new file mode 100644 index 0000000..94a3998 --- /dev/null +++ b/roles/rustdesk/tasks/main.yml @@ -0,0 +1,67 @@ +--- + +- name: "Install unzip" + ansible.builtin.include_tasks: install-unzip.yml + +- name: "Install curl" + ansible.builtin.include_tasks: install-curl.yml + +- name: "Add user rustdesk" + ansible.builtin.user: + name: "rustdesk" + shell: /usr/sbin/nologin + create_home: false + +# Rustdesk uses /opt/rustdesk-server/lib/ as a working directory. When it's first started it generates key pairs and puts them in lib directory +- name: "Create lib directory" + ansible.builtin.file: + path: /opt/rustdesk-server/lib/ + state: directory + mode: "0755" + owner: "rustdesk" + group: "rustdesk" + +- name: "Download rustdesk-client" + ansible.builtin.get_url: + url: "https://github.com/rustdesk/rustdesk/releases/download/{{ rustdesk_client_version }}/rustdesk-{{ rustdesk_client_version }}-x86_64.exe" + dest: /opt/rustdesk-server/lib/rustdesk-client.exe + mode: "0644" + owner: "rustdesk" + group: "rustdesk" + +- name: "Copy generate-rustdesk-exe.sh to target machine" + ansible.builtin.copy: + src: "generate-rustdesk-exe.sh" + dest: /opt/rustdesk-server/ + mode: "0755" + owner: "rustdesk" + group: "rustdesk" + +- name: "Install rustdesk" + block: + - name: "Check rustdesk version" + ansible.builtin.include_tasks: check-rustdesk-version.yml + rescue: + - name: "Download and unarchive the binaries" + ansible.builtin.unarchive: + src: "https://github.com/rustdesk/rustdesk-server/releases/download/{{ rustdesk_server_version }}/rustdesk-server-linux-amd64.zip" + dest: /opt/rustdesk-server/ + remote_src: true + +- name: "Copy rustdesk-hbbr.service to /etc/systemd/system/" + notify: "Start and enable rustdesk-hbbr.service" + ansible.builtin.copy: + src: "rustdesk-hbbr.service" + dest: /etc/systemd/system/ + mode: "0755" + owner: "rustdesk" + group: "rustdesk" + +- name: "Copy rustdesk-hbbs.service to /etc/systemd/system/" + notify: "Start and enable rustdesk-hbbs.service" + ansible.builtin.copy: + src: "rustdesk-hbbs.service" + dest: /etc/systemd/system/ + mode: "0755" + owner: "rustdesk" + group: "rustdesk" diff --git a/roles/rustdesk/vars/main.yml b/roles/rustdesk/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/rustdesk/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/sftp_share/README.md b/roles/sftp_share/README.md new file mode 100644 index 0000000..2a9c476 --- /dev/null +++ b/roles/sftp_share/README.md @@ -0,0 +1,73 @@ +SFTP Share Ansible Role +========= + +Creates a user account and directories for secured SFTP data exchange (one user = one share). + +The role creates a dedicated SSH config under `/etc/ssh/sshd_config.d/`, configuring: + +- chroot into the directory of the share +- limits commands to `internal-sftp` +- further controls SFTP permissions for the share + +Disk quota management is not included. + +Requirements +------------ + +None + +Role Variables +-------------- + +`sftp_username` — user account name on the sFTP server +`sftp_pubkey` — the public part of the SSH key the user will be using to connect to the server +`sftp_root` — directory to put the SFTP shares under +`sftp_transfers_groupname` — group owning `{{sftp_root}}/{{sftp_username}}-uploads/transfers` +`sftp_permissions` — list of permissions, see below for supported (default: `open,close,read,write,lstat,fstat,opendir,readdir,remove,mkdir,rmdir,realpath,rename`) + +List Of SFTP Operations +------------------------ +`open` — open a file for reading or writing +`close` — close an opened file +`read` — read data from a file +`write` — write data to a file +`lstat` — get file attributes without following symlinks +`fstat` — get attributes of an opened file +`setstat` — change file attributes (chmod, chown, etc.) +`fsetstat` — change attributes of an opened file +`opendir` — open a directory for reading +`readdir` — read the contents of a directory +`remove` — delete a file +`mkdir` — create a new directory +`rmdir` — remove an empty directory +`realpath` — resolve absolute path of a file or directory (inside chroot) +`stat` — get file attributes (follows symlinks) +`rename` — rename a file or directory +`readlink` — read the target of a symbolic link +`symlink` — create a symbolic link + +For example: + +- Default: `open,close,read,write,lstat,fstat,opendir,readdir,remove,mkdir,rmdir,realpath,rename` +- Read only: `open,close,read,lstat,fstat,opendir,readdir,realpath,stat` +- Required to establish an SFTP connection: `realpath` + +Dependencies +------------ + +None + +Example Playbook +---------------- + +See `molecule/default/converge.yml` for an example role invocation. + +License +------- + +BSD + +Author Information +------------------ + +malyuk.ss@genlab.llc diff --git a/roles/sftp_share/defaults/main.yml b/roles/sftp_share/defaults/main.yml new file mode 100644 index 0000000..44355ef --- /dev/null +++ b/roles/sftp_share/defaults/main.yml @@ -0,0 +1,2 @@ +--- +sftp_share_permissions: "open,close,read,write,lstat,fstat,opendir,readdir,remove,mkdir,rmdir,realpath,rename" diff --git a/roles/sftp_share/handlers/main.yml b/roles/sftp_share/handlers/main.yml new file mode 100644 index 0000000..46760ca --- /dev/null +++ b/roles/sftp_share/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Restart sshd + ansible.builtin.systemd_service: + name: ssh + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/sftp_share/meta/main.yml b/roles/sftp_share/meta/main.yml new file mode 100644 index 0000000..9d3f7e1 --- /dev/null +++ b/roles/sftp_share/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "sftp_share" + namespace: genlab + author: "Sergey Malyuk" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/sftp_share/molecule/default/converge.yml b/roles/sftp_share/molecule/default/converge.yml new file mode 100644 index 0000000..dee6099 --- /dev/null +++ b/roles/sftp_share/molecule/default/converge.yml @@ -0,0 +1,39 @@ +--- +- name: Converge + hosts: all + + pre_tasks: + - name: Create test group + ansible.builtin.group: + name: testgrp + state: present + + - name: Ensure OpenSSH server is installed + ansible.builtin.apt: + name: openssh-server + state: present + update_cache: true + + - name: Ensure the `/root/.ssh` directory exists + ansible.builtin.file: + state: directory + path: "/root/.ssh" + mode: "0700" + owner: root + group: root + + - name: Provide an SSH private key for testing purposes + ansible.builtin.copy: + src: "ssh/id_ed25519" + dest: "/root/.ssh/id_ed25519" + mode: "0600" + owner: root + group: root + + roles: + - role: genlab.common.sftp_share + sftp_username: "testusr" + sftp_pubkey: "{{ lookup('file', 'ssh/id_ed25519.pub') }}" + sftp_root: "/primary/data" + sftp_transfers_groupname: "testgrp" + sftp_share_permissions: "open,close,read,lstat,fstat,opendir,readdir,realpath,stat" # read-only diff --git a/roles/sftp_share/molecule/default/molecule.yml b/roles/sftp_share/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/sftp_share/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/sftp_share/molecule/default/ssh/id_ed25519 b/roles/sftp_share/molecule/default/ssh/id_ed25519 new file mode 100644 index 0000000..f18502b --- /dev/null +++ b/roles/sftp_share/molecule/default/ssh/id_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBSSKh3Oc0GgnvSzCmvloaoBVpa3/ZcEUhimzih81XMUAAAAJCpucRAqbnE +QAAAAAtzc2gtZWQyNTUxOQAAACBSSKh3Oc0GgnvSzCmvloaoBVpa3/ZcEUhimzih81XMUA +AAAEDKy8xJ6QdOj+DG1oromiQ0TmwnTWFcMwjUMdqIZFd8blJIqHc5zQaCe9LMKa+WhqgF +Wlrf9lwRSGKbOKHzVcxQAAAAC3Jvb3RAdWJ1bnR1AQI= +-----END OPENSSH PRIVATE KEY----- diff --git a/roles/sftp_share/molecule/default/ssh/id_ed25519.pub b/roles/sftp_share/molecule/default/ssh/id_ed25519.pub new file mode 100644 index 0000000..6c1eb0d --- /dev/null +++ b/roles/sftp_share/molecule/default/ssh/id_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFJIqHc5zQaCe9LMKa+WhqgFWlrf9lwRSGKbOKHzVcxQ root@ubuntu diff --git a/roles/sftp_share/molecule/default/verify.yml b/roles/sftp_share/molecule/default/verify.yml new file mode 100644 index 0000000..ebf4e65 --- /dev/null +++ b/roles/sftp_share/molecule/default/verify.yml @@ -0,0 +1,91 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + vars: + sftp_username: "testusr" + sftp_pubkey: "{{ lookup('file', 'ssh/id_ed25519.pub') }}" + sftp_root: "/primary/data" + sftp_transfers_groupname: "testgrp" + + tasks: + - name: Gather user SFTP directory info + ansible.builtin.stat: + path: "{{ sftp_root }}/{{ sftp_username }}-uploads" + register: sftp_share_permissions + + - name: Assert that user SFTP directory has correct owner + ansible.builtin.assert: + that: + - sftp_share_permissions.stat.pw_name == "root" + - sftp_share_permissions.stat.gr_name == "root" + success_msg: "SFTP directory {{ sftp_root }}/{{ sftp_username }}-uploads has correct owner root:root" + fail_msg: | + "SFTP directory {{ sftp_root }}/{{ sftp_username }}-uploads does not have correct owner root:root, + actual owner is {{ sftp_share_permissions.stat.pw_name }}:{{ sftp_share_permissions.stat.gr_name }}" + + - name: Gather transfers directory info + ansible.builtin.stat: + path: "{{ sftp_root }}/{{ sftp_username }}-uploads/transfers" + register: sftp_share_transfers_stat + + - name: Assert that transfers directory has correct owner + ansible.builtin.assert: + that: + - sftp_share_transfers_stat.stat.pw_name == sftp_username + - sftp_share_transfers_stat.stat.gr_name == sftp_transfers_groupname + success_msg: "Directory {{ sftp_root }}/{{ sftp_username }}-uploads/transfers has correct owner {{ sftp_username }}:{{ sftp_transfers_groupname }}" + fail_msg: | + "Directory {{ sftp_root }}/{{ sftp_username }}-uploads/transfers does not have correct owner {{ sftp_username }}:{{ sftp_transfers_groupname }}, + actual owner is {{ sftp_share_transfers_stat.stat.pw_name }}:{{ sftp_share_transfers_stat.stat.gr_name }}" + + - name: Assert that transfers directory has correct permissions + ansible.builtin.assert: + that: + - sftp_share_transfers_stat.stat.mode == "2770" + success_msg: "Directory {{ sftp_root }}/{{ sftp_username }}-uploads/transfers has correct permissions 2770" + fail_msg: | + "Directory {{ sftp_root }}/{{ sftp_username }}-uploads/transfers does not have correct permissions 2770, + actual permissions are {{ sftp_share_permissions.stat.mode }}" + + - name: Get user info + ansible.builtin.getent: + database: passwd + key: "{{ sftp_username }}" + register: sftp_share_user_info + + - name: Check user exists + ansible.builtin.assert: + that: + - sftp_share_user_info is defined + success_msg: "User {{ sftp_username }} exists" + fail_msg: "User {{ sftp_username }} does not exist" + + - name: Check that sshd refuses non-SFTP connection attempts + changed_when: false + failed_when: sftp_share_ssh.stdout != expected + register: sftp_share_ssh + vars: + expected: "This service allows sftp connections only." + ansible.builtin.command: "ssh -l {{ sftp_username }} 127.0.0.1 -o StrictHostKeyChecking=no exit" + + - name: Check that sFTP chroots into /transfers + changed_when: false + register: sftp_share_sftp + failed_when: sftp_share_sftp.stdout != expected + vars: + expected: "sftp> pwd\nRemote working directory: /transfers" + ansible.builtin.shell: + executable: /bin/bash + cmd: 'set -o pipefail; echo "pwd" | sftp -b - -o StrictHostKeyChecking=no {{ sftp_username | quote }}@127.0.0.1' + + - name: Check that non-whitelisted sFTP permissions are denied + changed_when: false + register: sftp_share_mkdir + failed_when: sftp_share_mkdir.stderr != expected + vars: + expected: 'remote mkdir "/transfers/foo": Permission denied' + ansible.builtin.shell: + executable: /bin/bash + cmd: 'set -o pipefail; echo "mkdir foo" | sftp -b - -o StrictHostKeyChecking=no {{ sftp_username | quote }}@127.0.0.1' diff --git a/roles/sftp_share/tasks/main.yml b/roles/sftp_share/tasks/main.yml new file mode 100644 index 0000000..f88a5a8 --- /dev/null +++ b/roles/sftp_share/tasks/main.yml @@ -0,0 +1,38 @@ +--- +- name: Create SFTP user - {{ sftp_username }} + ansible.builtin.user: + name: "{{ sftp_username }}" + state: present + create_home: true + shell: /sbin/nologin + +- name: Create SFTP user upload directory - {{ sftp_username }} + ansible.builtin.file: + state: directory + path: "{{ sftp_root }}/{{ sftp_username }}-uploads/" + mode: '0755' + owner: root + group: root + +- name: Create transfers directory - {{ sftp_username }} + ansible.builtin.file: + state: directory + path: "{{ sftp_root }}/{{ sftp_username }}-uploads/transfers/" + mode: '2770' + owner: "{{ sftp_username }}" + group: "{{ sftp_transfers_groupname }}" + +- name: Push sshd config - {{ sftp_username }} + ansible.builtin.template: + src: sshd_config.conf.j2 + dest: "/etc/ssh/sshd_config.d/60-sftp-jail-{{ sftp_username }}.conf" + mode: '0600' + owner: "root" + group: "root" + notify: Restart sshd + +- name: Push public ssh key - {{ sftp_username }} + ansible.posix.authorized_key: + user: "{{ sftp_username }}" + key: "{{ sftp_pubkey }}" + state: present diff --git a/roles/sftp_share/templates/sshd_config.conf.j2 b/roles/sftp_share/templates/sshd_config.conf.j2 new file mode 100644 index 0000000..f7dfdc8 --- /dev/null +++ b/roles/sftp_share/templates/sshd_config.conf.j2 @@ -0,0 +1,5 @@ +Match User {{ sftp_username }} + ChrootDirectory /primary/data/{{ sftp_username }}-uploads + ForceCommand internal-sftp -u 007 -p {{ sftp_share_permissions }} -d /transfers + AllowTcpForwarding no + X11Forwarding no diff --git a/roles/sftp_share/vars/main.yml b/roles/sftp_share/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/sftp_share/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/smartctl_exporter/.gitignore b/roles/smartctl_exporter/.gitignore new file mode 100644 index 0000000..8cd0df3 --- /dev/null +++ b/roles/smartctl_exporter/.gitignore @@ -0,0 +1,2 @@ +.vscode +.idea \ No newline at end of file diff --git a/roles/smartctl_exporter/README.md b/roles/smartctl_exporter/README.md new file mode 100644 index 0000000..1c1de90 --- /dev/null +++ b/roles/smartctl_exporter/README.md @@ -0,0 +1,56 @@ +genlab.smartctl-exporter +========= + +```smartctl``` is a command-line utility used to control and monitor the Self-Monitoring, Analysis and Reporting Technology (SMART) system in hard disk drives (HDDs) and solid-state drives (SSDs). ```smartctl-exporter``` exports ```smartctl``` statistics to Prometheus. + +Requirements +------------ + +smartctl + +Role Variables +-------------- +The exporter will scan the system for available devices if no ```--smartctl.device``` flags are used. The format of web.config file id described [here](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md). If ```smartctl_exp_source_web_config_dir``` is defined, role searches for ```web_smartctl.conf``` file and copy it to target host to ```smartctl_exp_web_config_dir``` directory +```yaml +--- +smartctl_exp_port: "localhost:9633" # Addresses on which to expose metrics and web interface. Repeatable for multiple addresses. +smartctl_exp_version: "0.14.0" # exporter version to install +smartctl_exp_dir: "/etc/exporters" # where to download and unarchive expoter +smartctl_exp_web_config_dir: "/etc/exporters/config" # Path to configuration file that can enable TLS or authentication +smartctl_exp_interval: "60s" # The interval between smartctl polls +smartctl_exp_rescan: "10m" # The interval between rescanning for new/disappeared devices. If the interval is smaller than 1s no rescanning takes place. If any devices are configured with smartctl.device also no rescanning takes place. +smartctl_exp_devices: [] # The device to monitor (repeatable) +smartctl_exp_device_exclude: "" #Regexp of devices to exclude from automatic scanning. (mutually exclusive to device-include) +smartctl_exp_device_include: "" # Regexp of devices to include in automatic scanning. (mutually exclusive to device-exclude) +smartctl_exp_web_telemetry_path: "/metrics" # Path under which to expose metrics +smartctl_exp_log_level: "info" # Only log messages with the given severity or above. One of: [debug, info, warn, error] +smartctl_exp_log_format: "logfmt" # Output format of log messages. One of: [logfmt, json] +smartctl_exp_args: # --version --web.systemd-socket + +smartctl_exp_source_web_config_dir: "" +``` +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: +- role: genlab.smartctl_exporter + smartctl_exp_version: "0.14.0" + smartctl_exp_dir: "/etc/exporters" + smartctl_exp_config_dir: "/etc/exporters/config" +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/smartctl_exporter/defaults/main.yml b/roles/smartctl_exporter/defaults/main.yml new file mode 100644 index 0000000..8c146b9 --- /dev/null +++ b/roles/smartctl_exporter/defaults/main.yml @@ -0,0 +1,14 @@ +--- +smartctl_exporter_web_listen_address: "localhost:9633" +smartctl_exporter_args: "" +smartctl_exporter_version: "0.14.0" +smartctl_exporter_dir: "/etc/exporters" +smartctl_exporter_web_config_dir: "/etc/exporters/config" +smartctl_exporter_interval: "60s" +smartctl_exporter_rescan: "10m" +smartctl_exporter_devices: [] +smartctl_exporter_device_exclude: "" +smartctl_exporter_device_include: "" +smartctl_exporter_web_telemetry_path: "/metrics" +smartctl_exporter_log_level: "info" +smartctl_exporter_log_format: "logfmt" diff --git a/roles/smartctl_exporter/handlers/main.yml b/roles/smartctl_exporter/handlers/main.yml new file mode 100644 index 0000000..f9173f1 --- /dev/null +++ b/roles/smartctl_exporter/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "(Re)start and enable smartctl_exporter" + ansible.builtin.systemd_service: + name: smartctl_exporter.service + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/smartctl_exporter/meta/main.yml b/roles/smartctl_exporter/meta/main.yml new file mode 100644 index 0000000..cde1c90 --- /dev/null +++ b/roles/smartctl_exporter/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "smartctl_exporter" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/smartctl_exporter/molecule/default/converge.yml b/roles/smartctl_exporter/molecule/default/converge.yml new file mode 100644 index 0000000..fcbdf59 --- /dev/null +++ b/roles/smartctl_exporter/molecule/default/converge.yml @@ -0,0 +1,7 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.smartctl_exporter + smartctl_exporter_dir: "/etc/exporters" + smartctl_exp_config_dir: "/etc/exporters/config" diff --git a/roles/smartctl_exporter/molecule/default/molecule.yml b/roles/smartctl_exporter/molecule/default/molecule.yml new file mode 100644 index 0000000..d82158e --- /dev/null +++ b/roles/smartctl_exporter/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/smartctl_exporter/molecule/default/verify.yml b/roles/smartctl_exporter/molecule/default/verify.yml new file mode 100644 index 0000000..de66787 --- /dev/null +++ b/roles/smartctl_exporter/molecule/default/verify.yml @@ -0,0 +1,37 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + tasks: + - name: "Include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + - name: "Check if smartctl_exporter is installed" + changed_when: false + ansible.builtin.command: "smartctl_exporter --version" + register: smartctl_exporter_installed_version + + - name: "Check smartctl_exporter version" + ansible.builtin.assert: + that: "smartctl_exporter_installed_version.stderr is regex('{{ smartctl_exporter_version }}')" + success_msg: "smartctl_exporter version {{ smartctl_exporter_version }} is installed and working" + fail_msg: "smartctl_exporter version {{ smartctl_exporter_version }} is not installed or not working correctly" + + # kics-scan ignore-block + - name: "Check if /metrics endpoint is reachable" + ansible.builtin.uri: + url: "http://{{ smartctl_exporter_web_listen_address }}/metrics" + return_content: true + status_code: 200 + timeout: 5 + register: smartctl_exporter_metrics_check + + - name: "Fail if /metrics doesn't contain smartctl_exporter_build_info line" + ansible.builtin.assert: + that: "'smartctl_exporter_build_info' in smartctl_exporter_metrics_check.content" + fail_msg: "smartctl_exporter /metrics endpoint doesn't contain smartctl_exporter_build_info line!" + success_msg: "smartctl_exporter /metrics endpoint contains smartctl_exporter_build_info line!" diff --git a/roles/smartctl_exporter/tasks/configure.yml b/roles/smartctl_exporter/tasks/configure.yml new file mode 100644 index 0000000..61ef10c --- /dev/null +++ b/roles/smartctl_exporter/tasks/configure.yml @@ -0,0 +1,10 @@ +--- +- name: "Upload smartctl_exporter local configure file" + notify: "(Re)start and enable smartctl_exporter" + when: smartctl_exp_source_web_config_dir is defined and smartctl_exp_source_web_config_dir | length > 0 + ansible.builtin.template: + src: "{{ smartctl_exp_source_web_config_dir }}/web_smartctl.conf" + dest: "{{ smartctl_exporter_web_config_dir }}/web_smartctl.conf" + owner: root + group: root + mode: '0640' diff --git a/roles/smartctl_exporter/tasks/install.yml b/roles/smartctl_exporter/tasks/install.yml new file mode 100644 index 0000000..3ddaafc --- /dev/null +++ b/roles/smartctl_exporter/tasks/install.yml @@ -0,0 +1,51 @@ +--- +- name: "Install smartctl_exporter from binary" + block: + - name: "Check smartctl_exporter version" + changed_when: false + ansible.builtin.command: + cmd: "smartctl_exporter --version" + register: smartctl_exporter_ver + + - name: "Assert version correctness" + ansible.builtin.assert: + that: "smartctl_exporter_ver.stderr is regex('{{ smartctl_exporter_version }}')" + success_msg: "smartctl_exporter version {{ smartctl_exporter_version }} is installed and working" + fail_msg: "smartctl_exporter version {{ smartctl_exporter_version }} is not installed or not working correctly" + + rescue: + - name: "Create smartctl_exporter directories" + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + with_items: + - "{{ smartctl_exporter_dir }}" + - "{{ smartctl_exp_config_dir }}" + + - name: "Unarchive smartctl_exporter tar file" + notify: "(Re)start and enable smartctl_exporter" + ansible.builtin.unarchive: + src: "https://github.com/prometheus-community/smartctl_exporter/releases/\ + download/v{{ smartctl_exporter_version }}/smartctl_exporter-{{ smartctl_exporter_version }}.linux-amd64.tar.gz" + dest: "{{ smartctl_exporter_dir }}" + remote_src: true + + - name: "Move smartctl_exporter binary" + ansible.builtin.copy: + src: "{{ smartctl_exporter_dir }}/smartctl_exporter-{{ smartctl_exporter_version }}.linux-amd64/smartctl_exporter" + dest: "/usr/local/bin/smartctl_exporter" + mode: "0755" + owner: root + group: root + remote_src: true + +- name: Create systemd service file + ansible.builtin.template: + src: smartctl_exporter.service.j2 + dest: /etc/systemd/system/smartctl_exporter.service + owner: root + group: root + mode: '0644' diff --git a/roles/smartctl_exporter/tasks/main.yml b/roles/smartctl_exporter/tasks/main.yml new file mode 100644 index 0000000..76dd507 --- /dev/null +++ b/roles/smartctl_exporter/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: "Include installation tasks" + ansible.builtin.include_tasks: "install.yml" + +- name: "Including configuration tasks" + ansible.builtin.include_tasks: "configure.yml" diff --git a/roles/smartctl_exporter/templates/smartctl_exporter.service.j2 b/roles/smartctl_exporter/templates/smartctl_exporter.service.j2 new file mode 100644 index 0000000..abe2a5b --- /dev/null +++ b/roles/smartctl_exporter/templates/smartctl_exporter.service.j2 @@ -0,0 +1,46 @@ +[Unit] +Description=Smartctl Exporter +Documentation=https://github.com/prometheus-community/smartctl_exporter +After=network.target +StartLimitIntervalSec=120 +StartLimitBurst=5 + +[Service] +Type=simple +User=root +Group=root +ExecStart=/usr/local/bin/smartctl_exporter \ + --web.listen-address={{ smartctl_exporter_web_listen_address }} \ + --smartctl.interval={{ smartctl_exporter_interval }} \ + --smartctl.rescan={{ smartctl_exporter_rescan }} \ +{% if smartctl_exporter_devices is defined and smartctl_exporter_devices | length > 0 %} + {% for device in smartctl_exporter_devices %} + --smartctl.device={{ device }} \ + {% endfor %} +{% endif %} + --smartctl.device-exclude={{ smartctl_exporter_device_exclude }} \ + --smartctl.device-include={{ smartctl_exporter_device_include }} \ + --web.telemetry-path={{ smartctl_exporter_web_telemetry_path }} \ + --log.level={{ smartctl_exporter_log_level }} \ + --log.format={{ smartctl_exporter_log_format }} \ +{% if smartctl_exporter_args is defined and smartctl_exporter_args | length > 0 %} + {{ smartctl_exporter_args }} \ +{% endif %} +{% if smartctl_exp_source_web_config_dir is defined and smartctl_exp_source_web_config_dir | length > 0 %} \ + --web.config.file={{ smartctl_exporter_web_config_dir }}/web_smartctl.conf +{% endif %} + +SyslogIdentifier=smartctl_exporter +Restart=on-failure +RestartSec=5 + +ProtectHome=yes +NoNewPrivileges=yes +ProtectSystem=strict +ProtectControlGroups=true +ProtectKernelModules=true +ProtectKernelTunables=yes +ProtectSystem=full + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/roles/smartctl_exporter/vars/main.yml b/roles/smartctl_exporter/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/smartctl_exporter/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/ufw/README.md b/roles/ufw/README.md new file mode 100644 index 0000000..7207f41 --- /dev/null +++ b/roles/ufw/README.md @@ -0,0 +1,37 @@ +template +========= + +Whitelist network ports with UFW + +Requirements +------------ + +None + +Role Variables +-------------- + +None + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.ufw +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/ufw/defaults/main.yml b/roles/ufw/defaults/main.yml new file mode 100644 index 0000000..702f638 --- /dev/null +++ b/roles/ufw/defaults/main.yml @@ -0,0 +1,4 @@ +--- +ufw_rules: [] +ufw_limit_ssh: false +ufw_openssh_port: 22 diff --git a/roles/ufw/handlers/main.yml b/roles/ufw/handlers/main.yml new file mode 100644 index 0000000..802d7ec --- /dev/null +++ b/roles/ufw/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: "Reload-ufw" + community.general.ufw: + state: reloaded diff --git a/roles/ufw/meta/main.yml b/roles/ufw/meta/main.yml new file mode 100644 index 0000000..004334a --- /dev/null +++ b/roles/ufw/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: ufw + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "Whitelist network ports with UFW" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/ufw/molecule/default/converge.yml b/roles/ufw/molecule/default/converge.yml new file mode 100644 index 0000000..ddd580c --- /dev/null +++ b/roles/ufw/molecule/default/converge.yml @@ -0,0 +1,17 @@ +--- +- name: Converge + hosts: all + vars: + custom_rules: + - port: 80 + - port: 9080 + src: "10.2.1.0/24" + - interface: eth0@if288 + direction: in + comment: "Allow all incoming traffic on eth0@if288" + + roles: + - role: genlab.common.ufw + disable_ipv6: true + ufw_limit_ssh: true + ufw_rules: "{{ custom_rules }}" diff --git a/roles/ufw/molecule/default/molecule.yml b/roles/ufw/molecule/default/molecule.yml new file mode 100644 index 0000000..fd2f06d --- /dev/null +++ b/roles/ufw/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/ufw/molecule/default/verify.yml b/roles/ufw/molecule/default/verify.yml new file mode 100644 index 0000000..3130f87 --- /dev/null +++ b/roles/ufw/molecule/default/verify.yml @@ -0,0 +1,28 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + tasks: + - name: "Get the UFW status" + register: ufw_status + changed_when: false + ansible.builtin.command: + cmd: ufw status + + - name: "Verify expected UFW status" + vars: + expected: + - "Status: active" + - "" + - "To Action From" + - "-- ------ ----" + - "22/tcp LIMIT Anywhere " + - "80 ALLOW Anywhere " + - "9080 ALLOW 10.2.1.0/24 " + - "Anywhere on eth0@if288 ALLOW Anywhere # Allow all incoming traffic on eth0@if288" + ansible.builtin.assert: + that: ufw_status.stdout_lines == expected + success_msg: "UFW has the expected state" + fail_msg: "Unexpected UFW state (some rules may have not been applied correctly)" diff --git a/roles/ufw/tasks/main.yml b/roles/ufw/tasks/main.yml new file mode 100644 index 0000000..30885d3 --- /dev/null +++ b/roles/ufw/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- name: "Ensure that ufw is installed" + ansible.builtin.apt: + name: ufw + update_cache: true + +- name: "Disable IPv6" + when: (disable_ipv6 is defined) and (disable_ipv6 is true) + ansible.builtin.lineinfile: + path: /etc/default/ufw + regexp: ^IPV6 + line: IPV6=no + +- name: "Deny incoming connections" + notify: Reload-ufw + community.general.ufw: + direction: incoming + proto: any + policy: deny + +- name: "Allow outgoing connections" + notify: Reload-ufw + community.general.ufw: + direction: outgoing + proto: any + policy: allow + +- name: "Allow SSH access" + notify: Reload-ufw + community.general.ufw: + rule: "{{ ufw_limit_ssh | ternary('limit', 'allow') }}" + port: "{{ ufw_openssh_port }}" + proto: tcp + +- name: "Set whitelist rules" + notify: Reload-ufw + loop: "{{ ufw_rules }}" + community.general.ufw: + rule: "{{ item.rule | default('allow') }}" + comment: "{{ item.comment | default(omit) }}" + port: "{{ item.port | default(omit) }}" + proto: "{{ item.proto | default('any') }}" + src: "{{ item.src | default('any') }}" + dest: "{{ item.dest | default(omit) }}" + interface: "{{ item.interface | default(omit) }}" + direction: "{{ item.direction | default(omit) }}" + route: "{{ item.route | default(false) }}" + +- name: "Enable the ufw service" + community.general.ufw: + state: enabled + +- name: "Flush handlers" + ansible.builtin.meta: flush_handlers diff --git a/roles/ufw/vars/main.yml b/roles/ufw/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/ufw/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/wg_hub/README.md b/roles/wg_hub/README.md new file mode 100644 index 0000000..fe551c0 --- /dev/null +++ b/roles/wg_hub/README.md @@ -0,0 +1,43 @@ +template +========= + +Template for Ansible role monorepos. + +⚠️ Do not forget to update: + +- `meta/main.yml` +- Conda/Mamba manifests +- this README =) + +Requirements +------------ + +None + +Role Variables +-------------- + +None + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.template +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/wg_hub/defaults/main.yml b/roles/wg_hub/defaults/main.yml new file mode 100644 index 0000000..7bf9b28 --- /dev/null +++ b/roles/wg_hub/defaults/main.yml @@ -0,0 +1,7 @@ +--- +wg_hub_iface_name: wg0 +wg_hub_host_id: hub +wg_hub_ipv4_vpn_addr: 10.0.0.254 +wg_hub_ipv4_vpn_cidr: 24 +wg_hub_wg_port: 51820 +wg_hub_hide_secrets: true diff --git a/roles/wg_hub/handlers/main.yml b/roles/wg_hub/handlers/main.yml new file mode 100644 index 0000000..ff2cedb --- /dev/null +++ b/roles/wg_hub/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "Run the Wireguard service" + ansible.builtin.systemd_service: + name: wg-quick@{{ wg_hub_iface_name | replace("-", "_") }} + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/wg_hub/meta/main.yml b/roles/wg_hub/meta/main.yml new file mode 100644 index 0000000..e2da64a --- /dev/null +++ b/roles/wg_hub/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "wg_hub" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/wg_hub/molecule/default/converge.yml b/roles/wg_hub/molecule/default/converge.yml new file mode 100644 index 0000000..e807dae --- /dev/null +++ b/roles/wg_hub/molecule/default/converge.yml @@ -0,0 +1,24 @@ +--- +- name: Converge + hosts: all + vars: + wg_hub_iface_name: adm + wg_hub_ipv4_vpn_addr: 10.0.0.254 + wg_hub_ipv4_vpn_cidr: 24 + wg_hub_wg_port: 51820 + wg_hub_wg_pkey: gABk6e/n3UDgudEVlUrEVcVdn0tc0YVDeCASsI10QFA= + wg_hub_wg_pubkey: EvcoZ21/p0AHz5y95jwjVIR9puljc2kXqh/f3U1A4nk= + wg_hub_hide_secrets: false + # dns_server: 127.0.0.1:5300 # doesn't work in Github Actions environment + peers: + - wg_hub_host_id: alpha + wg_hub_ipv4_vpn_addr: 10.0.0.1 + wg_hub_wg_pubkey: LfEJgNiJ05nx4nWB0Pj3wS3WRyFq567fsdDh4XZqRF0= + wg_psk: i3JCrQOfptZhgpL+BTm/65MPW/ljJexCgiuWMIqZJYo= + - wg_hub_host_id: beta + wg_hub_ipv4_vpn_addr: 10.0.0.2 + wg_hub_wg_pubkey: oq3Fcwwfxsi5f5UHcZKtxMwQ2aSeHOUe3r35soUUYzU= + wg_psk: Z/z7Qo8hW97UcImYE/ZbCxpNmizfVhvl0dzygtvtYYg= + + roles: + - role: genlab.common.wg_hub diff --git a/roles/wg_hub/molecule/default/molecule.yml b/roles/wg_hub/molecule/default/molecule.yml new file mode 100644 index 0000000..fd2f06d --- /dev/null +++ b/roles/wg_hub/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/wg_hub/molecule/default/verify.yml b/roles/wg_hub/molecule/default/verify.yml new file mode 100644 index 0000000..38f93ff --- /dev/null +++ b/roles/wg_hub/molecule/default/verify.yml @@ -0,0 +1,13 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + vars: + iface_name: adm + + tasks: + - name: "Check that the Wireguard service is running" + ansible.builtin.service: + name: wg-quick@{{ iface_name }} + state: started diff --git a/roles/wg_hub/tasks/main.yml b/roles/wg_hub/tasks/main.yml new file mode 100644 index 0000000..78551ea --- /dev/null +++ b/roles/wg_hub/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- name: "Install wireguard system-wide" + ansible.builtin.apt: + name: wireguard + state: present + update_cache: true + cache_valid_time: 3600 + +- name: "Ensure no dashes in the interface name" + when: "'-' in wg_hub_iface_name" + ansible.builtin.fail: + msg: "The interface name must not contain dashes, got: '{{ wg_hub_iface_name }}'" + +- name: "Create the Hub configuration file" + no_log: "{{ wg_hub_hide_secrets }}" + notify: "Run the Wireguard service" + ansible.builtin.blockinfile: + path: "/etc/wireguard/{{ wg_hub_iface_name }}.conf" + create: true + owner: root + group: root + mode: "0600" + state: present + block: | + [Interface] + Address = {{ wg_hub_ipv4_vpn_addr }}/32 + ListenPort = {{ wg_hub_wg_port }} + PrivateKey = {{ wg_hub_wg_pkey }} + + PreUp = sysctl -w net.ipv4.ip_forward=1 + {% if dns_server is defined %} + PostUp = resolvectl dns %i {{ dns_server }}; resolvectl domain %i {{ wg_hub_iface_name }}.local + {% endif %} + PostDown = sysctl -w net.ipv4.ip_forward=0 + +- name: "Add [Peer] sections to the Hub configuration file" + no_log: "{{ wg_hub_hide_secrets }}" + notify: "Run the Wireguard service" + loop: "{{ peers }}" + vars: + domain_name: "{{ item.wg_hub_host_id }}.{{ wg_hub_iface_name }}.local" + ansible.builtin.blockinfile: + path: "/etc/wireguard/{{ wg_hub_iface_name }}.conf" + owner: root + group: root + mode: "0600" + marker: "# {mark} ANSIBLE MANAGED SPOKE BLOCK: {{ domain_name }}" + block: | + # + [Peer] # {{ domain_name }} + PublicKey = {{ item.wg_hub_wg_pubkey }} + PresharedKey = {{ item.wg_psk }} + AllowedIPs = {{ item.wg_hub_ipv4_vpn_addr }}/32 + # diff --git a/roles/wg_hub/vars/main.yml b/roles/wg_hub/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/wg_hub/vars/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/wg_spoke/README.md b/roles/wg_spoke/README.md new file mode 100644 index 0000000..c0ba380 --- /dev/null +++ b/roles/wg_spoke/README.md @@ -0,0 +1,46 @@ +wg_spoke +========= + +Configure the host as a Spoke in Wireguard Spoke&Hub topology. + +Requirements +------------ + +Ubuntu 20.04, 22.04 or 24.04. + +Role Variables +-------------- + +```yaml +iface_name: name of the Wireguard interface (network), config file is named after it; WARNING, dashes are not supported +spoke_ipv4_vpn: IPv4 address that this Spoke should expect on the VPN +subnet: something like "10.0.0.0" +netmask: CIDR-style netmask without a slash, e.g. "24" +spoke_pkey: Spoke private key (Wireguard-generated, base64) +hub_pubkey: Hub public key (Wireguard-generated, base64) +hub_ipv4_wan: WAN (Internet) IPv4 address of the Hub +hub_ipv4_vpn: IPv4 address of the Hub on the VPN +hub_port: Hub should be listening on this port +wg_spoke_hide_secrets: whether or not prevent Ansible from logging tasks that handle sensitive info like keys (default is `true`) +wg_spoke_logging: whether or not to send Wireguard dyndbg logs to the kernel buffer accesible via journalctl -ek (default is `true`) +``` + +Dependencies +------------ + +None + +Example Playbook +---------------- + +See `molecule/default/converge.yml`. + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/wg_spoke/defaults/main.yml b/roles/wg_spoke/defaults/main.yml new file mode 100644 index 0000000..3cb9c8b --- /dev/null +++ b/roles/wg_spoke/defaults/main.yml @@ -0,0 +1,3 @@ +--- +wg_spoke_hide_secrets: true +wg_spoke_logging: true diff --git a/roles/wg_spoke/handlers/main.yml b/roles/wg_spoke/handlers/main.yml new file mode 100644 index 0000000..7372f6c --- /dev/null +++ b/roles/wg_spoke/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "Run the Wireguard service" + ansible.builtin.systemd_service: + name: wg-quick@{{ iface_name }} + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/wg_spoke/meta/main.yml b/roles/wg_spoke/meta/main.yml new file mode 100644 index 0000000..396d62a --- /dev/null +++ b/roles/wg_spoke/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: "wg_spoke" + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["focal", "jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/wg_spoke/molecule/default/converge.yml b/roles/wg_spoke/molecule/default/converge.yml new file mode 100644 index 0000000..cc5f583 --- /dev/null +++ b/roles/wg_spoke/molecule/default/converge.yml @@ -0,0 +1,20 @@ +--- +- name: Converge + hosts: all + roles: + - role: genlab.common.wg_spoke + iface_name: adm + subnet: "10.0.0.0" + netmask: "24" + spoke_ipv4_vpn: "10.0.0.1" + spoke_port: "51820" + spoke_pkey: eGqGx4A5ufFKatLflPFKNuzFgGuospIe08iPbTlEhm0= + hub_pubkey: EvcoZ21/p0AHz5y95jwjVIR9puljc2kXqh/f3U1A4nk= + # hub_psk: 5qmMuLuGQT7w8VnHPsZDO4vRF5wEKcpEtaCQW1BLmbg= + hub_ipv4_wan: "1.2.3.4" + hub_ipv4_vpn: "10.0.0.254" + hub_port: "51820" + dns_server: "10.0.0.254:5300" + domain: "adm.local" + wg_spoke_hide_secrets: false + wg_spoke_logging: false # doesn't work in Github Actions environment diff --git a/roles/wg_spoke/molecule/default/molecule.yml b/roles/wg_spoke/molecule/default/molecule.yml new file mode 100644 index 0000000..fd2f06d --- /dev/null +++ b/roles/wg_spoke/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/wg_spoke/molecule/default/verify.yml b/roles/wg_spoke/molecule/default/verify.yml new file mode 100644 index 0000000..38f93ff --- /dev/null +++ b/roles/wg_spoke/molecule/default/verify.yml @@ -0,0 +1,13 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + vars: + iface_name: adm + + tasks: + - name: "Check that the Wireguard service is running" + ansible.builtin.service: + name: wg-quick@{{ iface_name }} + state: started diff --git a/roles/wg_spoke/tasks/deploy.yml b/roles/wg_spoke/tasks/deploy.yml new file mode 100644 index 0000000..faa60ad --- /dev/null +++ b/roles/wg_spoke/tasks/deploy.yml @@ -0,0 +1,41 @@ +--- +- name: "Install Wireguard system-wide" + ansible.builtin.apt: + name: wireguard + state: present + update_cache: true + cache_valid_time: 3600 + +- name: "Ensure no dashes in the interface name" + when: "'-' in iface_name" + ansible.builtin.fail: + msg: "The interface name must not contain dashes, got: '{{ iface_name }}'" + +- name: "Create a Spoke configuration file" + no_log: "{{ wg_spoke_hide_secrets }}" + notify: "Run the Wireguard service" + ansible.builtin.blockinfile: + path: "/etc/wireguard/{{ iface_name }}.conf" + create: true + owner: root + group: root + mode: "0600" + state: present + block: | + [Interface] # Local settings for this node + Address = {{ spoke_ipv4_vpn }}/32 + ListenPort = {{ spoke_port }} + PrivateKey = {{ spoke_pkey }} + + {% if dns_server is defined and domain is defined %} + PostUp = resolvectl dns %i {{ dns_server }}; resolvectl domain %i {{ domain }} + {% endif %} + + [Peer] # Settings for the Hub peer + PublicKey = {{ hub_pubkey }} + {% if spoke_psk is defined %} + PresharedKey = {{ spoke_psk }} + {% endif %} + AllowedIPs = {{ subnet }}/{{ netmask }} + Endpoint = {{ hub_ipv4_wan }}:{{ hub_port }} + PersistentKeepalive = 20 diff --git a/roles/wg_spoke/tasks/logging.yml b/roles/wg_spoke/tasks/logging.yml new file mode 100644 index 0000000..8989ae4 --- /dev/null +++ b/roles/wg_spoke/tasks/logging.yml @@ -0,0 +1,11 @@ +--- +# SRC https://www.procustodibus.com/blog/2021/03/wireguard-logs/ +- name: "Add wireguard module" + community.general.modprobe: + name: wireguard + state: present + +- name: "Turn on Wireguard dyndbg logging into kernel message buffer" + changed_when: false + ansible.builtin.shell: + cmd: echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control diff --git a/roles/wg_spoke/tasks/main.yml b/roles/wg_spoke/tasks/main.yml new file mode 100644 index 0000000..d25fec3 --- /dev/null +++ b/roles/wg_spoke/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: "Include Wireguard deployment tasks" + ansible.builtin.include_tasks: + file: "tasks/deploy.yml" + +- name: "Include Wireguard logging configuration tasks" + when: wg_spoke_logging is true + ansible.builtin.include_tasks: + file: "tasks/logging.yml" diff --git a/roles/wg_spoke/vars/main.yml b/roles/wg_spoke/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/wg_spoke/vars/main.yml @@ -0,0 +1 @@ +---