Skip to content

Commit dbdfe22

Browse files
committed
feat(system_patch): support pfsense system patch
1 parent 004fa39 commit dbdfe22

File tree

4 files changed

+356
-1
lines changed

4 files changed

+356
-1
lines changed

meta/runtime.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
requires_ansible: ">=2.12"
12
plugin_routing:
3+
action:
4+
pfsense_system_patch:
5+
redirect: pfsensible.core.src_file_to_content
26
modules:
37
pfsense_haproxy_backend:
48
deprecation:
@@ -8,4 +12,3 @@ plugin_routing:
812
deprecation:
913
removal_version: 0.8.0
1014
warning_text: Use pfsensible.haproxy.pfsense_haproxy_backend_server instead.
11-
requires_ansible: ">=2.12"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright: (c) 2023, genofire <geno+dev@fireorbit.de>
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from __future__ import annotations
7+
8+
import os
9+
10+
from ansible.errors import AnsibleError, AnsibleActionFail
11+
from ansible.module_utils.common.text.converters import to_native, to_text
12+
from ansible.plugins.action import ActionBase
13+
14+
15+
class ActionModule(ActionBase):
16+
17+
def run(self, tmp=None, task_vars=None):
18+
''' handler for file transfer operations '''
19+
if task_vars is None:
20+
task_vars = dict()
21+
22+
result = super(ActionModule, self).run(tmp, task_vars)
23+
del tmp # tmp no longer has any effect
24+
25+
source = self._task.args.get('src', None)
26+
new_module_args = self._task.args.copy()
27+
if source is not None:
28+
del new_module_args['src']
29+
try:
30+
# find in expected paths
31+
source = self._find_needle('files', source)
32+
except AnsibleError as e:
33+
result['failed'] = True
34+
result['msg'] = to_text(e)
35+
# result['exception'] = traceback.format_exc()
36+
return result
37+
38+
if not os.path.isfile(source):
39+
raise AnsibleActionFail(u"Source (%s) is not a file" % source)
40+
41+
try:
42+
with open(source, 'rb') as src:
43+
content = src.read()
44+
new_module_args['content'] = content.decode('utf-8')
45+
except Exception as e:
46+
raise AnsibleError("Unexpected error while reading source (%s) for diff: %s " % (source, to_native(e)))
47+
module_return = self._execute_module(module_args=new_module_args, task_vars=task_vars)
48+
result.update(module_return)
49+
return result
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright: (c) 2023, genofire <geno+dev@fireorbit.de>
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from __future__ import (absolute_import, division, print_function)
7+
__metaclass__ = type
8+
9+
from base64 import b64encode
10+
from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase
11+
12+
SYSTEMPATCH_ARGUMENT_SPEC = dict(
13+
state=dict(default='present', choices=['present', 'absent']),
14+
run=dict(default='no', choices=['apply', 'revert', 'no']),
15+
# attributes
16+
id=dict(type='str', required=True),
17+
description=dict(type='str'),
18+
content=dict(type='str'),
19+
# patch or patch_file
20+
src=dict(type='path'),
21+
location=dict(type='str', default=""),
22+
pathstrip=dict(type='int', default=2),
23+
basedir=dict(type='str', default="/"),
24+
ignore_whitespace=dict(type='bool', default=True),
25+
auto_apply=dict(type='bool', default=False),
26+
)
27+
28+
SYSTEMPATCH_MUTUALLY_EXCLUSIVE = [
29+
['content', 'path'],
30+
]
31+
32+
SYSTEMPATCH_REQUIRED_IF = [
33+
['state', 'present', ['description']],
34+
['state', 'present', ['content', 'src'], True],
35+
['run', 'apply', ['content', 'src'], True],
36+
['run', 'revert', ['content', 'src'], True],
37+
]
38+
39+
40+
class PFSenseSystemPatchModule(PFSenseModuleBase):
41+
""" module managing pfsense system patches """
42+
43+
@staticmethod
44+
def get_argument_spec():
45+
""" return argument spec """
46+
return SYSTEMPATCH_ARGUMENT_SPEC
47+
48+
##############################
49+
# init
50+
#
51+
def __init__(self, module, pfsense=None):
52+
super(PFSenseSystemPatchModule, self).__init__(module, pfsense)
53+
self.name = "pfsense_systempatch"
54+
55+
self.root_elt = None
56+
installedpackages_elt = self.pfsense.get_element('installedpackages')
57+
if installedpackages_elt is not None:
58+
self.root_elt = self.pfsense.get_element('patches', root_elt=installedpackages_elt, create_node=True)
59+
60+
self.target_elt = None # unknown
61+
self.obj = dict() # The object to work on
62+
63+
##############################
64+
# params processing
65+
#
66+
67+
def _validate_params(self):
68+
""" do some extra checks on input parameters """
69+
pass
70+
71+
def _create_target(self):
72+
""" create the XML target_elt """
73+
return self.pfsense.new_element('item')
74+
75+
def _find_target(self):
76+
""" find the XML target_elt """
77+
return self.pfsense.find_elt('item', self.params['id'], 'uniqid', root_elt=self.root_elt)
78+
79+
def _get_obj_name(self):
80+
return "'{0}'".format(self.obj['uniqid'])
81+
82+
def _log_fields(self, before=None):
83+
""" generate pseudo-CLI command fields parameters to create an obj """
84+
values = ''
85+
fields = [
86+
'uniqid',
87+
'descr',
88+
'location',
89+
'pathstrip',
90+
'basedir',
91+
'ignorewhitespace',
92+
'autoapply',
93+
'patch',
94+
]
95+
if before is None:
96+
for field in fields:
97+
values += self.format_cli_field(self.obj, field)
98+
else:
99+
for field in fields:
100+
values += self.format_updated_cli_field(self.obj, before, field, add_comma=(values))
101+
return values
102+
103+
@staticmethod
104+
def _get_params_to_remove():
105+
""" returns the list of params to remove if they are not set """
106+
return ['ignorewhitespace', 'autoapply']
107+
108+
def _params_to_obj(self):
109+
""" return a dict from module params """
110+
obj = dict()
111+
112+
self._get_ansible_param(obj, 'id', 'uniqid')
113+
self._get_ansible_param(obj, 'description', 'descr')
114+
self._get_ansible_param(obj, 'location')
115+
self._get_ansible_param(obj, 'pathstrip')
116+
self._get_ansible_param(obj, 'basedir')
117+
118+
if self.params['ignore_whitespace']:
119+
obj['ignorewhitespace'] = ""
120+
if self.params['auto_apply']:
121+
obj['autoapply'] = ""
122+
123+
# src copied to content by action
124+
if self.params['content'] is not None:
125+
obj['patch'] = b64encode(bytes(self.params['content'], 'utf-8')).decode('ascii')
126+
127+
if self.params['run'] != 'no':
128+
# want to run _update so change manipulate
129+
self.result['changed'] = True
130+
131+
return obj
132+
133+
##############################
134+
# run
135+
#
136+
137+
def _update(self):
138+
run = self.params['run']
139+
if run == "no":
140+
return ('0', 'Patch is stored but not installed', '')
141+
142+
other_direction = 'revert' if run == 'apply' else 'apply'
143+
144+
cmd = '''
145+
require_once('functions.inc');
146+
require_once('patches.inc');
147+
148+
'''
149+
cmd += self.pfsense.dict_to_php(self.obj, 'thispatch')
150+
cmd += '''
151+
152+
$retval = 0;
153+
$test = patch_test_''' + run + '''($thispatch);
154+
$retval |= $test;
155+
$retval = $retval << 1;
156+
157+
if ($test) {
158+
$retval |= patch_''' + run + '''($thispatch);
159+
} else {
160+
$rerun = patch_test_''' + other_direction + '''($thispatch);
161+
if($rerun) {
162+
patch_''' + other_direction + '''($thispatch);
163+
$retval |= patch_''' + run + '''($thispatch);
164+
}
165+
}
166+
exit($retval);'''
167+
(code, out, err) = self.pfsense.phpshell(cmd)
168+
self.result['rc_merged'] = code
169+
170+
# patch_'''+ run
171+
rc_run = (code % 2) == 1
172+
self.result['rc_run'] = rc_run
173+
174+
# patch_test_'''+ other_direction
175+
# restore test code, so if revert (other direction) not works - patch was already applyied
176+
rc_test = ((code >> 1) % 2) == 1
177+
self.result['rc_test'] = rc_test
178+
179+
# recalc changed after overwritten to run _update
180+
self.result['changed'] = (rc_run and rc_test)
181+
if not rc_run:
182+
self.result['failed'] = True
183+
self.result['msg'] = "Patch was not possible to run (even after try other direction previously)"
184+
return ('', out, err)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright: (c) 2023, genofire <geno+dev@fireorbit.de>
5+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6+
7+
from __future__ import absolute_import, division, print_function
8+
__metaclass__ = type
9+
10+
ANSIBLE_METADATA = {'metadata_version': '1.1',
11+
'status': ['preview'],
12+
'supported_by': 'community'}
13+
14+
DOCUMENTATION = """
15+
---
16+
module: pfsense_system_patch
17+
version_added: 0.6.0
18+
author: Geno (@genofire)
19+
short_description: System Patch
20+
description:
21+
- Manage System Patch
22+
notes:
23+
options:
24+
id:
25+
description: ID of Patch - for update / delete the correct
26+
type: str
27+
required: True
28+
description:
29+
description: The name of the patch in the "System Patch" menu.
30+
type: str
31+
required: False
32+
content:
33+
description: The contents of the patch.
34+
type: str
35+
required: False
36+
src:
37+
description: Path to a patch file.
38+
type: path
39+
required: False
40+
location:
41+
description: Location.
42+
type: str
43+
required: False
44+
default: ""
45+
pathstrip:
46+
description: The number of levels to strip from the front of the path in the patch header.
47+
type: int
48+
required: False
49+
default: 2
50+
basedir:
51+
description: |
52+
Enter the base directory for the patch, default is /.
53+
Patches from github are all based in /.
54+
Custom patches may need a full path here such as /usr/local/www/.
55+
type: str
56+
required: False
57+
default: "/"
58+
ignore_whitespace:
59+
description: Ignore whitespace in the patch.
60+
type: bool
61+
required: False
62+
default: true
63+
auto_apply:
64+
description: Apply the patch automatically when possible, useful for patches to survive after updates.
65+
type: bool
66+
required: False
67+
default: false
68+
state:
69+
description: State in which to leave the interface group.
70+
choices: [ "present", "absent" ]
71+
default: present
72+
type: str
73+
run:
74+
description: State in which to leave the interface group.
75+
choices: [ "no", "apply", "revert" ]
76+
default: "no"
77+
type: str
78+
"""
79+
80+
EXAMPLES = """
81+
- name: Try Systempatch
82+
pfsense_system_patch:
83+
id: "3f60a103a613"
84+
description: "Hello Welt Patch"
85+
content: >
86+
--- b/tmp/test.txt
87+
+++ a/tmp/test.txt
88+
@@ -0,0 +1 @@
89+
+Hello Welt
90+
location: ""
91+
pathstrip: 1
92+
basedir: "/"
93+
ignore_whitespace: true
94+
auto_apply: true
95+
"""
96+
97+
from ansible.module_utils.basic import AnsibleModule
98+
from ansible_collections.pfsensible.core.plugins.module_utils.system_patch import (
99+
PFSenseSystemPatchModule,
100+
SYSTEMPATCH_ARGUMENT_SPEC,
101+
SYSTEMPATCH_MUTUALLY_EXCLUSIVE,
102+
SYSTEMPATCH_REQUIRED_IF
103+
)
104+
105+
106+
def main():
107+
module = AnsibleModule(
108+
argument_spec=SYSTEMPATCH_ARGUMENT_SPEC,
109+
mutually_exclusive=SYSTEMPATCH_MUTUALLY_EXCLUSIVE,
110+
required_if=SYSTEMPATCH_REQUIRED_IF,
111+
supports_check_mode=True)
112+
113+
pfmodule = PFSenseSystemPatchModule(module)
114+
pfmodule.run(module.params)
115+
pfmodule.commit_changes()
116+
117+
118+
if __name__ == '__main__':
119+
main()

0 commit comments

Comments
 (0)