Skip to content

Commit db0d7b0

Browse files
genofireopoplawski
authored andcommitted
feat: add virtual-ip module
1 parent eb570cf commit db0d7b0

1 file changed

Lines changed: 314 additions & 0 deletions

File tree

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
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_virtualip
20+
version_added: "0.6.2"
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+
descr:
33+
description: Description
34+
required: False
35+
type: str
36+
interface:
37+
description: Interface
38+
required: True
39+
type: str
40+
vhid:
41+
description: VHID Group
42+
required: False
43+
type: int
44+
advbase:
45+
description: Advertising Frequency Base
46+
required: False
47+
type: int
48+
advskew:
49+
description: Advertising Frequency Skew
50+
required: False
51+
type: int
52+
password:
53+
description: Virtual IP Password
54+
required: False
55+
type: str
56+
uniqid:
57+
description: Unique ID of Virtual IP in configuration
58+
required: False
59+
type: str
60+
type:
61+
description: Address Type
62+
required: False
63+
type: str
64+
choices: [ "single" ]
65+
default: single
66+
subnet_bits:
67+
description: Network's subnet mask
68+
required: False
69+
type: int
70+
default: 32
71+
subnet:
72+
description: Network subnet
73+
required: False
74+
type: str
75+
state:
76+
description: State in which to leave the Virtual IP
77+
choices: [ "present", "absent" ]
78+
default: present
79+
type: str
80+
"""
81+
82+
EXAMPLES = """
83+
- name: Setup Home vip
84+
pfsense_virtualip:
85+
mode: "carp"
86+
descr: "HOME VIP"
87+
interface: "opt2"
88+
vhid: 24
89+
advbase: 1,
90+
advskew: 0,
91+
password": "xaequae0sheiB7sh"
92+
uniqid": "vip_home"
93+
subnet_bits": 24
94+
subnet": "10.1.1.1"
95+
state": "present"
96+
"""
97+
98+
RETURN = """
99+
"""
100+
101+
import re
102+
from copy import deepcopy
103+
from ansible.module_utils.basic import AnsibleModule
104+
from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase
105+
106+
107+
VIRTUALIP_ARGUMENT_SPEC = dict(
108+
mode=dict(required=True, choices=['proxyarp', 'carp', 'ipalias', 'other'], type='str'),
109+
interface=dict(required=True, type='str'),
110+
vhid=dict(type='int'),
111+
advskew=dict(type='int'),
112+
advbase=dict(type='int'),
113+
password=dict(type='str', no_log=True),
114+
uniqid=dict(type='str'),
115+
descr=dict(type='str'),
116+
type=dict(type='str', choices=['single'], default='single'),
117+
subnet_bits=dict(type='int', default=32),
118+
subnet=dict(type='str'),
119+
state=dict(default='present', choices=['present', 'absent'], type='str'),
120+
)
121+
122+
VIRTUALIP_REQUIRED_IF = [
123+
["mode", "carp", ["uniqid", "password", "vhid", "advbase"]],
124+
["mode", "ipalias", ["uniqid"]],
125+
]
126+
127+
128+
# fields that are not written to pfsense
129+
skip_list = ['state']
130+
131+
132+
class PFSenseVirtualIPModule(PFSenseModuleBase):
133+
""" module managing pfsense virtual ip settings """
134+
135+
@staticmethod
136+
def get_argument_spec():
137+
""" return argument spec """
138+
return VIRTUALIP_ARGUMENT_SPEC
139+
140+
##############################
141+
# init
142+
#
143+
def __init__(self, module, pfsense=None):
144+
super(PFSenseVirtualIPModule, self).__init__(module, pfsense)
145+
self.name = "virtualip"
146+
self.root_elt = self.pfsense.get_element('virtualip')
147+
self.obj = dict()
148+
149+
if self.root_elt is None:
150+
self.root_elt = self.pfsense.new_element('virtualip')
151+
self.pfsense.root.append(self.root_elt)
152+
153+
##############################
154+
# params processing
155+
#
156+
def _params_to_obj(self):
157+
""" return a dict from module params """
158+
params = self.params
159+
160+
obj = dict()
161+
self.obj = obj
162+
163+
def _set_param(target, param):
164+
if params.get(param) is not None:
165+
if isinstance(params[param], str):
166+
target[param] = params[param]
167+
else:
168+
target[param] = str(params[param])
169+
170+
def _set_param_bool(target, param):
171+
if params.get(param) is not None:
172+
value = params.get(param)
173+
if value is True and param not in target:
174+
target[param] = ''
175+
elif value is False and param in target:
176+
del target[param]
177+
178+
for param in VIRTUALIP_ARGUMENT_SPEC:
179+
if param not in skip_list:
180+
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
181+
_set_param_bool(obj, param)
182+
else:
183+
_set_param(obj, param)
184+
185+
return obj
186+
187+
def _validate_params(self):
188+
""" do some extra checks on input parameters """
189+
params = self.params
190+
return
191+
192+
##############################
193+
# XML processing
194+
#
195+
def _create_target(self):
196+
""" create the XML target_elt """
197+
return self.pfsense.new_element('vip')
198+
199+
def _find_target(self):
200+
""" find the XML target elt """
201+
for vip_elt in self.root_elt:
202+
if self.params['mode'] in ['ipalias', 'carp']:
203+
if vip_elt.find('uniqid') is not None and vip_elt.find('uniqid').text == self.params['uniqid']:
204+
return vip_elt
205+
else:
206+
if vip_elt.find('descr') is not None and vip_elt.find('descr').text == self.params['descr']:
207+
return vip_elt
208+
return None
209+
210+
def _remove_deleted_params(self):
211+
""" Remove from target_elt a few deleted params """
212+
changed = False
213+
for param in VIRTUALIP_ARGUMENT_SPEC:
214+
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
215+
if self.pfsense.remove_deleted_param_from_elt(self.target_elt, param, self.obj):
216+
changed = True
217+
218+
return changed
219+
220+
def _update(self):
221+
""" make the target pfsense reload """
222+
cmd = '''
223+
require_once("globals.inc");
224+
require_once("functions.inc");
225+
require_once("filter.inc");
226+
require_once("shaper.inc");
227+
require_once("interfaces.inc");
228+
require_once("util.inc");
229+
$check_carp = false;
230+
$retval = 0;
231+
'''
232+
233+
if self.params.get('mode') in ['carp', 'ipalias']:
234+
cmd += '$uniqid = "' + self.params.get('uniqid') + '";\n'
235+
cmd += '$subnet = "' + self.params.get('subnet') + '";\n'
236+
cmd += '$interface = "' + self.params.get('interface') + '";\n'
237+
cmd += '$vipif = get_real_interface($interface);\n'
238+
239+
if self.params.get('state') == 'present':
240+
if self.params.get('mode') in ['carp', 'ipalias']:
241+
cmd += '$check_carp = true;\n'
242+
cmd += 'foreach ($config["virtualip"]["vip"] as $vip) {\n'
243+
cmd += 'if ($vip["uniqid"] == $uniqid) {\n'
244+
cmd += 'interface_' + self.params.get('mode') + '_configure($vip);\n'
245+
cmd += '}\n}\n'
246+
else:
247+
if self.params.get('mode') == 'carp':
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) . " delete");\n'
251+
cmd += '} else {\n'
252+
cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n'
253+
cmd += '}\n}\n'
254+
elif self.params.get('mode') == 'ipalias':
255+
cmd += 'if (does_interface_exist($vipif)) {\n'
256+
cmd += 'if (is_ipaddrv6($subnet)) {\n'
257+
cmd += 'mwexec("/sbin/ifconfig " . escapeshellarg($vipif) . " inet6 " . escapeshellarg($subnet) . " -alias");\n'
258+
cmd += '} else {\n'
259+
cmd += 'pfSense_interface_deladdress($vipif, $subnet);\n'
260+
cmd += '}\n}\n'
261+
262+
cmd += '''
263+
if ($check_carp === true && !get_carp_status()) {
264+
set_single_sysctl("net.inet.carp.allow", "1");
265+
}
266+
$retval |= filter_configure();
267+
$retval |= mwexec("/etc/rc.filter_synchronize");
268+
clear_subsystem_dirty('vip');'''
269+
270+
return self.pfsense.phpshell(cmd)
271+
272+
##############################
273+
# Logging
274+
#
275+
@staticmethod
276+
def _get_obj_name():
277+
""" return obj's name """
278+
return "vip"
279+
280+
def _log_fields(self, before=None):
281+
""" generate pseudo-CLI command fields parameters to create an obj """
282+
values = ''
283+
284+
if before is None:
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_cli_field(self.obj, param, fvalue=self.fvalue_bool)
289+
else:
290+
values += self.format_cli_field(self.obj, param)
291+
else:
292+
for param in VIRTUALIP_ARGUMENT_SPEC:
293+
if param not in skip_list:
294+
if VIRTUALIP_ARGUMENT_SPEC[param]['type'] == 'bool':
295+
values += self.format_updated_cli_field(self.obj, before, param, fvalue=self.fvalue_bool, add_comma=(values), log_none=False)
296+
else:
297+
values += self.format_updated_cli_field(self.obj, before, param, add_comma=(values), log_none=False)
298+
299+
return values
300+
301+
302+
def main():
303+
module = AnsibleModule(
304+
argument_spec=VIRTUALIP_ARGUMENT_SPEC,
305+
required_if=VIRTUALIP_REQUIRED_IF,
306+
supports_check_mode=True)
307+
308+
pfmodule = PFSenseVirtualIPModule(module)
309+
pfmodule.run(module.params)
310+
pfmodule.commit_changes()
311+
312+
313+
if __name__ == '__main__':
314+
main()

0 commit comments

Comments
 (0)