Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add virtual-ip module #68

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 313 additions & 0 deletions plugins/modules/pfsense_virtualip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2018, Frederic Bor <[email protected]>
# Copyright: (c) 2021, Jan Wenzel <[email protected]>
# Copyright: (c) 2023, Martin Müller <[email protected]>
# Copyright: (c) 2025, Orion Poplawski <[email protected]>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}

DOCUMENTATION = """
---
module: pfsense_virtualip
version_added: "0.6.2"
author: Jan Wenzel (@coffeelover)
short_description: Manage pfSense virtual ip settings
description:
- Manage pfSense virtual ip settings.
notes:
options:
mode:
description: Type of the virtual ip.
required: True
type: str
choices: [ "proxyarp", "carp", "ipalias", "other" ]
descr:
description: Description of the virtual ip.
required: False
type: str
interface:
description: Interface of the virtual ip.
required: True
type: str
vhid:
description: VHID Group of the virtual ip. Required if `mode` is `carp`, unused for others. Valid value is from 1 to 255.
required: False
type: int
advbase:
description:
- Advertising frequency base. Used with `mode` of `carp`. The frequency that this machine will advertise. 0 means usually master.
- Otherwise the lowest combination of both base and skew values in the cluster determines the master.
- Valid value is from 1 to 254.
required: False
type: int
advskew:
description: Advertising frequency skew.
- Advertising frequency skew. Used with `mode` of `carp`. The frequency that this machine will advertise. 0 means usually master.
- Otherwise the lowest combination of both base and skew values in the cluster determines the master.
- Valid value is from 0 to 254.
required: False
type: int
password:
description: Virtual IP password. Required if `mode` is `carp`, unused for others.
required: False
type: str
type:
description: Address type - `single` or `network. Used for `proxyarp` or `other`.
required: False
type: str
choices: [ "single", "network" ]
default: single
subnet_bits:
description: Network's subnet mask if address type is `network`.
required: False
type: int
default: 32
subnet:
description: Network address or subnet.
required: False
type: str
state:
description: State in which to leave the Virtual IP.
choices: [ "present", "absent" ]
default: present
type: str
"""

EXAMPLES = """
- name: Setup Home vip
pfsense_virtualip:
mode: "carp"
descr: "HOME VIP"
interface: "opt2"
vhid: 24
advbase: 1
advskew: 0
password": "xaequae0sheiB7sh"
subnet_bits": 24
subnet": "10.1.1.1"
state": "present"
"""

RETURN = """
"""

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase


VIRTUALIP_ARGUMENT_SPEC = dict(
mode=dict(required=True, choices=['proxyarp', 'carp', 'ipalias', 'other'], type='str'),
interface=dict(required=True, type='str'),
vhid=dict(type='int'),
advskew=dict(type='int'),
advbase=dict(type='int'),
password=dict(type='str', no_log=True),
descr=dict(type='str'),
type=dict(type='str', choices=['single'], default='single'),
subnet_bits=dict(type='int', default=32),
subnet=dict(type='str'),
state=dict(default='present', choices=['present', 'absent'], type='str'),
)

VIRTUALIP_REQUIRED_IF = [
["mode", "carp", ["password", "vhid", "advbase", "advskew"]],
]


# fields that are not written to pfsense
skip_list = ['state']


class PFSenseVirtualIPModule(PFSenseModuleBase):
""" module managing pfsense virtual ip settings """

@staticmethod
def get_argument_spec():
""" return argument spec """
return VIRTUALIP_ARGUMENT_SPEC

##############################
# init
#
def __init__(self, module, pfsense=None):
super(PFSenseVirtualIPModule, self).__init__(module, pfsense)
self.name = "virtualip"
self.root_elt = self.pfsense.get_element('virtualip')
self.obj = dict()

if self.root_elt is None:
self.root_elt = self.pfsense.new_element('virtualip')
self.pfsense.root.append(self.root_elt)

##############################
# params processing
#
def _params_to_obj(self):
""" return a dict from module params """
params = self.params

obj = dict()
self.obj = obj

def _set_param(target, param):
if params.get(param) is not None:
if isinstance(params[param], str):
target[param] = params[param]
else:
target[param] = str(params[param])

def _set_param_bool(target, param):
if params.get(param) is not None:
value = params.get(param)
if value is True and param not in target:
target[param] = ''
elif value is False and param in target:
del target[param]

for param in VIRTUALIP_ARGUMENT_SPEC:
if param not in skip_list:
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
_set_param_bool(obj, param)
else:
_set_param(obj, param)

return obj

def _validate_params(self):
""" do some extra checks on input parameters """
params = self.params
return

##############################
# XML processing
#
def _create_target(self):
""" create the XML target_elt """
self.obj['uniqid'] = self.pfsense.uniqid()
return self.pfsense.new_element('vip')

def _find_target(self):
""" find the XML target elt """
for vip_elt in self.root_elt:
if self.params['mode'] in ['ipalias', 'carp']:
if vip_elt.find('uniqid') is not None and vip_elt.find('uniqid').text == self.params['uniqid']:
return vip_elt
else:
if vip_elt.find('descr') is not None and vip_elt.find('descr').text == self.params['descr']:
return vip_elt
return None

def _remove_deleted_params(self):
""" Remove from target_elt a few deleted params """
changed = False
for param in VIRTUALIP_ARGUMENT_SPEC:
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
if self.pfsense.remove_deleted_param_from_elt(self.target_elt, param, self.obj):
changed = True

return changed

def _update(self):
""" make the target pfsense reload """
cmd = '''
require_once("globals.inc");
require_once("functions.inc");
require_once("filter.inc");
require_once("shaper.inc");
require_once("interfaces.inc");
require_once("util.inc");
$check_carp = false;
$retval = 0;
'''

if self.params.get('mode') in ['carp', 'ipalias']:
cmd += '$uniqid = "' + self.params.get('uniqid') + '";\n'
cmd += '$subnet = "' + self.params.get('subnet') + '";\n'
cmd += '$interface = "' + self.params.get('interface') + '";\n'
cmd += '$vipif = get_real_interface($interface);\n'

if self.params.get('state') == 'present':
if self.params.get('mode') in ['carp', 'ipalias']:
cmd += '$check_carp = true;\n'
cmd += 'foreach ($config["virtualip"]["vip"] as $vip) {\n'
cmd += 'if ($vip["uniqid"] == $uniqid) {\n'
cmd += 'interface_' + self.params.get('mode') + '_configure($vip);\n'
cmd += '}\n}\n'
else:
if self.params.get('mode') == 'carp':
cmd += 'if (does_interface_exist($vipif)) {\n'
cmd += 'if (is_ipaddrv6($subnet)) {\n'
cmd += 'mwexec("/sbin/ifconfig " . escapeshellarg($vipif) . " inet6 " . escapeshellarg($subnet) . " delete");\n'
cmd += '} else {\n'
cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n'
cmd += '}\n}\n'
elif self.params.get('mode') == 'ipalias':
cmd += 'if (does_interface_exist($vipif)) {\n'
cmd += 'if (is_ipaddrv6($subnet)) {\n'
cmd += 'mwexec("/sbin/ifconfig " . escapeshellarg($vipif) . " inet6 " . escapeshellarg($subnet) . " -alias");\n'
cmd += '} else {\n'
cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n'
cmd += '}\n}\n'

cmd += '''
if ($check_carp === true && !get_carp_status()) {
set_single_sysctl("net.inet.carp.allow", "1");
}
$retval |= filter_configure();
$retval |= mwexec("/etc/rc.filter_synchronize");
clear_subsystem_dirty('vip');'''

return self.pfsense.phpshell(cmd)

##############################
# Logging
#
@staticmethod
def _get_obj_name():
""" return obj's name """
return "vip"

def _log_fields(self, before=None):
""" generate pseudo-CLI command fields parameters to create an obj """
values = ''

if before is None:
for param in VIRTUALIP_ARGUMENT_SPEC:
if param not in skip_list:
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool)
else:
values += self.format_cli_field(self.obj, param)
else:
for param in VIRTUALIP_ARGUMENT_SPEC:
if param not in skip_list:
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
values += self.format_updated_cli_field(self.obj, before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False)
else:
values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False)

return values


def main():
module = AnsibleModule(
argument_spec=VIRTUALIP_ARGUMENT_SPEC,
required_if=VIRTUALIP_REQUIRED_IF,
supports_check_mode=True)

pfmodule = PFSenseVirtualIPModule(module)
pfmodule.run(module.params)
pfmodule.commit_changes()


if __name__ == '__main__':
main()
Loading