Skip to content

Commit 9b72b41

Browse files
committed
feat: add virtual-ip module
1 parent 3cadeaf commit 9b72b41

File tree

1 file changed

+307
-0
lines changed

1 file changed

+307
-0
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright: (c) 2018, Frederic Bor <frederic.bor@wanadoo.fr>
5+
# Copyright: (c) 2021, Jan Wenzel <jan.wenzel@gonicus.de>
6+
# Copyright: (c) 2023, Martin Müller <martin.mueller@dataport.de>
7+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
8+
9+
from __future__ import absolute_import, division, print_function
10+
__metaclass__ = type
11+
12+
13+
ANSIBLE_METADATA = {'metadata_version': '1.1',
14+
'status': ['preview'],
15+
'supported_by': 'community'}
16+
17+
DOCUMENTATION = """
18+
---
19+
module: pfsense_virtual_ip
20+
version_added: "0.6.0"
21+
author: Jan Wenzel (@coffeelover)
22+
short_description: Manage pfSense virtual ip settings
23+
description:
24+
- Manage pfSense virtual ip settings
25+
notes:
26+
options:
27+
mode:
28+
description: Type
29+
required: true
30+
type: str
31+
choices: ['proxyarp', 'carp', 'ipalias', 'other']
32+
noexpand:
33+
description: Disable expansion of this entry into IPs on NAT lists (e.g. 192.168.1.0/24 expands to 256 entries.)
34+
required: false
35+
type: bool
36+
descr:
37+
description: Description
38+
required: false
39+
type: str
40+
interface:
41+
description: Interface
42+
required: true
43+
type: str
44+
vhid:
45+
description: VHID Group
46+
required: false
47+
type: int
48+
advbase:
49+
description: Advertising Frequency Base
50+
required: false
51+
type: int
52+
advskew:
53+
description: Advertising Frequency Skew
54+
required: false
55+
type: int
56+
password:
57+
description: Virtual IP Password
58+
required: false
59+
type: str
60+
uniqid:
61+
description: Unique ID of Virtual IP in configuration
62+
required: false
63+
type: str
64+
type:
65+
description: Address Type
66+
required: false
67+
type: str
68+
choices: ['single']
69+
default: single
70+
subnet_bits
71+
description: Network's subnet mask
72+
required: false
73+
type: int
74+
default: 32
75+
subnet
76+
description: Network subnet
77+
required: false
78+
type: str
79+
state:
80+
description: State in which to leave the Virtual IP
81+
choices: [ "present", "absent" ]
82+
default: present
83+
type: str
84+
"""
85+
86+
EXAMPLES = """
87+
"""
88+
89+
RETURN = """
90+
"""
91+
92+
import re
93+
from copy import deepcopy
94+
from ansible.module_utils.basic import AnsibleModule
95+
from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase
96+
97+
98+
VIRTUALIP_ARGUMENT_SPEC = dict(
99+
mode=dict(required=True, type='str'),
100+
interface=dict(required=True, type='str'),
101+
vhid=dict(required=False, type='int'),
102+
advskew=dict(required=False, type='int'),
103+
advbase=dict(required=False, type='int'),
104+
password=dict(required=False, type='str'),
105+
uniqid=dict(required=False, type='str'),
106+
descr=dict(required=False, type='str'),
107+
type=dict(required=False, type='str', default='single'),
108+
subnet_bits=dict(required=False, type='int', default=32),
109+
subnet=dict(required=False, type='str'),
110+
state=dict(default='present', choices=['present', 'absent'], type='str'),
111+
)
112+
113+
VIRTUALIP_REQUIRED_IF = [
114+
["mode", "carp", ["uniqid", "password", "vhid", "advbase"]],
115+
["mode", "ipalias", ["uniqid"]],
116+
]
117+
118+
# map field names between ansible and pfsense
119+
params_map = {}
120+
121+
# fields that are not written to pfsense
122+
skip_list = ['state']
123+
124+
class PFSenseVirtualIPModule(PFSenseModuleBase):
125+
""" module managing pfsense virtual ip settings """
126+
127+
@staticmethod
128+
def get_argument_spec():
129+
""" return argument spec """
130+
return VIRTUALIP_ARGUMENT_SPEC
131+
132+
##############################
133+
# init
134+
#
135+
def __init__(self, module, pfsense=None):
136+
super(PFSenseVirtualIPModule, self).__init__(module, pfsense)
137+
self.name = "virtual_ip"
138+
self.root_elt = self.pfsense.get_element('virtualip')
139+
self.obj = dict()
140+
141+
if self.root_elt is None:
142+
self.root_elt = self.pfsense.new_element('virtualip')
143+
self.pfsense.root.append(self.root_elt)
144+
145+
##############################
146+
# params processing
147+
#
148+
def _params_to_obj(self):
149+
""" return a dict from module params """
150+
params = self.params
151+
152+
obj = dict()
153+
self.obj = obj
154+
155+
def _set_param(target, param):
156+
if params.get(param) is not None:
157+
if isinstance(params[param], str):
158+
target[param] = params[param]
159+
else:
160+
target[param] = str(params[param])
161+
162+
def _set_param_bool(target, param):
163+
if params.get(param) is not None:
164+
value = params.get(param)
165+
if value is True and param not in target:
166+
target[param] = ''
167+
elif value is False and param in target:
168+
del target[param]
169+
170+
for param in VIRTUALIP_ARGUMENT_SPEC:
171+
if param not in skip_list:
172+
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
173+
_set_param_bool(obj, param)
174+
else:
175+
_set_param(obj, param)
176+
177+
return obj
178+
179+
180+
def _validate_params(self):
181+
""" do some extra checks on input parameters """
182+
params = self.params
183+
return
184+
185+
##############################
186+
# XML processing
187+
#
188+
def _create_target(self):
189+
""" create the XML target_elt """
190+
return self.pfsense.new_element('vip')
191+
192+
def _find_target(self):
193+
""" find the XML target elt """
194+
for vip_elt in self.root_elt:
195+
if self.params['mode'] in ['ipalias', 'carp']:
196+
if vip_elt.find('uniqid') is not None and vip_elt.find('uniqid').text == self.params['uniqid']:
197+
return vip_elt
198+
else:
199+
if vip_elt.find('descr') is not None and vip_elt.find('descr').text == self.params['descr']:
200+
return vip_elt
201+
return None
202+
203+
def _remove_deleted_params(self):
204+
""" Remove from target_elt a few deleted params """
205+
changed = False
206+
for param in VIRTUALIP_ARGUMENT_SPEC:
207+
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
208+
if self.pfsense.remove_deleted_param_from_elt(self.target_elt, param, self.obj):
209+
changed = True
210+
211+
return changed
212+
213+
def _update(self):
214+
""" make the target pfsense reload """
215+
cmd = '''
216+
require_once("globals.inc");
217+
require_once("functions.inc");
218+
require_once("filter.inc");
219+
require_once("shaper.inc");
220+
require_once("interfaces.inc");
221+
require_once("util.inc");
222+
$check_carp = false;
223+
$retval = 0;
224+
'''
225+
226+
if self.params.get('mode') in ['carp', 'ipalias']:
227+
cmd += '$uniqid = "' + self.params.get('uniqid') +'";\n'
228+
cmd += '$subnet = "' + self.params.get('subnet') +'";\n'
229+
cmd += '$interface = "' + self.params.get('interface') +'";\n'
230+
cmd += '$vipif = get_real_interface($interface);\n'
231+
232+
if self.params.get('state') == 'present':
233+
if self.params.get('mode') in ['carp', 'ipalias']:
234+
cmd += '$check_carp = true;\n'
235+
cmd += 'foreach ($config["virtualip"]["vip"] as $vip) {\n'
236+
cmd += 'if ($vip["uniqid"] == $uniqid) {\n'
237+
cmd += 'interface_' + self.params.get('mode') + '_configure($vip);\n'
238+
cmd += '}\n}\n'
239+
else:
240+
if self.params.get('mode') == 'carp':
241+
cmd += 'if (does_interface_exist($vipif)) {\n'
242+
cmd += 'if (is_ipaddrv6($subnet)) {\n'
243+
cmd += 'mwexec("/sbin/ifconfig " . escapeshellarg($vipif) . " inet6 " . escapeshellarg($subnet) . " delete");\n'
244+
cmd += '} else {\n'
245+
cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n'
246+
cmd += '}\n}\n'
247+
elif self.params.get('mode') == 'ipalias':
248+
cmd += 'if (does_interface_exist($vipif)) {\n'
249+
cmd += 'if (is_ipaddrv6($subnet)) {\n'
250+
cmd += 'mwexec("/sbin/ifconfig " . escapeshellarg($vipif) . " inet6 " . escapeshellarg($subnet) . " -alias");\n'
251+
cmd += '} else {\n'
252+
cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n'
253+
cmd += '}\n}\n'
254+
255+
cmd += '''
256+
if ($check_carp === true && !get_carp_status()) {
257+
set_single_sysctl("net.inet.carp.allow", "1");
258+
}
259+
$retval |= filter_configure();
260+
$retval |= mwexec("/etc/rc.filter_synchronize");
261+
clear_subsystem_dirty('vip');'''
262+
263+
return self.pfsense.phpshell(cmd)
264+
265+
##############################
266+
# Logging
267+
#
268+
@staticmethod
269+
def _get_obj_name():
270+
""" return obj's name """
271+
return "vip"
272+
273+
def _log_fields(self, before=None):
274+
""" generate pseudo-CLI command fields parameters to create an obj """
275+
values = ''
276+
277+
if before is None:
278+
for param in VIRTUALIP_ARGUMENT_SPEC:
279+
if param not in skip_list:
280+
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
281+
values += self.format_cli_field(self.obj, param, fvalue=self.fvalue_bool)
282+
else:
283+
values += self.format_cli_field(self.obj, param)
284+
else:
285+
for param in VIRTUALIP_ARGUMENT_SPEC:
286+
if param not in skip_list:
287+
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
288+
values += self.format_updated_cli_field(self.obj, before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False)
289+
else:
290+
values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False)
291+
292+
return values
293+
294+
295+
def main():
296+
module = AnsibleModule(
297+
argument_spec=VIRTUALIP_ARGUMENT_SPEC,
298+
required_if=VIRTUALIP_REQUIRED_IF,
299+
supports_check_mode=True)
300+
301+
pfmodule = PFSenseVirtualIPModule(module)
302+
pfmodule.run(module.params)
303+
pfmodule.commit_changes()
304+
305+
306+
if __name__ == '__main__':
307+
main()

0 commit comments

Comments
 (0)