Skip to content

Commit 4710031

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

File tree

4 files changed

+353
-1
lines changed

4 files changed

+353
-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: 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: 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)