Skip to content

Commit 95af117

Browse files
committed
feat(system_patch): support pfsense system patch
1 parent 3cadeaf commit 95af117

File tree

4 files changed

+347
-1
lines changed

4 files changed

+347
-1
lines changed

meta/runtime.yml

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

0 commit comments

Comments
 (0)