# Host-side setup (runs on the jail host via SSH) # Normalize zfs and nullfs entries: expand short names to full paths - name: "Normalize zfs and nullfs entries for {{ jail_name }}" set_fact: _zfs: "{{ zfs | default([]) | normalize_zfs }}" _nullfs: "{{ nullfs | default([]) | normalize_nullfs }}" - name: "Check if {{ jail_name }} container exists" shell: "zfs list -o name | grep -Fxq 'zroot/jails/containers/{{ jail_name }}'" failed_when: false changed_when: false register: jail_exists delegate_to: "{{ jail_delegate_host }}" - name: "Check {{ jail_name }} userland version" shell: "zfs get -H -o value origin zroot/jails/containers/{{ jail_name }}" register: jail_origin changed_when: false when: jail_exists.rc == 0 delegate_to: "{{ jail_delegate_host }}" # Userland migration - name: "*** USERLAND MIGRATION REQUIRED: {{ jail_name }} ***" pause: prompt: | ════════════════════════════════════════════════════════════════ JAIL USERLAND MIGRATION: {{ jail_name }} ════════════════════════════════════════════════════════════════ Current: {{ jail_origin.stdout | trim }} Target: zroot/jails/templates/{{ userland }}@base This will: 1. Stop the jail 2. Rename existing dataset to *.old. 3. Clone fresh from {{ userland }} ════════════════════════════════════════════════════════════════ Press Enter to continue or Ctrl+C to abort when: - jail_exists.rc == 0 - jail_origin.stdout is defined - "userland + '@base' not in jail_origin.stdout" - name: "Stop {{ jail_name }} jail for userland migration" shell: "service jail stop {{ jail_name }} || true" when: - jail_exists.rc == 0 - jail_origin.stdout is defined - "userland + '@base' not in jail_origin.stdout" delegate_to: "{{ jail_delegate_host }}" - name: "Unmount any remaining mounts inside {{ jail_name }} container" shell: | mount | awk '$3 ~ "^/usr/local/jails/containers/{{ jail_name }}/" {print $3}' | sort -r | xargs -I{} umount -f {} 2>/dev/null || true when: - jail_exists.rc == 0 - jail_origin.stdout is defined - "userland + '@base' not in jail_origin.stdout" delegate_to: "{{ jail_delegate_host }}" - name: "Migrate {{ jail_name }} to {{ userland }}" shell: | zfs rename zroot/jails/containers/{{ jail_name }} zroot/jails/containers/{{ jail_name }}.old.$(date +%s) when: - jail_exists.rc == 0 - jail_origin.stdout is defined - "userland + '@base' not in jail_origin.stdout" delegate_to: "{{ jail_delegate_host }}" - name: "Clone {{ jail_name }} from template" shell: "zfs clone zroot/jails/templates/{{ userland }}@base zroot/jails/containers/{{ jail_name }}" when: jail_exists.rc != 0 or (jail_origin.stdout is defined and userland + '@base' not in jail_origin.stdout) delegate_to: "{{ jail_delegate_host }}" # ZFS volume setup (for all volumes — existing, migrated, and new) - name: "Ensure ZFS volumes exist for {{ jail_name }}" community.general.zfs: name: "{{ item.name }}" state: present loop: "{{ _zfs }}" loop_control: label: "{{ item.name }}" delegate_to: "{{ jail_delegate_host }}" - name: "Set ownership on ZFS volume host mountpoints for {{ jail_name }}" file: path: "/usr/local/jails/volumes/{{ item.name | basename }}" state: directory owner: "{{ item.owner | default('root') }}" group: "{{ item.group | default('wheel') }}" mode: "{{ item.mode | default('0755') }}" loop: "{{ _zfs }}" loop_control: label: "{{ item.name }}" delegate_to: "{{ jail_delegate_host }}" - name: "Mount ZFS volumes for {{ jail_name }}" shell: "zfs mount {{ item.name }} || true" loop: "{{ _zfs }}" loop_control: label: "{{ item.name }}" changed_when: false delegate_to: "{{ jail_delegate_host }}" # Container setup - name: "Create directories for {{ jail_name }}" file: path: "/usr/local/jails/containers/{{ jail_name }}{{ item }}" state: directory owner: root group: wheel mode: "0755" loop: "{{ dirs | default([]) }}" delegate_to: "{{ jail_delegate_host }}" - name: "Verify mount point sources exist for {{ jail_name }}" stat: path: "{{ item.src }}" loop: "{{ _nullfs }}" loop_control: label: "{{ item.src }}" register: _nullfs_sources delegate_to: "{{ jail_delegate_host }}" - name: "Assert mount point sources exist for {{ jail_name }}" assert: that: item.stat.exists fail_msg: "nullfs source does not exist: {{ item.item.src }}" loop: "{{ _nullfs_sources.results }}" loop_control: label: "{{ item.item.src }}" - name: "Create mount point destinations for {{ jail_name }}" file: path: "/usr/local/jails/containers/{{ jail_name }}{{ item.dst }}" state: directory owner: root group: wheel mode: "0755" loop: "{{ _nullfs }}" loop_control: label: "{{ item.dst }}" when: jail_exists.rc != 0 or (jail_origin.stdout is defined and userland + '@base' not in jail_origin.stdout) delegate_to: "{{ jail_delegate_host }}" - name: "Deploy jail.conf.d/{{ jail_name }}.conf" template: src: jail_conf.j2 dest: "/etc/jail.conf.d/{{ jail_name }}.conf" owner: root group: wheel mode: "0644" vars: jail: name: "{{ jail_name }}" num: "{{ jail_num }}" ip: "{{ jail_ips[jail_name] }}/{{ jail_lan_cidr | ipv4_prefixlen }}" devfs_ruleset: "{{ devfs_ruleset | default(4) }}" options: "{{ jail_conf_options | default([]) }}" exec_prestart: "{{ exec_prestart | default([]) }}" exec_start: "{{ exec_start | default([]) }}" exec_poststart: "{{ exec_poststart | default([]) }}" exec_prestop: "{{ exec_prestop | default([]) }}" exec_stop: "{{ exec_stop | default([]) }}" exec_poststop: "{{ exec_poststop | default([]) }}" nullfs: "{{ _nullfs }}" delegate_to: "{{ jail_delegate_host }}" - name: "Start {{ jail_name }} jail" shell: "service jail start {{ jail_name }}" register: jail_start failed_when: "jail_start.rc != 0 and 'already exists' not in jail_start.stdout" changed_when: "'already exists' not in jail_start.stdout" delegate_to: "{{ jail_delegate_host }}" # In-jail provisioning - name: Create pkg repos directory file: path: /usr/local/etc/pkg/repos state: directory owner: root group: wheel mode: "0755" - name: Configure poudriere repo in jail copy: content: | poudriere: { url: "http://{{ jail_ips.poudriere_repo }}/packages", enabled: no, priority: 100 } dest: /usr/local/etc/pkg/repos/poudriere.conf owner: root group: wheel mode: "0644" - name: Install packages shell: "pkg install -y {{ pkg | join(' ') }}" environment: ASSUME_ALWAYS_YES: "yes" when: pkg is defined and pkg | length > 0 register: pkg_result changed_when: "'Number of packages to be installed' in pkg_result.stdout" - name: Install packages from poudriere shell: "timeout 120 pkg install -y -r poudriere {{ pkg_custom | join(' ') }}" when: pkg_custom is defined and pkg_custom | length > 0 register: pkg_custom_result changed_when: "'Installing' in pkg_custom_result.stdout" - name: Create parent directories for files file: path: "{{ item.dest | dirname }}" state: directory owner: root group: wheel mode: "0755" loop: "{{ files | default([]) }}" loop_control: label: "{{ item.dest | dirname }}" when: files is defined - name: Deploy files template: src: "{{ jail_role_dir }}/templates/{{ item.src }}" dest: "{{ item.dest }}" owner: "{{ item.owner | default('root') }}" group: "{{ item.group | default('wheel') }}" mode: "{{ item.mode | default('0644') }}" loop: "{{ files | default([]) }}" loop_control: label: "{{ item.dest }}" when: files is defined notify: Restart jail services - name: Enable services community.general.sysrc: name: "{{ item }}_enable" value: "YES" loop: "{{ services | default([]) }}" - name: Set sysrc values community.general.sysrc: name: "{{ item.name }}" value: "{{ item.value }}" loop: "{{ sysrc | default([]) }}" when: sysrc is defined - name: Set sysctl values sysctl: name: "{{ item.name }}" value: "{{ item.value }}" state: present loop: "{{ sysctl | default([]) }}" when: sysctl is defined