diff --git a/.github/workflows/molecule-resolved.yml b/.github/workflows/molecule-resolved.yml new file mode 100644 index 0000000..766ca23 --- /dev/null +++ b/.github/workflows/molecule-resolved.yml @@ -0,0 +1,45 @@ +--- + +name: molecule-resolved + +on: + pull_request: + paths: + - .config/molecule + - .github/workflows/molecule-resolved.yml + - roles/systemd_resolved + push: + branches: + - main + - wip/next + paths: + - .config/molecule + - .github/workflows/molecule-resolved.yml + - roles/systemd_resolved + +jobs: + + molecule: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + distro: + - archlinux + scenario: + - default + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip3 install ansible molecule molecule-plugins[docker] docker + - run: ansible --version + - run: molecule --version + - run: molecule test -p ${{ matrix.distro }} -s ${{ matrix.scenario }} + working-directory: ./roles/systemd_resolved + env: + ANSIBLE_DIFF_ALWAYS: 'True' + PY_COLORS: '1' + +... diff --git a/README.md b/README.md index 755a1d1..7bda255 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,4 @@ - [idiv_biodiversity.systemd.systemd_journald](roles/systemd_journald/README.md) - [idiv_biodiversity.systemd.systemd_networkd](roles/systemd_networkd/README.md) +- [idiv_biodiversity.systemd.systemd_resolved](roles/systemd_resolved/README.md) diff --git a/roles/systemd_resolved/README.md b/roles/systemd_resolved/README.md new file mode 100644 index 0000000..aa14b0a --- /dev/null +++ b/roles/systemd_resolved/README.md @@ -0,0 +1,116 @@ +Ansible Role: systemd_resolved +============================== + +An Ansible role that configures **systemd-resolved**. + +Table of Contents +----------------- + + + +- [Role Variables](#role-variables) +- [Dependencies](#dependencies) +- [Example Playbook](#example-playbook) + * [Top-Level Playbook](#top-level-playbook) + * [Role Dependency](#role-dependency) + + + +Role Variables +-------------- + +For a detailed description see `man 5 resolved.conf`. + +Define your DNS servers: + +```yml +systemd_resolved_servers: + - a.b.c.1 + - a.b.c.2 + +systemd_resolved_fallback_servers: + - d.e.f.1 + - d.e.f.2 +``` + +Define your domains: + +```yml +systemd_resolved_domains: + - example.com +``` + +Other variables in the order they show up along with their default values: + +```yml +systemd_resolved_dnssec: no +systemd_resolved_dns_over_tls: no +systemd_resolved_multicast_dns: yes +systemd_resolved_llmnr: yes +systemd_resolved_cache: yes +systemd_resolved_cache_from_localhost: no +systemd_resolved_dns_stub_listener: yes +systemd_resolved_dns_stub_listener_extra: '' +systemd_resolved_read_etc_hosts: yes +systemd_resolved_resolve_unicast_single_label: no +systemd_resolved_stale_retention_sec: 0 +``` + + +Dependencies +------------ + +```yml +--- + +# requirements.yml + +collections: + + - name: ansible.posix + + - name: idiv_biodiversity.systemd + version: X.Y.Z + +... +``` + + +Example Playbook +---------------- + +### Top-Level Playbook + +Write a top-level playbook: + +```yml +--- + +- name: head server + hosts: head + + roles: + - role: idiv_biodiversity.systemd.systemd_resolved + tags: + - systemd + - systemd-resolved + +... +``` + +### Role Dependency + +Define the role dependency in `meta/main.yml`: + +```yml +--- + +dependencies: + + - role: idiv_biodiversity.systemd.systemd_resolved + tags: + - systemd + - systemd-resolved + +... +``` diff --git a/roles/systemd_resolved/meta/main.yml b/roles/systemd_resolved/meta/main.yml new file mode 100644 index 0000000..533a456 --- /dev/null +++ b/roles/systemd_resolved/meta/main.yml @@ -0,0 +1,20 @@ +--- + +galaxy_info: + author: Christian Krause + description: install and configure systemd-resolved + company: German Centre for Integrative Biodiversity Research (iDiv) + license: MIT + min_ansible_version: '2.9' + + platforms: + + - name: ArchLinux + versions: + - all + + galaxy_tags: + - systemd + - resolved + +... diff --git a/roles/systemd_resolved/molecule/default/converge.yml b/roles/systemd_resolved/molecule/default/converge.yml new file mode 100644 index 0000000..05420a2 --- /dev/null +++ b/roles/systemd_resolved/molecule/default/converge.yml @@ -0,0 +1,24 @@ +--- + +- name: converge + hosts: all + + pre_tasks: + + - name: update package cache + ansible.builtin.package: + update_cache: yes + become: yes + changed_when: no + register: __update_package_cache + until: __update_package_cache is success + retries: 10 + delay: 2 + + tasks: + + - name: include the role + ansible.builtin.include_role: + name: idiv_biodiversity.systemd.systemd_resolved + +... diff --git a/roles/systemd_resolved/molecule/default/molecule.yml b/roles/systemd_resolved/molecule/default/molecule.yml new file mode 100644 index 0000000..c81cf5b --- /dev/null +++ b/roles/systemd_resolved/molecule/default/molecule.yml @@ -0,0 +1,3 @@ +--- + +... diff --git a/roles/systemd_resolved/molecule/default/verify.yml b/roles/systemd_resolved/molecule/default/verify.yml new file mode 100644 index 0000000..0e66827 --- /dev/null +++ b/roles/systemd_resolved/molecule/default/verify.yml @@ -0,0 +1,99 @@ +--- + +- name: verify + hosts: all + tasks: + + # ------------------------------------------------------------------------- + # check package + # ------------------------------------------------------------------------- + + - name: check package installation + ansible.builtin.package: + name: systemd + state: present + check_mode: yes + register: __systemd_resolved_installation + + - name: debug package installation + ansible.builtin.debug: + var: __systemd_resolved_installation + + - name: assert on package installation + ansible.builtin.assert: + that: + - not __systemd_resolved_installation.failed + - not __systemd_resolved_installation.changed + success_msg: 'package is installed' + + # ------------------------------------------------------------------------- + # check configuration + # ------------------------------------------------------------------------- + + - name: check configuration file + ansible.builtin.stat: + path: /etc/systemd/resolved.conf.d/60-ansible.conf + get_attributes: no + get_checksum: no + get_mime: no + check_mode: yes + register: __systemd_resolved_configuration + + - name: debug configuration file + ansible.builtin.debug: + var: __systemd_resolved_configuration + + - name: assert on configuration file + ansible.builtin.assert: + that: + - __systemd_resolved_configuration.stat.exists + success_msg: 'configuration file exists' + + # ------------------------------------------------------------------------- + # check service + # ------------------------------------------------------------------------- + + - name: check service + ansible.builtin.service: + name: systemd-resolved + enabled: yes + state: started + check_mode: yes + register: __systemd_resolved_service + + - name: debug service + ansible.builtin.debug: + var: __systemd_resolved_service + + - name: assert on service + ansible.builtin.assert: + that: + - not __systemd_resolved_service.failed + - not __systemd_resolved_service.changed + - __systemd_resolved_service.state == 'started' + - __systemd_resolved_service.enabled + success_msg: 'service is both enabled and started' + + # ------------------------------------------------------------------------- + # check commands + # ------------------------------------------------------------------------- + + - name: 'check if `resolvectl status` works' + ansible.builtin.command: resolvectl status + changed_when: no + register: __systemd_resolved_resolvectl_status + + - name: 'debug `resolvectl status`' + ansible.builtin.debug: + var: __systemd_resolved_resolvectl_status + + - name: 'check if `systemd-resolve` can resolve' + ansible.builtin.command: systemd-resolve archlinux.org + changed_when: no + register: __systemd_resolved_systemd_resolve + + - name: 'debug `systemd-resolve`' + ansible.builtin.debug: + var: __systemd_resolved_systemd_resolve + +... diff --git a/roles/systemd_resolved/requirements.yml b/roles/systemd_resolved/requirements.yml new file mode 100644 index 0000000..8cade0c --- /dev/null +++ b/roles/systemd_resolved/requirements.yml @@ -0,0 +1,7 @@ +--- + +collections: + + - name: ansible.posix + +... diff --git a/roles/systemd_resolved/tasks/main.yml b/roles/systemd_resolved/tasks/main.yml new file mode 100644 index 0000000..c945fe9 --- /dev/null +++ b/roles/systemd_resolved/tasks/main.yml @@ -0,0 +1,107 @@ +--- + +# ----------------------------------------------------------------------------- +# install +# ----------------------------------------------------------------------------- + +- name: install systemd + ansible.builtin.package: + name: systemd + state: present + become: yes + tags: + - install + +# ----------------------------------------------------------------------------- +# config +# ----------------------------------------------------------------------------- + +- name: create conf.d directory + ansible.builtin.file: + dest: /etc/systemd/resolved.conf.d + state: directory + owner: root + group: root + mode: '0755' + become: yes + tags: + - config + +- name: configure systemd-resolved + ansible.builtin.template: + src: resolved.conf.j2 + dest: /etc/systemd/resolved.conf.d/60-ansible.conf + owner: root + group: root + mode: '0644' + become: yes + register: __systemd_resolved_configuration + tags: + - config + +# ----------------------------------------------------------------------------- +# service +# ----------------------------------------------------------------------------- + +- name: start systemd-resolved service + ansible.builtin.systemd: + name: systemd-resolved + state: >- + {{ + __systemd_resolved_configuration.changed | + default(False) | + ternary("restarted", "started") + }} + become: yes + tags: + - service + +- name: enable systemd-resolved service + ansible.builtin.systemd: + name: systemd-resolved + enabled: yes + become: yes + tags: + - service + +# ----------------------------------------------------------------------------- +# /etc/resolv.conf +# ----------------------------------------------------------------------------- + +- name: remove /etc/resolv.conf mount if running in container + ansible.posix.mount: + path: /etc/resolv.conf + state: unmounted + become: yes + when: >- + ansible_mounts | + selectattr('mount', 'equalto', '/etc/resolv.conf') | + length + +- name: check /etc/resolv.conf + ansible.builtin.stat: + path: /etc/resolv.conf + get_checksum: no + get_mime: no + register: __etc_resolv_conf + +- name: remove /etc/resolv.conf if not yet linked + ansible.builtin.file: + path: /etc/resolv.conf + state: absent + become: yes + when: >- + not __etc_resolv_conf.stat.islnk + or + not __etc_resolv_conf.stat.lnk_target | default('') == + '/run/systemd/resolve/stub-resolv.conf' + +- name: link /etc/resolv.conf to stub + ansible.builtin.file: + src: /run/systemd/resolve/stub-resolv.conf + dest: /etc/resolv.conf + state: link + force: yes + become: yes + +... diff --git a/roles/systemd_resolved/templates/resolved.conf.j2 b/roles/systemd_resolved/templates/resolved.conf.j2 new file mode 100644 index 0000000..179e43d --- /dev/null +++ b/roles/systemd_resolved/templates/resolved.conf.j2 @@ -0,0 +1,99 @@ +# {{ ansible_managed }} + +[Resolve] +{% if systemd_resolved_servers is defined %} +{% if systemd_resolved_servers is iterable and + systemd_resolved_servers is not mapping and + systemd_resolved_servers is not string %} +DNS={{ systemd_resolved_servers | join(" ") }} +{% else %} +DNS={{ systemd_resolved_servers }} +{% endif %} +{% endif %} +{% if systemd_resolved_fallback_servers is defined %} +{% if systemd_resolved_fallback_servers is iterable and + systemd_resolved_fallback_servers is not mapping and + systemd_resolved_fallback_servers is not string %} +FallbackDNS={{ systemd_resolved_fallback_servers | join(" ") }} +{% else %} +FallbackDNS={{ systemd_resolved_fallback_servers }} +{% endif %} +{% endif %} +{% if systemd_resolved_domains is defined %} +{% if systemd_resolved_domains is iterable and + systemd_resolved_domains is not mapping and + systemd_resolved_domains is not string %} +Domains={{ systemd_resolved_domains | join(" ") }} +{% else %} +Domains={{ systemd_resolved_domains }} +{% endif %} +{% endif %} +{% if systemd_resolved_dnssec is defined %} +{% if systemd_resolved_dnssec is boolean %} +DNSSEC={{ systemd_resolved_dnssec | ternary('yes', 'no') }} +{% else %} +DNSSEC={{ systemd_resolved_dnssec }} +{% endif %} +{% endif %} +{% if systemd_resolved_dns_over_tls is defined %} +{% if systemd_resolved_dns_over_tls is boolean %} +DNSOverTLS={{ systemd_resolved_dns_over_tls | ternary('yes', 'no') }} +{% else %} +DNSOverTLS={{ systemd_resolved_dns_over_tls }} +{% endif %} +{% endif %} +{% if systemd_resolved_multicast_dns is defined %} +{% if systemd_resolved_multicast_dns is boolean %} +MulticastDNS={{ systemd_resolved_multicast_dns | ternary('yes', 'no') }} +{% else %} +MulticastDNS={{ systemd_resolved_multicast_dns }} +{% endif %} +{% endif %} +{% if systemd_resolved_llmnr is defined %} +{% if systemd_resolved_llmnr is boolean %} +LLMNR={{ systemd_resolved_llmnr | ternary('yes', 'no') }} +{% else %} +LLMNR={{ systemd_resolved_llmnr }} +{% endif %} +{% endif %} +{% if systemd_resolved_cache is defined %} +{% if systemd_resolved_cache is boolean %} +Cache={{ systemd_resolved_cache | ternary('yes', 'no') }} +{% else %} +Cache={{ systemd_resolved_cache }} +{% endif %} +{% endif %} +{% if systemd_resolved_cache_from_localhost is defined %} +{% if systemd_resolved_cache_from_localhost is boolean %} +CacheFromLocalhost={{ systemd_resolved_cache_from_localhost | ternary('yes', 'no') }} +{% else %} +CacheFromLocalhost={{ systemd_resolved_cache_from_localhost }} +{% endif %} +{% endif %} +{% if systemd_resolved_dns_stub_listener is defined %} +{% if systemd_resolved_dns_stub_listener is boolean %} +DNSStubListener={{ systemd_resolved_dns_stub_listener | ternary('yes', 'no') }} +{% else %} +DNSStubListener={{ systemd_resolved_dns_stub_listener }} +{% endif %} +{% endif %} +{% if systemd_resolved_dns_stub_listener_extra is defined and systemd_resolved_dns_stub_listener_extra %} +DNSStubListenerExtra={{ systemd_resolved_dns_stub_listener_extra }} +{% endif %} +{% if systemd_resolved_read_etc_hosts is defined %} +{% if systemd_resolved_read_etc_hosts is boolean %} +ReadEtcHosts={{ systemd_resolved_read_etc_hosts | ternary('yes', 'no') }} +{% else %} +ReadEtcHosts={{ systemd_resolved_read_etc_hosts }} +{% endif %} +{% endif %} +{% if systemd_resolved_resolve_unicast_single_label is defined %} +{% if systemd_resolved_resolve_unicast_single_label is boolean %} +ResolveUnicastSingleLabel={{ systemd_resolved_resolve_unicast_single_label | ternary('yes', 'no') }} +{% else %} +ResolveUnicastSingleLabel={{ systemd_resolved_resolve_unicast_single_label }} +{% endif %} +{% endif %} +{% if systemd_resolved_stale_retention_sec is defined %} +StaleRetentionSec={{ systemd_resolved_stale_retention_sec }} +{% endif %}