diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..591e8ae --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,20 @@ +--- +profile: production + +exclude_paths: + - .cache/ + - .github/ + - molecule/ + +offline: false + +supported_ansible_also: + - ">=2.12" + +verbosity: 1 + +warn_list: + - experimental + - yaml[truthy] + - yaml[line-length] + - role-name diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1c79e16 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +--- +name: Molecule CI + +'on': + pull_request: + branches: + - "master" + - "main" + push: + branches: + - "master" + - "main" + workflow_dispatch: + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Run lint + run: make lint + + test: + name: Test (${{ matrix.os }}-${{ matrix.version }}) + runs-on: ubuntu-latest + needs: lint + strategy: + fail-fast: false + matrix: + include: + - os: debian + version: '12' + image: geerlingguy/docker-debian12-ansible:latest + - os: ubuntu + version: '2004' + image: geerlingguy/docker-ubuntu2004-ansible:latest + - os: ubuntu + version: '2204' + image: geerlingguy/docker-ubuntu2204-ansible:latest + - os: ubuntu + version: '2404' + image: geerlingguy/docker-ubuntu2404-ansible:latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Run molecule test + run: molecule test + env: + MOLECULE_OS: ${{ matrix.os }} + MOLECULE_VERSION: ${{ matrix.version }} + MOLECULE_IMAGE: ${{ matrix.image }} diff --git a/.gitignore b/.gitignore index 5112ae2..356d843 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,8 @@ Icon ._* .Spotlight-V100 .Trashes -.vagrant -test +molecule/default/roles + +__pycache__ +.ansible/ +.venv/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 00e7bec..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -language: python -python: "2.7" -env: - matrix: - - ANSIBLE_VERSION="2.4.2.0" - - ANSIBLE_VERSION="2.7" -before_install: - - sudo apt-get update -qq - - sudo apt-get install -qq python-apt python-pycurl -install: - - pip install ansible=="$ANSIBLE_VERSION" -script: - - echo localhost > inventory - - ansible-playbook -i inventory test.yml --syntax-check - - ansible-playbook -i inventory test.yml --connection=local --sudo - - > - ansible-playbook -i inventory test.yml --connection=local --sudo - | grep -q 'changed=0.*failed=0' - && (echo 'Idempotence test: pass' && exit 0) - || (echo 'Idempotence test: fail' && exit 1) -notifications: - webhooks: https://galaxy.ansible.com/api/v1/notifications/ diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..3aa6935 --- /dev/null +++ b/.yamllint @@ -0,0 +1,17 @@ +--- +extends: default +rules: + line-length: + max: 120 + level: warning + comments: + min-spaces-from-content: 1 + comments-indentation: false + braces: + max-spaces-inside: 1 + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true + +ignore: | + **/.#* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9755280 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +.PHONY: lint test test-debian12 test-ubuntu2004 test-ubuntu2204 test-ubuntu2404 test-all clean distclean + +VENV := .venv +BIN := $(VENV)/bin +export PATH := $(CURDIR)/$(BIN):$(PATH) + +$(VENV): requirements-dev.txt + python3 -m venv $(VENV) + $(BIN)/pip install --upgrade pip + $(BIN)/pip install -r requirements-dev.txt + @touch $(VENV) + +lint: $(VENV) + $(BIN)/yamllint -c .yamllint defaults tasks vars meta + $(BIN)/ansible-lint -c .ansible-lint defaults tasks vars meta + +test: lint test-all + +test-debian12: $(VENV) + MOLECULE_OS=debian MOLECULE_VERSION=12 \ + MOLECULE_IMAGE=geerlingguy/docker-debian12-ansible:latest \ + $(BIN)/molecule test + +test-ubuntu2004: $(VENV) + MOLECULE_OS=ubuntu MOLECULE_VERSION=2004 \ + MOLECULE_IMAGE=geerlingguy/docker-ubuntu2004-ansible:latest \ + $(BIN)/molecule test + +test-ubuntu2204: $(VENV) + MOLECULE_OS=ubuntu MOLECULE_VERSION=2204 \ + MOLECULE_IMAGE=geerlingguy/docker-ubuntu2204-ansible:latest \ + $(BIN)/molecule test + +test-ubuntu2404: $(VENV) + MOLECULE_OS=ubuntu MOLECULE_VERSION=2404 \ + MOLECULE_IMAGE=geerlingguy/docker-ubuntu2404-ansible:latest \ + $(BIN)/molecule test + +test-all: test-debian12 test-ubuntu2004 test-ubuntu2204 test-ubuntu2404 + +clean: + $(BIN)/molecule destroy 2>/dev/null || true + +distclean: clean + rm -rf $(VENV) diff --git a/README.md b/README.md index 862311f..67d4367 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,38 @@ -## ANXS - hostname [![Build Status](https://travis-ci.com/ANXS/hostname.png)](https://travis-ci.com/ANXS/hostname) +## [ANXS](https://github.com/ANXS) - hostname -Ansible role that sets the hostname and FQDN of the node. +[![CI Status](https://img.shields.io/github/actions/workflow/status/anxs/hostname/ci.yml)](https://github.com/ANXS/hostname/actions/workflows/ci.yml) +[![Maintenance](https://img.shields.io/maintenance/yes/2026.svg)](https://github.com/ANXS/hostname) +[![Ansible Role](https://img.shields.io/ansible/role/d/anxs/hostname)](https://galaxy.ansible.com/ui/standalone/roles/ANXS/hostname/) +[![License](https://img.shields.io/github/license/ANXS/hostname)](https://github.com/ANXS/hostname/blob/master/LICENSE) +Ansible role that sets the hostname and FQDN of a node based on its inventory name, keeping `/etc/hostname` and `/etc/hosts` in sync for IPv4 and IPv6. -#### Variables +## Requirements & Dependencies -This depends on your ansible hosts inventory. +* Ansible 2.12 or higher. +* Ubuntu 20.04+ or Debian 12+. +* Depends on `ANXS.apt` (installed via Galaxy). -Add the hosts to your inventory with their FQDN (e.g. foo.bar.com), and the role will take care of setting your hostname accordingly (hostname: foo, FQDN: foo.bar.com). +## Usage -If you just name it with the hostname in the inventory, it will similarly work (hostname set, but no FQDN attached to it). +Add hosts to your inventory with their FQDN (e.g. `foo.bar.com`), and the role will set the short hostname and FQDN accordingly (hostname: `foo`, FQDN: `foo.bar.com`). A bare short name in inventory works too — hostname is set without an attached FQDN. -```yaml -hostname_avahi: no # You may optionall install avahi-mdns and libnss-mdns. This is useful in vagrant. -``` +## Variables -#### Example +Some commonly adjusted variables. See [`defaults/main.yml`](https://github.com/ANXS/hostname/blob/master/defaults/main.yml) for the full set. -Your inventory file should look like this: +* `hostname_avahi` (default `false`) installs `avahi-daemon` and `libnss-mdns` for mDNS-based name resolution. Useful for Vagrant and LAN discovery. -```yaml -foo.bar.com ansible_ssh_host=xxx.xxx.xxx.xxx ansible_ssh_port=22 -baz.bar.com ansible_ssh_host=xxx.xxx.xxx.xxx ansible_ssh_port=22 -``` +## Testing -And the structure of the files in your host_vars folders should match accordingly: +Tests use [Molecule](https://github.com/ansible/molecule) with Docker and [Testinfra](https://testinfra.readthedocs.io/). Run the full suite with `make test`, or target a specific platform (e.g. `make test-ubuntu2404`). -``` -- host_vars - |- foo.bar.com - |- baz.bar.com -``` +The test suite verifies hostname and `/etc/hostname` are set from inventory, `/etc/hosts` carries correct IPv4 and IPv6 entries, and the legacy `127.0.1.1` Raspberry Pi override is removed when present. Tests run across Ubuntu 20.04/22.04/24.04 and Debian 12. +## Note on AI Usage -#### Testing -This project comes with a VagrantFile, this is a fast and easy way to test changes to the role, fire it up with `vagrant up` +This project has been developed with AI assistance. Contributions making use of AI generated content are welcome, however they _must_ be human reviewed prior to submission as pull requests, or issues. All contributors must be able to fully explain and defend any AI generated code, documentation, issues, or tests they submit. Contributions making use of AI must have this explicitly declared in the pull request or issue. This also applies to utilization of AI for reviewing of pull requests. -See [vagrant docs](https://docs.vagrantup.com/v2/) for getting setup with vagrant - - -#### License - -Licensed under the MIT License. See the LICENSE file for details. - - -#### Feedback, bug-reports, requests, ... +## Feedback, bug-reports, requests, ... Are [welcome](https://github.com/ANXS/hostname/issues)! diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index 5db2035..0000000 --- a/Vagrantfile +++ /dev/null @@ -1,20 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -hostname_avahi = ENV.key?('HOSTNAME_AVAHI') ? ENV['HOSTNAME_AVAHI'] : 'false' -Vagrant.configure('2') do |config| - config.vm.define 'anxs' do |c| - c.vm.box = 'ubuntu/trusty64' - c.vm.network :private_network, ip: '192.168.88.12' - c.vm.hostname = 'anxs.local' - c.vm.provision 'ansible' do |ansible| - ansible.playbook = 'test.yml' - ansible.sudo = true - ansible.inventory_path = 'vagrant-inventory' - ansible.host_key_checking = false - ansible.extra_vars = { - hostname_avahi: hostname_avahi - } - end - end -end diff --git a/defaults/main.yml b/defaults/main.yml index d65e422..aa9a506 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,3 +1,3 @@ --- -hostname_avahi: no +hostname_avahi: false diff --git a/meta/main.yml b/meta/main.yml index 52cc7b5..6a97f23 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,16 +1,25 @@ --- galaxy_info: - author: pjan vandaele - company: ANXS - description: sets the hostname and FQDN - min_ansible_version: 2.4.2.0 - license: MIT + role_name: "hostname" + namespace: "anxs" + author: "ANXS Contributors" + company: "ANXS" + description: "sets the hostname and FQDN" + min_ansible_version: "2.12" + license: "MIT" platforms: - name: Ubuntu versions: - - all - categories: + - focal + - jammy + - noble + - name: Debian + versions: + - bookworm + galaxy_tags: - system + - hostname + - networking -dependencies: - - role: ANXS.apt +dependencies: + - role: "ANXS.apt" diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100755 index 0000000..8e6c0cf --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,7 @@ +--- +- name: Converge + hosts: all + become: true + gather_facts: true + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100755 index 0000000..604371b --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,41 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: "anxs-hostname-${MOLECULE_OS:-ubuntu}-${MOLECULE_VERSION:-2404}" + image: "${MOLECULE_IMAGE:-geerlingguy/docker-ubuntu2404-ansible:latest}" + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + privileged: true + pre_build_image: true + cgroupns_mode: host + command: ${MOLECULE_DOCKER_COMMAND:-""} + +provisioner: + name: ansible + config_options: + defaults: + remote_tmp: /tmp/.ansible-${USER}/tmp/ + env: + ANSIBLE_ROLES_PATH: "${MOLECULE_PROJECT_DIRECTORY}/..:${HOME}/.ansible/roles" + +verifier: + name: testinfra + +scenario: + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - idempotence + - verify + - cleanup + - destroy diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml new file mode 100755 index 0000000..e47bd28 --- /dev/null +++ b/molecule/default/prepare.yml @@ -0,0 +1,6 @@ +--- +- hosts: all + tasks: + - name: "early apt-get update" + apt: + update_cache: true diff --git a/molecule/default/requirements.yml b/molecule/default/requirements.yml new file mode 100755 index 0000000..edced44 --- /dev/null +++ b/molecule/default/requirements.yml @@ -0,0 +1,9 @@ +--- +# Temporary pin to GitHub master until ANXS Galaxy namespace case-sensitivity +# issue is resolved and a new ANXS.apt release can be published. The Galaxy +# v2.1.0 tarball has a jinja list-concatenation bug that stringifies the +# package list and crashes hostname's converge. +# See: memory://planning/anxs-galaxy-and-branch-migration +- src: https://github.com/ANXS/apt + name: ANXS.apt + version: master diff --git a/molecule/default/tests/test_hostname.py b/molecule/default/tests/test_hostname.py new file mode 100644 index 0000000..ac785f9 --- /dev/null +++ b/molecule/default/tests/test_hostname.py @@ -0,0 +1,39 @@ +"""Testinfra tests for anxs-hostname role.""" +import os + + +def test_hostname_file_exists(host): + """Verify /etc/hostname exists with correct permissions.""" + f = host.file("/etc/hostname") + assert f.exists + assert f.is_file + assert f.user == "root" + assert f.group == "root" + assert f.mode == 0o644 + + +def test_hostname_file_not_empty(host): + """Verify /etc/hostname contains a hostname.""" + f = host.file("/etc/hostname") + assert f.exists + content = f.content_string.strip() + assert len(content) > 0 + + +def test_dbus_installed(host): + """Verify dbus package is installed.""" + assert host.package("dbus").is_installed + + +def test_hosts_file_exists(host): + """Verify /etc/hosts exists.""" + f = host.file("/etc/hosts") + assert f.exists + assert f.is_file + + +def test_hosts_has_localhost(host): + """Verify /etc/hosts contains localhost entry.""" + f = host.file("/etc/hosts") + assert f.contains("127.0.0.1") + assert f.contains("localhost") diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..6b0500f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,17 @@ +# Core testing framework +molecule>=25.9.0 +molecule-plugins[docker]>=23.5.0 + +# Ansible ecosystem +ansible>=8.0.0,<12 +ansible-lint>=6.22.0 + +# Python testing +pytest>=7.4.0 +pytest-testinfra>=10.2.0 + +# Code quality +yamllint>=1.35.0 + +# Docker support +docker>=6.1.0 diff --git a/tasks/main.yml b/tasks/main.yml index 9fddb1d..239d9fd 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,39 +1,63 @@ --- -- name: Hostname | Ensure dbus installed - apt: - name: "{{anxs_hostname_packages}}" +- name: "Detect Recent OS" + ansible.builtin.set_fact: + anxs_hostname_recent: >- + {{ (ansible_distribution == 'Debian' and ansible_distribution_major_version|int >= 13) or + (ansible_distribution == 'Ubuntu' + and ansible_distribution_version is version('24.04', '>=')) }} + +- name: "Ensure dbus installed (legacy)" + ansible.builtin.apt: + name: "{{ anxs_hostname_packages }}" state: present tags: hostname - when: ansible_os_family == "Debian" + when: ansible_os_family == "Debian" and not anxs_hostname_recent -- name: Hostname | Update the hostname (pt. 1) - hostname cmd - hostname: - name: "{{inventory_hostname_short}}" +- name: "Ensure dbus installed (current)" + ansible.builtin.apt: + name: "{{ anxs_hostname_packages + ['pkexec', 'polkitd'] }}" + state: present + tags: hostname + when: ansible_os_family == "Debian" and anxs_hostname_recent -- name: Hostname | Update the hostname (pt. 2) - (/etc/hostname) - copy: - content: "{{inventory_hostname_short}}{{'\n'}}" +- name: "Update the hostname (pt. 1) - hostname cmd" + ansible.builtin.hostname: + name: "{{ inventory_hostname_short }}" + +- name: "Update the hostname (pt. 2) - (/etc/hostname)" + ansible.builtin.copy: + content: "{{ inventory_hostname_short }}{{ '\n' }}" dest: /etc/hostname owner: root group: root - mode: 0644 + mode: "0644" -- name: Hostname | Update the IPv4 hostname (pt. 3) - (/etc/hosts) - lineinfile: +- name: "Update the IPv4 hostname (pt. 3) - (/etc/hosts)" + ansible.builtin.lineinfile: dest: /etc/hosts regexp: "^127.0.0.1" - line: "127.0.0.1{{'\t'}}{{inventory_hostname}}{% if inventory_hostname != inventory_hostname_short %}{{'\t'}}{{inventory_hostname_short}}{% endif %}{{'\t'}}localhost" + line: >- + 127.0.0.1 {{ inventory_hostname }}{% if inventory_hostname != inventory_hostname_short %} {{ inventory_hostname_short }}{% endif %} localhost state: present + when: ansible_virtualization_type != 'docker' -- name: Hostname | Update the IPv6 hostname (pt. 3) - (/etc/hosts) - lineinfile: +- name: "Update the IPv6 hostname (pt. 3) - (/etc/hosts)" + ansible.builtin.lineinfile: dest: /etc/hosts regexp: "^::1" - line: "::1{{'\t\t'}}{{inventory_hostname}}{% if inventory_hostname != inventory_hostname_short %}{{'\t'}}{{inventory_hostname_short}}{% endif %}{{'\t'}}localhost ip6-localhost ip6-loopback" + line: >- + ::1 {{ inventory_hostname }}{% if inventory_hostname != inventory_hostname_short %} {{ inventory_hostname_short }}{% endif %} localhost ip6-localhost ip6-loopback state: present + when: ansible_virtualization_type != 'docker' + +- name: "Remove raspberrypi override if exists" + ansible.builtin.lineinfile: + dest: "/etc/hosts" + regexp: "^127.0.1.1.+" + state: absent -- name: Hostname | Install MDNS Components - apt: - name: "{{anxs_hostname_mdns_packages}}" +- name: "Install MDNS Components" + ansible.builtin.apt: + name: "{{ anxs_hostname_mdns_packages }}" state: present when: 'ansible_os_family == "Debian" and hostname_avahi' diff --git a/test.yml b/test.yml deleted file mode 100644 index a7ec1f5..0000000 --- a/test.yml +++ /dev/null @@ -1,6 +0,0 @@ -- hosts: all - vars_files: - - 'defaults/main.yml' - - 'vars/main.yml' - tasks: - - include_tasks: 'tasks/main.yml' diff --git a/vagrant-inventory b/vagrant-inventory deleted file mode 100644 index cbebc89..0000000 --- a/vagrant-inventory +++ /dev/null @@ -1,2 +0,0 @@ -[anxs] -anxs.local ansible_ssh_host=192.168.88.12 ansible_ssh_port=22 diff --git a/vars/main.yml b/vars/main.yml index 2e7fde0..e3c1690 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -1,7 +1,6 @@ --- anxs_hostname_packages: - - dbus - - policykit-1 + - "dbus" anxs_hostname_mdns_packages: - - avahi-daemon - - libnss-mdns + - "avahi-daemon" + - "libnss-mdns"