Skip to content

Latest commit

 

History

History
2672 lines (2237 loc) · 100 KB

pot.org

File metadata and controls

2672 lines (2237 loc) · 100 KB

Ansible Pot

I am a role to manage your Pot jails on FreeBSD. My source is located in the pot.org file.

Requirements

None.

Global Blocks

This is the header for all examples to turn them into runnable tests:

- hosts: localhost
  connection: local
  become: yes
  collections:
    - zilti.pot
  roles:
    - role: zilti.pot.pot
      vars:
        pot:
          enabled: true
          vnet_enabled: true
          zfs_root: tank/pot
          extif: vtnet0
  tasks:

Running shell commands:

(concat (format "self._execute_module(module_name='ansible.builtin.command', module_args=dict(_raw_params=%s, _uses_shell=True" cmd)
        (when (> (length creates) 0)
          (format ", creates=%s" creates))
        (when (> (length removes) 0)
          (format ", removes=%s" removes))
        "), task_vars=task_vars, tmp=tmp)")

Determining Pot’s root directory:

def pot_root(self, tmp, task_vars):
    display.vvv("Determining pot root...")
    result = self._execute_module(
        module_name='ansible.builtin.command',
        module_args=dict(_uses_shell=True,_raw_params='$(which pot) config -g fs_root'),
        task_vars=task_vars,
        tmp=tmp
    )
    display.vvv("Pot Root output: %s" % result['stdout'])
    return result['stdout'].split("=")[1].strip()

And the ZFS root:

def pot_zfs_root(self, tmp, task_vars):
    result = self._execute_module(
        module_name='ansible.builtin.command',
        module_args=dict(_uses_shell=True,_raw_params='$(which pot) config -g zfs_root'),
        task_vars=task_vars,
        tmp=tmp
    )
    return result['stdout'].split('=')[1].strip()

Role Variables

Pot Server

VariableTypeChoicesRequired?DefaultInfo
enabledboolFalseTriggers pot init
vnet_enabledboolFalseTriggers pot vnet-start
zfs_rootstr‘tank/pot’Is written to pot.conf
fs_rootstr‘/opt/pot’Is written to pot.conf
cachestr‘/var/cache/pot’Is written to pot.conf
tmpstr‘/tmp’Is written to pot.conf
mktemp_suffixstr‘.XXXXXXXX’Is written to pot.conf
hostname_max_lengthint64Is written to pot.conf
networkstr‘10.192.0.0/10’Is written to pot.conf
netmaskstr‘255.192.0.0’Is written to pot.conf
gatewaystr‘10.192.0.1’Is written to pot.conf
extifstr‘em0’Is written to pot.conf
---
pot:
  <<gen-defaults-varlist(srctbl=server-default-vars)>>

Main Tasks

Installing the fact gathering script:

- file:
    path: '/usr/local/etc/ansible/facts.d'
    state: directory
  become: yes
- copy:
    dest: '/usr/local/etc/ansible/facts.d/pot.fact'
    src: 'pot_local.fact'
    mode: '0755'
  become: yes

Installing Pot:

- name: Installing Pot
  community.general.pkgng:
    name: pot
    state: present

Collecting facts:

- name: Gathering Facts
  setup:
    filter: ansible_local

The following task gets run in case pot.enabled has been set to true:

- block:
  - name: enable pot service
    community.general.sysrc:
      name: pot_enable
      value: YES
  - name: create pot config
    template:
      src: pot.conf.j2
      dest: /usr/local/etc/pot/pot.conf
      mode: 0644
  - name: initialize pot
    shell: pot init
  <<gather-facts>>
  when:
  - pot.enabled|bool
  - not potintel.initialized|bool

And the following if it has been set to false:

- block:
  - name: de-initialize pot
    shell: pot de-init
  - name: disable pot service
    community.general.sysrc:
      name: pot_enable
      state: absent
  <<gather-facts>>
  when:
  - not pot.enabled|bool
  - potintel.initialized|bool

VNET Initialisation:

- block:
  - name: initialize vnet
    file:
      path: '/usr/local/etc/ansible/.pot_vnet_init'
      state: touch
  - shell: pot vnet-start
  <<gather-facts>>
  when:
  - pot.enabled|bool
  - not pot.vnet_enabled|bool
  - not potintel.vnet_initialized|bool

Collected Variables

VariableDefaultInfo
initializedIf pot init has been run already.
vnet_initializedIf pot vnet-start has been run already.
versionThe pot version.
fscomps[]
bridges[]
bases[]
jails{}A JSON list of the data returned by pot info -p; keys are the jail names.
---
potintel:
  <<gen-vars-varlist(srctbl=pot-intel,prefix="ansible_local.pot")>>

Collecting Script

I’ve split up the shell script into multiple parts to make it easier understandable.

Variables

First, there are the scripts to determine variables. We start with determining the root directory of Pot:

pot config -g fs_root | awk '{print $3}'
[ -d $(<<sh-pot-root>>) ] && echo true || echo false
[ -f /usr/local/etc/ansible/.pot_vnet_init ] && echo true || echo false
pot version | awk '{print $3}'
pot info -p "${j}" | grep active | awk -F' : ' '{print $2}'

Generating Config JSON

Goal: generate JSON data for Ansible from the jail’s pot.conf file. The format is already quite well. The first thing we have to do is to remove the quotes from the file.

cat "$(<<sh-pot-root>>)/jails/${j}/conf/pot.conf" | sed -r 's/"//g'

This awk script converts a list of key-value pairs into almost valid JSON:

BEGIN{print "{"} {print "\"" $1 "\": \"" $2 "\""} END{print "}"}

We take that script, and hand it to Awk with a few extra arguments: the comma as *O*utput *R*ecord *S*eparator, and the = as *F*ield separator.

awk -vORS=, -F'=' '<<awk-jsonize>>'

We also have to remove the superfluous commas after the opening { and before the closing }.

sed -r 's/\{,/\{/' | sed -r 's/,\},/\}/'

And to finish it all off, we turn the YES, true, NO, and false values into proper booleans.

sed -r 's/"(YES|true)"/true/g' | sed -r 's/"(NO|false)"/false/g'
(format "if [ ${#%s} -gt 0 ]; then
    %s_sep=''
    echo -n ', \"%s\": ['
    for x in ${%s}; do
        echo -n ${%s_sep} '\"'${x}'\"'
        %s_sep=', '
    done
    echo -n ']'
fi" varname varname varname varname varname varname)

Script Assembly

pot_root=$(pot config -g fs_root | awk '{print $3}')
fscomps=$(ls "${pot_root}/fscomp")
bridges=$(ls "${pot_root}/bridges")
bases=$(ls "${pot_root}/bases")
jails=$(ls "${pot_root}/jails")

echo -n '{'
echo -n '"initialized": ' $(<<sh-pot-initialized>>) ','
echo -n '"vnet_initialized": ' $(<<sh-vnet-initialized>>) ','
echo -n '"version": "' $(<<sh-pot-version>>) '"'
<<sh-simple-array(varname="fscomps")>>
<<sh-simple-array(varname="bridges")>>
<<sh-simple-array(varname="bases")>>
if [ ${#jails} -gt 0 ]; then
jails_sep=''
echo -n ', "jails":  {'
for j in ${jails}; do
    echo -n ${jails_sep} '"'${j}'":  {'
    echo -n '"active": ' $(<<sh-jail-active>>) ','
    echo -n '"config": ' $(<<potconf-quote-removal>> | <<sh-awk-jsonize>> | <<sh-json-cleanup>> | <<sh-boolean-conv>>)
    echo -n '}'
    jails_sep=','
done
echo -n '}'
fi
echo '}'

Pot Configuration Template

# {{ ansible_managed }}
# pot configuration file

# All datasets related to pot use the some zfs dataset as parent
# With this variable, you can choose which dataset has to be used
POT_ZFS_ROOT={{ pot.zfs_root|default("zroot/pot") }}

# It is also important to know where the root dataset is mounted
POT_FS_ROOT={{ pot.fs_root|default("/opt/pot") }}

# This is the cache used to import/export pots
POT_CACHE={{ pot.cache|default("/var/cache/pot") }}

# This is where pot is going to store temporary files
POT_TMP={{ pot.tmp|default("/tmp") }}

# This is the suffix added to temporary files created using mktemp,
# X is a placeholder for a random character, see mktemp(1)
POT_MKTEMP_SUFFIX={{ pot.mktemp_suffix|default(".XXXXXXXX") }}

# Define the max length of the hostname inside the pot
POT_HOSTNAME_MAX_LENGTH={{ pot.hostname_max_length|default(64) }}

# Internal Virtual Network configuration

# IPv4 Internal Virtual network
POT_NETWORK={{ pot.network|default("10.192.0.0/10") }}

# Internal Virtual Network netmask
POT_NETMASK={{ pot.netmask|default("255.192.0.0") }}

# The default gateway of the Internal Virtual Network
POT_GATEWAY={{ pot.gateway|default("10.192.0.1") }}

# The name of the network physical interface, to be used as default gateway
POT_EXTIF={{ pot.extif|default("em0") }}

{% if "extra_extif" in pot %}
# The list of extra network interface, to make other network segments accessible
POT_EXTRA_EXTIF={%- for item in pot.extra_extif %}{{ item.name }} {%- endfor %}

# for each extra interface, a variable is used to sepcify its network segment
{% for item in pot.extra_extif %}
POT_NETWORK_{{ item.name }}={{ item.netmask }}
{% endfor %}
{% else %}
# POT_EXTRA_EXTIF=expl0
# POT_NETWORK_expl0=
{% endif %}

# DNS on the Internal Virtual Network

# name of the pot running the DNS
POT_DNS_NAME={{ pot.dns_name|default() }}

# IP of the DNS
POT_DNS_IP={{ pot.dns_ip|default() }}

# VPN support

# name of the tunnel network interface
POT_VPN_EXTIF={{ pot.vpn_extif|default() }}

{% if "vpn_networks" in pot %}
POT_VPN_NETWORKS={%- for item in pot.vpn_networks %}{{ item }} {%- endfor %}
{% else %}
# POT_VPN_NETWORKS=
{% endif %}

# EOF

Plugins

Bridges Module

Pot bridges created with pot create-private-bridge.

VariableTypeChoicesRequired?DefaultInfo
namestr#tNoneThe bridge name
sizeint#fNoneexpected number of hosts
statestr‘present’, ‘absent’#f‘present’
ignorebool#fFalse

Examples

- name: Create private bridge
  pot_bridge:
    name: mybridge
    size: 5
- name: Check if creation was successful
  shell:
    cmd: if [ -f /opt/pot/bridges/mybridge ]; then exit 0; else exit 1; fi
  register: bridgetest
- name: Assert test result
  assert:
    that:
      - bridgetest.rc == 0

Bridge Plugin

Bridge creation arguments:

ArgumentSwitchTypePlugin-side Default
name-BsingleNone
size-SsingleNone
def create(self, tmp, task_vars):
    exists_path = self.pot_root(tmp, task_vars)+'/bridges/'+self._task.args.get('name')
    cmd = ["$(which pot)", "create-private-bridge"]
    <<cmdswitches(srctbl=bridge-create-args,dict="cmd")>>
    cmd = ' '.join(cmd)
    display.vvv('Creating bridge using %s' % cmd)
    return <<py_shell(creates="exists_path")>>

Bridge destruction:

def destroy(self, tmp, task_vars):
    exists_path = self.pot_root(tmp, task_vars)+'/bridges/'+self._task.args.get('name')
    cmd = ["$(which pot)", "destroy", "-B", self._task.args.get('name')]
    cmd = ' '.join(cmd)
    display.vvv('Destroying bridge using %s' % cmd)
    return <<py_shell(removes="exists_path")>>

Plugin:

<<action-header>>

class ActionModule(ActionBase):

    <<py_pot_root>>
    <<bridge_create>>
    <<bridge_destroy>>
    def run(self, tmp=None, task_vars=None):
        result = super(ActionModule, self).run(tmp, task_vars)
        state = self._task.args.get('state', 'present')
        if state == 'present':
            result.update(self.create(tmp, task_vars))
        if state == 'absent':
            result.update(self.destroy(tmp, task_vars))
        return result

Module:

<<action-module-header>>

DOCUMENTATION = r"""
<<bridge-docstr>>
"""

EXAMPLES = r"""
<<bridge-examples>>
"""

FS Components Module

The ones created with pot create-fscomp.

VariableTypeChoicesRequired?DefaultInfo
namestr#tNoneThe fscomp name
statestr‘present’, ‘absent’#f‘present’
ignorebool#fFalseIgnore this task?

Examples

FSComp Plugin

FS Component creation arguments:

ArgumentSwitchTypePlugin-side Default
name-fSingleNone
def create(self, tmp, task_vars):
    exists_path = self.pot_root(tmp, task_vars)+'/fscomp/'+self._task.args.get('name')
    cmd = ['$(which pot)', 'create-fscomp']
    <<cmdswitches(srctbl=fscomp-create-args,dict="cmd")>>
    cmd = ' '.join(cmd)
    return <<py_shell(creates="exists_path")>>

And destroying FS Components:

def destroy(self, tmp, task_vars):
    exists_path = self.pot_root(tmp, task_vars)+'/fscomp/'+self._task.args.get('name')
    cmd = ['$(which pot)', 'destroy', '-f', self._task.args.get('name')]
    cmd = ' '.join(cmd)
    return <<py_shell(removes="exists_path")>>

Plugin:

<<action-header>>

class ActionModule(ActionBase):

    <<py_pot_root>>
    <<fscomp_create>>
    <<fscomp_destroy>>
    def run(self, tmp=None, task_vars=None):
        result = super(ActionModule, self).run(tmp, task_vars)
        state = self._task.args.get('state')
        if state == 'present':
            result.update(self.create(tmp, task_vars))
        if state == 'absent':
            result.update(self.destroy(tmp, task_vars))
        return result

Module:

<<action-module-header>>

DOCUMENTATION = r"""
<<fscomp-docstr>>
"""

EXAMPLES = r"""
"""

Bases Module

The ones created with pot create-base.

VariableTypeChoicesRequired?DefaultInfo
namestr#tNoneThe base name
releasestr#tNoneThe FreeBSD release to use
statestr‘present’, ‘absent’#f‘present’
ignorebool#fFalseIgnore this task?

Examples

Base Plugin

Base creation arguments:

ArgumentSwitchTypePlugin-side Default
name-bsingleNone
release-rsingleNone
def create(self, tmp, task_vars):
    exists_path = self.pot_root(tmp, task_vars)+'/fscomp/'+self._task.args.get('name')
    cmd = ['$(which pot)', 'create-base']
    <<cmdswitches(srctbl=base-create-args,dict="cmd")>>
    cmd = ' '.join(cmd)
    return <<py_shell(creates="exists_path")>>

Destroying a basejail:

def destroy(self, tmp, task_vars):
    exists_path = self.pot_root(tmp, task_vars)+'/fscomp/'+self._task.args.get('name')
    cmd = ['$(which pot)', 'destroy', '-br', self._task.args.get('name')]
    cmd = ' '.join(cmd)
    return <<py_shell(removes="exists_path")>>

Plugin:

<<action-header>>

class ActionModule(ActionBase):

    <<py_pot_root>>
    <<base_create>>
    <<base_destroy>>
    def run(self, tmp=None, task_vars=None):
        result = super(ActionModule, self).run(tmp, task_vars)
        state = self._task.args.get('state')
        if state == 'present':
            result.update(self.create(tmp, task_vars))
        if state == 'absent':
            result.update(self.destroy(tmp, task_vars))
        return result
<<action-module-header>>

DOCUMENTATION = r"""
<<base-docstr>>
"""

EXAMPLES = r"""
"""

Jails Module

For each jail, you can supply a number of arguments.

VariableTypeChoicesRequired?DefaultInfo
namestr#tNoneThe jail name
statestr‘present’, ‘absent’, ‘started’, ‘stopped’, ‘restarted’#f‘present’
ignorebool#fFalseIgnore this task?
ipliststr#f[]Defaults to auto
network_stackstr‘ipv4’, ‘ipv6’, ‘dual’#f‘dual’
network_typestr‘inherit’, ‘alias’, ‘public-bridge’, ‘private-bridge’#f‘inherit’
bridge_namestr#fNone
basestr#tNone
potstr#fNone
typestr‘single’, ‘multi’#f‘multi’
levelint#fNone
flavourliststr#f[‘ansible-managed’]
mountslistdict#f[]Things to mount
portslistdict#f[]Ports to map
attributesdict#f{}Attributes

Options for mounts:

VariableTypeChoicesRequired?DefaultInfo
targetpath#tNoneMount point
dirpath#fNoneDirectory on the host to mount
fscompstr#fNonefscomp to mount
datasetstr#fNoneZFS dataset to mount
directbool#fFalsechange the ZFS mount point instead of using nullfs
modestr‘ro’, ‘rw’#f‘rw’Mount as read-only or read-write?

Options for ports:

VariableTypeChoicesRequired?DefaultInfo
protocolstr‘tcp’, ‘udp’#f‘tcp’
portint#tNoneThe port to export
pot_portint#fNonedynamically allocated by default

Return values:

VariableTypeInfo
ipstrThe assigned IP address

Examples

Jail Module

Determining if the jail already exists:

def jail_exists(self, tmp, task_vars):
    cmd = ' '.join(['$(which pot)', 'ls'])
    display.vvv('Determining if jail exists')
    result = <<py_shell()>>
    filtered = filter(lambda x: x.startswith("pot name"), result['stdout'].split("\n"))
    return self._task.args.get('name') in list(map(lambda x: x.split(":")[1].strip(), filtered))

A helper function to extract infos from pot info -p:

def get_info(self, tmp, task_vars, key):
    cmd = ' '.join(['$(which pot)', 'info', '-p', self._task.args.get('name')])
    result = <<py_shell()>>
    splat = map(lambda x: x.strip(), result['stdout'].split("\n"))
    filtered = list(filter(lambda x: x.startswith(key), splat))
    return filtered[0].split(":")[1].strip()

Creating a jail accepts a number of arguments:

ArgumentSwitchTypePlugin-side Default
name-psingleNone
ip-imulti[]
dns-dsingleNone
base-bsingleNone
type-tsingleNone
flavour-fmulti[‘ansible-managed’]
pot-PsingleNone
level-lsingleNone
network_type-NsingleNone
network_stack-SsingleNone
bridge_name-BsingleNone
def create(self, result, tmp, task_vars):
    if self.jail_exists(tmp, task_vars):
        return result
    exists_path = self.pot_root(tmp, task_vars)+'/jails/'+self._task.args.get('name')
    cmd = ['$(which pot)', 'create']
    <<cmdswitches(srctbl=jail-create-args,dict="cmd")>>
    display.vvv("Prepared jail creation command: %s" % cmd)
    cmd = ' '.join(cmd)
    <<py_shell(creates="exists_path")>>
    result['changed'] = True
    return result

Destroying a jail requires that the jail state has been set to 'absent' and that the jail is defined in the first place.

def destroy(self, result, tmp, task_vars):
    if not self.jail_exists(tmp, task_vars):
        return result
    exists_path = self.pot_root(tmp, task_vars)+'/jails/'+self._task.args.get('name')
    cmd = ' '.join(['$(which pot)', 'destroy', '-rp', self._task.args.get('name')])
    <<py_shell(removes="exists_path")>>
    result['changed'] = True
    return result

Stopping a jail:

def stop(self, result, tmp, task_vars):
    cmd = ' '.join(['$(which pot)', 'stop', self._task.args.get('name')])
    if self.get_info(tmp, task_vars, 'active') == 'true':
        <<py_shell()>>
        result['changed'] = True
    return result

Starting a jail:

def start(self, result, tmp, task_vars):
    cmd = ' '.join(['$(which pot)', 'start', self._task.args.get('name')])
    if self.get_info(tmp, task_vars, 'active') == 'false':
        <<py_shell()>>
        result['changed'] = True
    return result

Mounting things in jails:

ArgumentSwitchTypePlugin-side Default
target-msingleNone
dir-dsingleNone
fscomp-fsingleNone
dataset-zsingleNone
direct-wflagFalse
mode-rflagFalse
def has_mount(self, tmp, task_vars, mountpoint, mounttarget):
    jaildir = self.pot_root(tmp, task_vars)+'/jails/'+self._task.args.get('name')
    jailroot = jaildir+'/m'
    mountline = mounttarget+' '+jailroot+mountpoint
    cmd = ' '.join(['cat', jaildir+'/conf/fscomp.conf', '|', 'awk \'{ print $1 " " $2 }\''])
    result = <<py_shell()>>
    res = list(filter(lambda x: x == mountline, result['stdout'].split("\n")))
    return len(res) > 0
def mounts(self, result, tmp, task_vars):
    mounts = self._task.args.get('mounts', None)
    if not mounts:
        return result
    for mount in mounts:
        mounttarget = ""
        if "dir" in mount:
            mounttarget = mount["dir"]
        elif "fscomp" in mount:
            mounttarget = self.pot_zfs_root(tmp, task_vars)+'/fscomp/'+mount["fscomp"]
        elif "dataset" in mount:
            mounttarget = mount["dataset"]

        if not self.has_mount(tmp, task_vars, mount["target"], mounttarget):
            cmd = ['$(which pot)', 'mount-in', '-p', self._task.args.get('name')]
            if "mode" in mount and mount["mode"] != "ro":
                mount.pop("mode")
            <<cmdswitches(srctbl=jail-mounts-args,dict="cmd",kwargs="mount")>>
            cmd = ' '.join(cmd)
            result.update(<<py_shell()>>)
    return result

Mapping ports to jails:

def map_ports(self, result, tmp, task_vars):
    display.vvv('Mapping Jail Ports')
    ports = self._task.args.get('ports', None)
    if not ports:
        return result
    pmcmd = ['$(which pot)', 'export-ports', '-p', self._task.args.get('name')]
    portlist = []
    for port in ports:
        portstr = "{0}".format(port["port"])
        if "protocol" in port:
            portstr = "{0}:{1}".format(port["protocol"], portstr)
        if "pot_port" in port:
            portstr = "{0}:{1}".format(portstr, port["pot_port"])
        pmcmd.append('-e')
        pmcmd.append(portstr)
        portlist.append(portstr)
    cmd = ' '.join(['$(which pot)', 'info', '-vp', self._task.args.get('name')])
    out = <<py_shell()>>
    out = list(filter(lambda x : 'exported ports' in x, out['stdout'].split('\n')))
    if len(out) > 0 and 'exported ports' in out[0]:
        out = out[0].split('exported ports:')[1].strip()
    else:
        out = ''
    display.vvv('Comparing %s with %s' % (out, ' '.join(portlist)))
    if out == ' '.join(portlist):
        return result
    cmd = ' '.join(pmcmd)
    <<py_shell()>>
    result["changed"] = True
    return result

Attributes management:

def set_attributes(self, result, tmp, task_vars):
    display.vvv('Setting Jail Attributes')
    attrs = self._task.args.get('attributes', None)
    if not attrs:
        return result
    cmd = ' '.join(['$(which pot)', 'info', '-vp', self._task.args.get('name')])
    out = <<py_shell()>>
    display.vvv('Splitting %s by "jail attributes:"' % out['stdout'])
    pot_attr_list = list(filter(lambda x: ':' in x, out['stdout'].split('jail attributes:')[1].split('\n')))
    pot_attrs = {}
    for pot_attr in pot_attr_list:
        display.vvv('Splitting %s' % pot_attr)
        pot_attr = pot_attr.split(':')
        pot_attr[0] = pot_attr[0].strip()
        pot_attr[1] = pot_attr[1].strip()
        if pot_attr[1] == 'YES':
            pot_attr[1] = True
        if pot_attr[1] == 'NO':
            pot_attr[1] = False
        pot_attrs[pot_attr[0]] = pot_attr[1]
    for attrk in attrs.keys():
        if attrs[attrk] in ['YES', 'Yes', 'yes', 'true', 'True', 'TRUE']:
            attrs[attrk] = True
        if attrs[attrk] in ['NO', 'No', 'no', 'false', 'False', 'FALSE']:
            attrs[attrk] = False
    for attrk in attrs.keys():
        if attrk in pot_attrs and pot_attrs[attrk] == attrs[attrk]:
            continue
        else:
            cmd = ' '.join(['$(which pot)', 'set-attribute', '-p', self._task.args.get('name'), '-A', attrk, '-V', '%s' % attrs[attrk]])
            newres = <<py_shell()>>
            result['changed'] = True
    return result

Plugin:

<<action-header>>

class ActionModule(ActionBase):

    <<py_pot_root>>
    <<py_pot_zfs_root>>
    <<jail_exists>>
    <<jail_getinfo>>
    <<jail_has_mount>>
    <<jail_create>>
    <<jail_destroy>>
    <<jail_stop>>
    <<jail_start>>
    <<jail_mounts>>
    <<jail_portmap>>
    <<jail_attributes>>
    def run(self, tmp=None, task_vars=None):
        result = super(ActionModule, self).run(tmp, task_vars)
        state = self._task.args.get('state')
        if state in ['present', 'stopped', 'started', 'restarted']:
            result = self.create(result, tmp, task_vars)
        if state in ['stopped', 'restarted', 'absent']:
            result = self.stop(result, tmp, task_vars)
        if state in ['absent']:
            result = self.destroy(result, tmp, task_vars)
        if state != 'absent':
            result = self.mounts(result, tmp, task_vars)
            result = self.map_ports(result, tmp, task_vars)
            result = self.set_attributes(result, tmp, task_vars)
        if state in ['started', 'restarted']:
            result = self.start(result, tmp, task_vars)
        if state != 'absent':
            result['ip'] = self.get_info(tmp, task_vars, 'ip')
        return result

Module:

<<action-module-header>>

DOCUMENTATION = r"""
<<jail-docstr>>
"""

EXAMPLES = r"""
"""

ansible-managed Flavour

A freshly created pot is somewhat useless if you want to manage it with Ansible, because there is no Python installation, and no sudo.

pkg install -y python3 sudo
pkg clean -ayq
- name: Install ansible-managed Flavour
  copy:
    dest: '/usr/local/etc/pot/flavours/ansible-managed.sh'
    src: 'ansible-managed.sh'
    mode: '0755'
  become: yes

Inventory

Pot Connection

This collection also provides a connection plugin to execute commands inside a Pot. Two variants are provided: one for local pots, and one for remote pots.

Local Pots

VariableTypeChoicesRequired?DefaultInfo
ansible_hoststr#finventory_hostnameName of the jail
ansible_userstr#fUser inside the jail to run as

Examples

Pot Connection

def __init__(self, play_context, new_stdin, *args, **kwargs):
    super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
    self.executable = "/usr/local/bin/pot"
    self.jail = self._play_context.remote_host

    if os.geteuid() != 0:
        raise AnsibleError("jail connection requires running as root")
    if self.jail not in self.list_jails():
        raise AnsibleError("jail %s does not exist" % self.jail)

We need to have a list of all jails.

def list_jails(self):
    rc, out, err = self._exec([self.executable, 'ls'])
    filtered = filter(lambda x: x.startswith("pot name"), out.split("\n"))
    jailnames = map(lambda x: x.split(":")[1].strip(), filtered)
    return jailnames

We have to do three things to implement ConnectionBase. The main one is executing a command:

def exec_command(self, cmd, in_data=None, sudoable=False):
    super(Connection, self).exec_command(cmd, in_data, sudoable)
    display.vvv("In jail %s: exec %s" % (self.jail, cmd))
    rc, out, err = self._exec([self.executable, 'exec', '-p', self.jail, cmd])
    return rc, out, err

We also need to provide facilities to put and fetch files:

def put_file(self, in_path, out_path):
    super(Connection, self).put_file(in_path, out_path)
    display.vvv("In jail %s: put %s to %s" % (self.jail, in_path, out_path))
    rc, out, err = self._exec([self.executable, 'copy-in', '-p', self.jail, '-s', in_path, '-d', out_path])
def fetch_file(self, in_path, out_path):
    super(Connection, self).fetch_file(in_path, out_path)
    display.vvv("In jail %s: fetch %s to %s" % (self.jail, in_path, out_path))
    rc, out, err = self._exec([self.executable, 'copy-out', '-p', self.jail, '-s', in_path, '-d', out_path])

The whole plugin:

<<connection-header>>

DOCUMENTATION = r"""
<<potconn-local-docstr>>
"""

EXAMPLES = r"""
"""


class Connection(ConnectionBase):
    transport = 'zilti.pot.pot'
    has_pipelining = True
    has_tty = False

    <<py_potconn_local__init>>
    <<py__exec>>
    <<py_potconn_local_list_jails>>
    <<py_potconn_local_exec_command>>
    <<py_potconn_local_put_file>>
    <<py_potconn_local_fetch_file>>

Remote Pots

Connecting to remote pots works almost like the SSH connection plugin - it is an extension of it. The difference is that you have to specify the name of the pot, and of course tell Ansible to use the zilti.pot.pot_remote connection plugin. Here’s an example inventory file:

[jails]
[email protected] ansible_connection=zilti.pot.pot_remote

Be aware that the connection plugin will need to use a become plugin to copy files into and out of the pot.

Lookup Plugin

This lookup plugin is currently in a testing phase.

Lookup Plugin

<<lookup-header>>

class LookupModule(LookupBase):
    def run(self, terms, variables=None, **kwargs):
        self.set_options(var_options=variables, direct=kwargs)
        paramvals = self.get_options()
        cmd = '/usr/local/etc/ansible/facts.d/pot.fact'
        potfact = <<py_shell()>>
        potfact = json.loads(potfact.stdout)
        potname = paramvals['pot']

        if 'active' in paramvals:
            return potfact['jails'][potname]['active']

        attr = paramvals['attribute']
        return potfact['jails'][potname]['config'][attr]

Dependencies

Needs the community.general collection.

Example Playbook

- hosts: all
  become: yes
  remote_user: root
  roles:
  - role: zilti.pot.pot
    vars:
      pot:
        enabled: true
        vnet_enabled: true
        zfs_root: tank/pot
        extif: vtnet0
  tasks:
  - zilti.pot.pot_base:
      name: 13.1
      release: 13.1

  - zilti.pot.pot_fscomp:
      name: testfs

  - zilti.pot.pot_jail:
      name: testpot1
      base: 13.1
      type: single
      state: started
      mounts:
      - target: /opt
        fscomp: testfs

License

GPL3.0

Author Information

Daniel Ziltener, Code & Magic UG

Ansible Galaxy Metadata

requires_ansible: ">=2.9"
namespace: zilti
name: pot
version: 0.5.34

authors:
  - Daniel Ziltener <[email protected]>

dependencies:
  community.general: "*"

tags:
  - freebsd
  - jails
  - pot

readme: README.md
license: GPL-3.0-or-later
description: Roles and modules for installing and using Pot

repository: https://github.com/zilti/ansible-pot
issues: https://github.com/zilti/ansible-pot/issues
documentation: https://github.com/zilti/ansible-pot
homepage: https://github.com/zilti/ansible-pot
galaxy_info:
  author: Daniel Ziltener
  description: A role to manage Pot jails
  company: Code & Magic UG

  # If the issue tracker for your role is not on github, uncomment the
  # next line and provide a value
  # issue_tracker_url: http://example.com/issue/tracker

  # Choose a valid license ID from https://spdx.org - some suggested licenses:
  # - BSD-3-Clause (default)
  # - MIT
  # - GPL-2.0-or-later
  # - GPL-3.0-only
  # - Apache-2.0
  # - CC-BY-4.0
  license: GPL-3.0-or-later

  min_ansible_version: 2.9

  # If this a Container Enabled role, provide the minimum Ansible Container version.
  # min_ansible_container_version:

  #
  # Provide a list of supported platforms, and for each platform a list of versions.
  # If you don't wish to enumerate all versions for a particular platform, use 'all'.
  # To view available platforms and versions (or releases), visit:
  # https://galaxy.ansible.com/api/v1/platforms/

  platforms:
  - name: FreeBSD
    versions:
    - all

  galaxy_tags:
  - freebsd
  - jails
    # List tags for your role here, one per line. A tag is a keyword that describes
    # and categorizes the role. Users find roles by searching for tags. Be sure to
    # remove the '[]' above, if you add tags to this list.
    #
    # NOTE: A tag is limited to a single word comprised of alphanumeric characters.
    #       Maximum 20 tags per role.

dependencies: []
  # List your role dependencies here, one per line. Be sure to remove the '[]' above,
  # if you add dependencies to this list.

Helper Code

# -*- Coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import os
import re
import subprocess
from os.path import exists
from ansible.module_utils.basic import AnsibleModule


__metaclass__ = type
# -*- Coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
# -*- Coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import subprocess
from ansible.errors import AnsibleAction, AnsibleActionFail
from ansible.plugins.action import ActionBase
from ansible.utils.display import Display

display = Display()
# -*- Coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import os
import pipes
from ansible.errors import AnsibleError
from ansible.plugins.connection.ssh import Connection as SSHConnection
from ansible.module_utils._text import to_text
from ansible.plugins.loader import get_shell_plugin
from ansible.utils.display import Display
from contextlib import contextmanager

display = Display()


__metaclass__ = type
# -*- Coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from ansible.utils.display import Display
import json

display = Display()
(concat "arg_spec = dict(\n"
        (mapconcat
         (lambda (row)
           (let* ((variable (cl-first row))
                  (type (cl-second row))
                  (choices (cl-third row))
                  (requiredp (cl-fourth row))
                  (default (cl-fifth row)))
             (if (> (length choices) 0)
                 (if (string= type "list")
                     (format "    %s=dict(default=%s, type=%S, elements=%S)" variable default type choices)
                     (format "    %s=dict(default=%s, type=%S, choices=[%s])" variable default type choices))
               (format "    %s=dict(default=%s, type=%S)" variable default type))
             ))
         srctbl ",\n")
        ")\nmodule = AnsibleModule(argument_spec=arg_spec, supports_check_mode=True)")
(mapconcat
 (lambda (row)
   (let* ((variable (cl-first row))
          (type (cl-second row))
          (choices (cl-third row))
          (requiredp (cl-fourth row))
          (default (cl-fifth row))
          (description (cl-sixth row)))
     (concat variable ":\n"
             (when (> (length description) 0)
               (concat
                "    description:\n"
                "      - " description "\n"))
             "    type: " type "\n"
             "    required: " (if (string= requiredp "#t") "True" "False") "\n"
             (when (> (length default) 0)
               (concat "    default: " default "\n"))
             (when (> (length choices) 0)
               (if (string= type "list")
                   (concat "    elements: " choices "\n")
                   (concat "    choices: [ " choices " ]\n")))
             )
     ))
 srctbl "")
(mapconcat
 (lambda (row)
   (format "%s: %s" (cl-first row) (cl-fifth row)))
 srctbl "\n")
(concat (mapconcat
         (lambda (row)
           (let ((argument (cl-first row))
                 (switch (cl-second row))
                 (type (cl-third row))
                 (default (cl-fourth row)))
             (if (string= type "multi")
                 (concat (format "for elem in %s.get(%S, %s):\n" kwargs argument default)
                         (format "    %s.append(%S)\n" dict switch)
                         (format "    %s.append('%%s' %% elem)\n" dict))
               (concat (format "if %s.get(%S, %s):\n" kwargs argument default)
                       (format "    %s.append(%S)\n" dict switch)
                       (when (not (string= type "flag"))
                       (format "    %s.append('%%s' %% %s.get(%S, %s))\n" dict kwargs argument default))))))
         srctbl "\n"))
(mapconcat
 (lambda (row)
   (format "%s: '{{ %s.%s|default(%S) }}'" (car row) prefix (car row) (cadr row)))
 srctbl "\n")
(mapconcat
 (lambda (row)
   (let ((arg (car row))
         (switch (cadr row)))
     (format "{%% if %s.%s|length %%} %s {{ %s.%s }}{%% endif %%} \\ "
             prefix arg switch prefix arg)))
 srctbl "\n")