202 Create Ansible client templates and clone DHCP Ansible client jails

Extending example 200 Create Ansible client templates and clone Ansible client jails.

Use case

Get the IP addresses by DHCP. Create the dhclient-exit-hooks. For example, the below hook:

shell> cat /zroot/iocage/templates/ansible_client/root/etc/dhclient-exit-hooks
case "$reason" in
    "BOUND"|"REBIND"|"REBOOT"|"RENEW")
    echo $new_ip_address > /var/db/dhclient-hook.address.$interface
    ;;
esac

creates files. For example,

shell> cat /zroot/iocage/jails/test_101/root/var/db/dhclient-hook.address.epair0b
10.1.0.130

Read the files, created by the hooks, and use the IP addresses to compose the variable ansible_host

shell> cat hosts/01_iocage.yml
plugin: vbotka.freebsd.iocage
...
hooks_results:
  - /var/db/dhclient-hook.address.epair0b
compose:
  ansible_host: iocage_hooks.0

Default to iocage_ip4 if the hook is not available

compose:
  ansible_host: (iocage_hooks.0 == '-') | ternary(iocage_ip4, iocage_hooks.0)

Tree

shell> tree .
.
├── ansible.cfg
├── files
│   └── pk_admins.txt
├── hosts
│   ├── 01_iocage.yml
│   ├── 02_iocage.yml
│   └── 03_constructed.yml
├── host_vars
│   ├── iocage_01
│   │   └── iocage.yml
│   └── iocage_02
│       └── iocage.yml
├── iocage-hosts.ini
├── pb-iocage-ansible-clients.yml -> ../../../../playbooks/pb-iocage-ansible-clients.yml
├── pb-iocage-template -> ../../../../playbooks/pb-iocage-template
├── pb-iocage-template.yml -> ../../../../playbooks/pb-iocage-template.yml
└── pb-test-01.yml

Synopsis

  • On two iocage hosts:

    • iocage_01

    • iocage_02

    In the playbook pb-iocage-template.yml, use the modules:

    • vbotka.freebsd.iocage to create, start, stop, and convert jails to templates.

    • vbotka.freebsd.iocage exec to create a user and set .ssh ownership.

    • community.general.pkgng to install packages.

    • ansible.posix.authorized_key to configure public keys.

    • ansible.builtin.lineinfile to configure /etc/rc.conf and /usr/local/etc/sudoers

    • configure dhclient hooks.

    In the playbook pb-iocage-ansible-clients.yml, use the module vbotka.freebsd.iocage to:

    • create jails from the Ansible client templates

    • start all jails

    • optionally display the lists of jails.

  • On all created jails:

    In the playbook pb-test-01.yml:

    • connect created jails

    • display basic configuration of the jails.

Requirements

Notes

  • The option hooks_results expects the poolname of a jail to be mounted to /poolname. For example, if you activate the pool zroot this plugin expects to find the hooks_results items in the path /zroot/iocage/jails/<name>/root. If you mount the poolname to a different path the easiest remedy is to create a symlink.

Configuration ansible.cfg

[defaults]
gathering = explicit
callback_result_format = yaml

[connection]
pipelining = true

host_vars/iocage_01/iocage.yml

ansible_python_interpreter: /usr/local/bin/python3.9
ansible_become_password: admin

freebsd_iocage_pool: pool2
freebsd_iocage_pool_mount: /mnt/pool2
freebsd_iocage_mount: "{{ freebsd_iocage_pool_mount }}/iocage"

templates:
  ansible_client:
    release: 13.4-RELEASE
    properties:
      ip4_addr: 'vnet0|10.1.0.198/24'
    dhclient: "{{ act_dhclient | dict2items }}"
    rcconf: "{{ act_rcconf | dict2items }}"

# ansible client template defaults
act_pkg:
  - security/sudo
  - lang/python39
act_user: admin
act_pk: pk_admins.txt
act_sudo: true
act_rcconf:
  iocage_enable: '"YES"'
  sshd_enable: '"YES"'
act_dhclient:
  dhclient-enter-hooks: |
    echo "dhclient-enter-hooks: $interface $reason new_ip_address=$new_ip_address" >> /var/log/dhclient-hook.log
    # logger "dhclient-enter-hooks: $interface $reason new_ip_address=$new_ip_address"
  dhclient-exit-hooks: |
    echo "dhclient-exit-hooks: $interface $reason new_ip_address=$new_ip_address" >> /var/log/dhclient-hook.log
    # logger "dhclient-exit-hooks: $interface $reason new_ip_address=$new_ip_address"
    case "$reason" in
        "BOUND"|"REBIND"|"REBOOT"|"RENEW")
        echo $new_ip_address > /var/db/dhclient-hook.address.$interface
        ;;
    esac

clones:
  test_101: {clone_from: ansible_client}
  test_102: {clone_from: ansible_client}
  test_103: {clone_from: ansible_client}
start: "{{ clones.keys() }}"
swarms: {}

# clones default properties
properties:
  bpf: 1
  dhcp: 1
  vnet: 1
  notes: "vmm={{ inventory_hostname }}"

Hint

The option hooks_results expects the pool to be mounted to /pool. In the above host_vars the pool pool2 is mounted to /mnt/pool2. For hooks_results to work properly, create symlink in the root directory

pool2 -> /mnt/pool2

host_vars/iocage_02/iocage.yml

ansible_python_interpreter: /usr/local/bin/python3.11

freebsd_iocage_runner_env:
  CRYPTOGRAPHY_OPENSSL_NO_LEGACY: 1

freebsd_iocage_pool: zroot
freebsd_iocage_pool_mount: /zroot
freebsd_iocage_mount: "{{ freebsd_iocage_pool_mount }}/iocage"

templates:
  ansible_client:
    release: 14.1-RELEASE
    properties:
      ip4_addr: 'em0|10.1.0.199/24'
    dhclient: "{{ act_dhclient | dict2items }}"
    rcconf: "{{ act_rcconf | dict2items }}"

# ansible client template defaults
act_pkg:
  - security/sudo
  - lang/python311
act_user: admin
act_pk: pk_admins.txt
act_sudo: true
act_rcconf:
  iocage_enable: '"YES"'
  sshd_enable: '"YES"'
act_dhclient:
  dhclient-enter-hooks: |
    echo "dhclient-enter-hooks: $interface $reason new_ip_address=$new_ip_address" >> /var/log/dhclient-hook.log
    # logger "dhclient-enter-hooks: $interface $reason new_ip_address=$new_ip_address"
  dhclient-exit-hooks: |
    echo "dhclient-exit-hooks: $interface $reason new_ip_address=$new_ip_address" >> /var/log/dhclient-hook.log
    # logger "dhclient-exit-hooks: $interface $reason new_ip_address=$new_ip_address"
    case "$reason" in
        "BOUND"|"REBIND"|"REBOOT"|"RENEW")
        echo $new_ip_address > /var/db/dhclient-hook.address.$interface
        ;;
    esac

clones:
  test_111: {clone_from: ansible_client}
  test_112: {clone_from: ansible_client}
  test_113: {clone_from: ansible_client}
start: "{{ clones.keys() }}"
swarms: {}

# clones default properties
properties:
  bpf: 1
  dhcp: 1
  vnet: 1
  notes: "vmm={{ inventory_hostname }}"

Note

The variables act_* are used to configure ansible_client template

  • The dhclient hooks act_dhclient will be created in /etc.

  • The user act_user will be created in the template.

  • The user act_user will serve as Ansible remote_user.

  • The file act_pk provides the public keys allowed to ssh to act_user.

Warning

  • The user act_user must exist on the iocage host. Otherwise, the module ansible.posix.authorized_key will crash. See pb-iocage-template/pk.yml

  • The file files/pk_admins.txt was sanitized. Fit the public keys to your needs

    shell> cat files/pk_admins.txt
    ssh-rsa <sanitized> admin@controller
    

Inventory iocage-hosts.ini

iocage_01 ansible_host=10.1.0.18
iocage_02 ansible_host=10.1.0.73

[iocage]
iocage_01
iocage_02

[iocage:vars]
ansible_user=admin
ansible_become=true

Playbook pb-iocage-template.yml

- name: Create Ansible client templates.
  hosts: iocage
  environment: "{{ freebsd_iocage_runner_env | d({}) }}"

  vars:

    debug: false
    debug2: false

  tasks:

    - name: Setup
      tags: [always, setup]
      ansible.builtin.import_tasks: pb-iocage-template/setup.yml

    - name: Create jails
      tags: create
      ansible.builtin.import_tasks: pb-iocage-template/create.yml

    - name: Start jails
      tags: start
      ansible.builtin.import_tasks: pb-iocage-template/start.yml

    - name: Install packages
      when: act_pkg | length > 0
      tags: pkg
      ansible.builtin.import_tasks: pb-iocage-template/pkg.yml

    - name: Create user
      when: act_user | length > 0
      tags: user
      ansible.builtin.import_tasks: pb-iocage-template/user.yml

    - name: Configure public keys
      when: act_pk | length > 0
      tags: pk
      ansible.builtin.import_tasks: pb-iocage-template/pk.yml

    - name: Configure sudo
      when: act_sudo | bool
      tags: sudo
      ansible.builtin.import_tasks: pb-iocage-template/sudo.yml

    - name: Configure dhclient
      when: act_dhclient | length > 0
      tags: dhclient
      ansible.builtin.import_tasks: pb-iocage-template/dhclient.yml

    - name: Configure /etc/rc.conf
      when: act_rcconf | length > 0
      tags: rcconf
      ansible.builtin.import_tasks: pb-iocage-template/rc_conf.yml

    - name: Stop jails
      tags: stop
      ansible.builtin.import_tasks: pb-iocage-template/stop.yml

    - name: Convert jails to templates
      tags: template
      ansible.builtin.import_tasks: pb-iocage-template/template.yml

Playbook output

(env) > ansible-playbook pb-iocage-template.yml -i iocage-hosts.ini -e debug=true

  ...

List templates at iocage_01

[iocage_01]# iocage list -lt
+------+----------------+------+-------+----------+-----------------+---------------------+-----+----------+----------+
| JID  |      NAME      | BOOT | STATE |   TYPE   |     RELEASE     |         IP4         | IP6 | TEMPLATE | BASEJAIL |
+======+================+======+=======+==========+=================+=====================+=====+==========+==========+
| None | ansible_client | off  | down  | template | 13.4-RELEASE-p2 | vnet0|10.1.0.198/24 | -   | -        | no       |
+------+----------------+------+-------+----------+-----------------+---------------------+-----+----------+----------+

List templates at iocage_02

[iocage_02]# iocage list -lt
| JID  |      NAME      | BOOT | STATE |   TYPE   |     RELEASE     |        IP4        | IP6 | TEMPLATE | BASEJAIL |
+======+================+======+=======+==========+=================+===================+=====+==========+==========+
| None | ansible_client | off  | down  | template | 14.1-RELEASE-p6 | em0|10.1.0.199/24 | -   | -        | no       |
+------+----------------+------+-------+----------+-----------------+-------------------+-----+----------+----------+

Playbook pb-iocage-ansible-clients.yml

- name: Create and start jails. Optionally stop and destroy jails.
  hosts: iocage
  environment: "{{ freebsd_iocage_runner_env | d({}) }}"

  vars:

    debug: false
    debug2: false
    dry_run: false

    swarm: false
    swarm_destroy: false
    clone: false
    clone_destroy: false

    _tags: "{{ dict(iocage_jails.keys()
                    | zip(iocage_jails.values()
                          | map(attribute='properties.notes')
                          | map('split')
                          | map('map', 'split', '=')
                          | map('community.general.dict'))) }}"
    _started: "{{ iocage_jails | dict2items
                               | selectattr('value.state', 'eq', 'up')
                               | map(attribute='key') }}"

  tasks:

    - name: Create and start swarms
      when: swarm | bool
      tags: swarm
      ansible.builtin.import_tasks: pb-iocage-ansible-clients/swarm.yml

    - name: Stop and destroy swarms
      when: swarm_destroy | bool
      tags: swarm_destroy
      ansible.builtin.import_tasks: pb-iocage-ansible-clients/swarm_destroy.yml

    - name: Create and start clones
      when: clone | bool
      tags: clone
      ansible.builtin.import_tasks: pb-iocage-ansible-clients/clone.yml

    - name: Stop and destroy clones
      when: clone_destroy | bool
      tags: clone_destroy
      ansible.builtin.import_tasks: pb-iocage-ansible-clients/clone_destroy.yml

    - name: Display iocage_jails
      when: debug | bool or debug2 | bool
      tags: list
      ansible.builtin.import_tasks: pb-iocage-ansible-clients/list.yml

Playbook output

(env) > ansible-playbook pb-iocage-ansible-clients.yml -i iocage-hosts.ini \
                                                       -t clone \
						       -e clone=true

PLAY [Create and start jails. Optionally stop and destroy jails.] ******************************

TASK [Create clones from template] *************************************************************
changed: [iocage_01] => (item=test_101 ansible_client)
changed: [iocage_01] => (item=test_102 ansible_client)
changed: [iocage_02] => (item=test_111 ansible_client)
changed: [iocage_01] => (item=test_103 ansible_client)
changed: [iocage_02] => (item=test_112 ansible_client)
changed: [iocage_02] => (item=test_113 ansible_client)

TASK [Debug clones from template debug2=False] *************************************************
skipping: [iocage_01]
skipping: [iocage_02]

TASK [Start clones] ****************************************************************************
changed: [iocage_01] => (item=test_101)
changed: [iocage_01] => (item=test_102)
changed: [iocage_02] => (item=test_111)
changed: [iocage_01] => (item=test_103)
changed: [iocage_02] => (item=test_112)
changed: [iocage_02] => (item=test_113)

TASK [Debug start clones debug2=False] *********************************************************
skipping: [iocage_01]
skipping: [iocage_02]

PLAY RECAP *************************************************************************************
iocage_01: ok=2    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
iocage_02: ok=2    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0

List jails at iocage_01

[iocage_01]# iocage list -l
+-----+----------+------+-------+------+-----------------+--------------------+-----+----------------+----------+
| JID |   NAME   | BOOT | STATE | TYPE |     RELEASE     |        IP4         | IP6 |    TEMPLATE    | BASEJAIL |
+=====+==========+======+=======+======+=================+====================+=====+================+==========+
| 38  | test_101 | off  | up    | jail | 13.4-RELEASE-p2 | epair0b|10.1.0.130 | -   | ansible_client | no       |
+-----+----------+------+-------+------+-----------------+--------------------+-----+----------------+----------+
| 39  | test_102 | off  | up    | jail | 13.4-RELEASE-p2 | epair0b|10.1.0.245 | -   | ansible_client | no       |
+-----+----------+------+-------+------+-----------------+--------------------+-----+----------------+----------+
| 40  | test_103 | off  | up    | jail | 13.4-RELEASE-p2 | epair0b|10.1.0.180 | -   | ansible_client | no       |
+-----+----------+------+-------+------+-----------------+--------------------+-----+----------------+----------+

List jails at iocage_02

[iocage_02]# iocage list -l
+-----+----------+------+-------+------+-----------------+--------------------+-----+----------------+----------+
| JID |   NAME   | BOOT | STATE | TYPE |     RELEASE     |        IP4         | IP6 |    TEMPLATE    | BASEJAIL |
+=====+==========+======+=======+======+=================+====================+=====+================+==========+
| 44  | test_111 | off  | up    | jail | 14.1-RELEASE-p6 | epair0b|10.1.0.174 | -   | ansible_client | no       |
+-----+----------+------+-------+------+-----------------+--------------------+-----+----------------+----------+
| 45  | test_112 | off  | up    | jail | 14.1-RELEASE-p6 | epair0b|10.1.0.147 | -   | ansible_client | no       |
+-----+----------+------+-------+------+-----------------+--------------------+-----+----------------+----------+
| 46  | test_113 | off  | up    | jail | 14.1-RELEASE-p6 | epair0b|10.1.0.231 | -   | ansible_client | no       |
+-----+----------+------+-------+------+-----------------+--------------------+-----+----------------+----------+

Inventory hosts/01_iocage.yml

plugin: vbotka.freebsd.iocage
host: 10.1.0.18
user: admin
hooks_results:
  - /var/db/dhclient-hook.address.epair0b
compose:
  ansible_host: (iocage_hooks.0 == '-') | ternary(iocage_ip4, iocage_hooks.0)
groups:
  test_01: inventory_hostname.startswith('test')

Inventory hosts/02_iocage.yml

plugin: vbotka.freebsd.iocage
host: 10.1.0.73
user: admin
env:
  CRYPTOGRAPHY_OPENSSL_NO_LEGACY: 1
hooks_results:
  - /var/db/dhclient-hook.address.epair0b
compose:
  ansible_host: (iocage_hooks.0 == '-') | ternary(iocage_ip4, iocage_hooks.0)
groups:
  test_02: inventory_hostname.startswith('test')

Inventory hosts/03_constructed.yml

plugin: ansible.builtin.constructed
groups:
    test: inventory_hostname.startswith('test')

Display inventory

(env) > ansible-inventory -i hosts --graph
@all:
  |--@ungrouped:
  |--@test_01:
  |  |--test_101
  |  |--test_102
  |  |--test_103
  |--@test_02:
  |  |--test_111
  |  |--test_112
  |  |--test_113
  |--@test:
  |  |--test_101
  |  |--test_102
  |  |--test_103
  |  |--test_111
  |  |--test_112
  |  |--test_113

Playbook pb-test-01.yml

- name: Connect to the group test.
  hosts: test
  remote_user: admin
  gather_facts: false

  vars:

    ansible_python_interpreter: auto_silent
    
  tasks:

    - ansible.builtin.command: hostname
      register: out_host
    
    - ansible.builtin.debug:
        msg: "ansible_host={{ ansible_host }} iocage_hooks={{ iocage_hooks }}"

Playbook output

(env) > ansible-playbook pb-test-01.yml -i hosts

PLAY [Connect to the group test.] *************************************************************

TASK [ansible.builtin.command] ****************************************************************
changed: [test_102]
changed: [test_101]
changed: [test_103]
changed: [test_112]
changed: [test_111]
changed: [test_113]

TASK [ansible.builtin.debug] ******************************************************************
ok: [test_101] => 
    msg: ansible_host=10.1.0.130 iocage_hooks=['10.1.0.130']
ok: [test_102] => 
    msg: ansible_host=10.1.0.245 iocage_hooks=['10.1.0.245']
ok: [test_103] => 
    msg: ansible_host=10.1.0.180 iocage_hooks=['10.1.0.180']
ok: [test_111] => 
    msg: ansible_host=10.1.0.174 iocage_hooks=['10.1.0.174']
ok: [test_112] => 
    msg: ansible_host=10.1.0.147 iocage_hooks=['10.1.0.147']
ok: [test_113] => 
    msg: ansible_host=10.1.0.231 iocage_hooks=['10.1.0.231']

PLAY RECAP ************************************************************************************
test_101: ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
test_102: ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
test_103: ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
test_111: ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
test_112: ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
test_113: ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Hint

The below command stops and destroys the cloned jails

ansible-playbook pb-iocage-ansible-clients.yml -i iocage-hosts.ini \
                                               -t clone_destroy \
                                               -e clone_destroy=true