I am a role to manage your Pot jails on FreeBSD. My source is located in the pot.org file.
None.
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()
Variable | Type | Choices | Required? | Default | Info |
---|---|---|---|---|---|
enabled | bool | False | Triggers pot init | ||
vnet_enabled | bool | False | Triggers pot vnet-start | ||
zfs_root | str | ‘tank/pot’ | Is written to pot.conf | ||
fs_root | str | ‘/opt/pot’ | Is written to pot.conf | ||
cache | str | ‘/var/cache/pot’ | Is written to pot.conf | ||
tmp | str | ‘/tmp’ | Is written to pot.conf | ||
mktemp_suffix | str | ‘.XXXXXXXX’ | Is written to pot.conf | ||
hostname_max_length | int | 64 | Is written to pot.conf | ||
network | str | ‘10.192.0.0/10’ | Is written to pot.conf | ||
netmask | str | ‘255.192.0.0’ | Is written to pot.conf | ||
gateway | str | ‘10.192.0.1’ | Is written to pot.conf | ||
extif | str | ‘em0’ | Is written to pot.conf |
---
pot:
<<gen-defaults-varlist(srctbl=server-default-vars)>>
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
Variable | Default | Info |
---|---|---|
initialized | If pot init has been run already. | |
vnet_initialized | If pot vnet-start has been run already. | |
version | The 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")>>
I’ve split up the shell script into multiple parts to make it easier understandable.
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}'
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)
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 '}'
# {{ 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
Pot bridges created with pot create-private-bridge
.
Variable | Type | Choices | Required? | Default | Info |
---|---|---|---|---|---|
name | str | #t | None | The bridge name | |
size | int | #f | None | expected number of hosts | |
state | str | ‘present’, ‘absent’ | #f | ‘present’ | |
ignore | bool | #f | False |
- 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 creation arguments:
Argument | Switch | Type | Plugin-side Default |
---|---|---|---|
name | -B | single | None |
size | -S | single | None |
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>>
"""
The ones created with pot create-fscomp
.
Variable | Type | Choices | Required? | Default | Info |
---|---|---|---|---|---|
name | str | #t | None | The fscomp name | |
state | str | ‘present’, ‘absent’ | #f | ‘present’ | |
ignore | bool | #f | False | Ignore this task? |
FS Component creation arguments:
Argument | Switch | Type | Plugin-side Default |
---|---|---|---|
name | -f | Single | None |
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"""
"""
The ones created with pot create-base
.
Variable | Type | Choices | Required? | Default | Info |
---|---|---|---|---|---|
name | str | #t | None | The base name | |
release | str | #t | None | The FreeBSD release to use | |
state | str | ‘present’, ‘absent’ | #f | ‘present’ | |
ignore | bool | #f | False | Ignore this task? |
Base creation arguments:
Argument | Switch | Type | Plugin-side Default |
---|---|---|---|
name | -b | single | None |
release | -r | single | None |
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"""
"""
For each jail, you can supply a number of arguments.
Variable | Type | Choices | Required? | Default | Info |
---|---|---|---|---|---|
name | str | #t | None | The jail name | |
state | str | ‘present’, ‘absent’, ‘started’, ‘stopped’, ‘restarted’ | #f | ‘present’ | |
ignore | bool | #f | False | Ignore this task? | |
ip | list | str | #f | [] | Defaults to auto |
network_stack | str | ‘ipv4’, ‘ipv6’, ‘dual’ | #f | ‘dual’ | |
network_type | str | ‘inherit’, ‘alias’, ‘public-bridge’, ‘private-bridge’ | #f | ‘inherit’ | |
bridge_name | str | #f | None | ||
base | str | #t | None | ||
pot | str | #f | None | ||
type | str | ‘single’, ‘multi’ | #f | ‘multi’ | |
level | int | #f | None | ||
flavour | list | str | #f | [‘ansible-managed’] | |
mounts | list | dict | #f | [] | Things to mount |
ports | list | dict | #f | [] | Ports to map |
attributes | dict | #f | {} | Attributes |
Options for mounts:
Variable | Type | Choices | Required? | Default | Info |
---|---|---|---|---|---|
target | path | #t | None | Mount point | |
dir | path | #f | None | Directory on the host to mount | |
fscomp | str | #f | None | fscomp to mount | |
dataset | str | #f | None | ZFS dataset to mount | |
direct | bool | #f | False | change the ZFS mount point instead of using nullfs | |
mode | str | ‘ro’, ‘rw’ | #f | ‘rw’ | Mount as read-only or read-write? |
Options for ports:
Variable | Type | Choices | Required? | Default | Info |
---|---|---|---|---|---|
protocol | str | ‘tcp’, ‘udp’ | #f | ‘tcp’ | |
port | int | #t | None | The port to export | |
pot_port | int | #f | None | dynamically allocated by default |
Return values:
Variable | Type | Info |
---|---|---|
ip | str | The assigned IP address |
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:
Argument | Switch | Type | Plugin-side Default |
---|---|---|---|
name | -p | single | None |
ip | -i | multi | [] |
dns | -d | single | None |
base | -b | single | None |
type | -t | single | None |
flavour | -f | multi | [‘ansible-managed’] |
pot | -P | single | None |
level | -l | single | None |
network_type | -N | single | None |
network_stack | -S | single | None |
bridge_name | -B | single | None |
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:
Argument | Switch | Type | Plugin-side Default |
---|---|---|---|
target | -m | single | None |
dir | -d | single | None |
fscomp | -f | single | None |
dataset | -z | single | None |
direct | -w | flag | False |
mode | -r | flag | False |
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"""
"""
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
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.
Variable | Type | Choices | Required? | Default | Info |
---|---|---|---|---|---|
ansible_host | str | #f | inventory_hostname | Name of the jail | |
ansible_user | str | #f | User inside the jail to run as |
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>>
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.
This lookup plugin is currently in a testing phase.
<<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]
Needs the community.general
collection.
- 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
GPL3.0
Daniel Ziltener, Code & Magic UG
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.
# -*- 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")