-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(system_patch): support pfsense system patch
- Loading branch information
Showing
4 changed files
with
353 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,6 @@ | ||
requires_ansible: ">=2.9" | ||
--- | ||
requires_ansible: '>=2.9.10' | ||
plugin_routing: | ||
action: | ||
pfsense_system_patch: | ||
redirect: pfsensible.core.src_file_to_content |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright: (c) 2023, genofire <[email protected]> | ||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
|
||
from __future__ import annotations | ||
|
||
import os | ||
|
||
from ansible.errors import AnsibleError, AnsibleActionFail | ||
from ansible.module_utils.common.text.converters import to_native, to_text | ||
from ansible.plugins.action import ActionBase | ||
|
||
|
||
class ActionModule(ActionBase): | ||
|
||
def run(self, tmp=None, task_vars=None): | ||
''' handler for file transfer operations ''' | ||
if task_vars is None: | ||
task_vars = dict() | ||
|
||
result = super(ActionModule, self).run(tmp, task_vars) | ||
del tmp # tmp no longer has any effect | ||
|
||
source = self._task.args.get('src', None) | ||
new_module_args = self._task.args.copy() | ||
if source is not None: | ||
del new_module_args['src'] | ||
try: | ||
# find in expected paths | ||
source = self._find_needle('files', source) | ||
except AnsibleError as e: | ||
result['failed'] = True | ||
result['msg'] = to_text(e) | ||
# result['exception'] = traceback.format_exc() | ||
return result | ||
|
||
if not os.path.isfile(source): | ||
raise AnsibleActionFail(u"Source (%s) is not a file" % source) | ||
|
||
try: | ||
with open(source, 'rb') as src: | ||
content = src.read() | ||
new_module_args['content'] = content.decode('utf-8') | ||
except Exception as e: | ||
raise AnsibleError("Unexpected error while reading source (%s) for diff: %s " % (source, to_native(e))) | ||
module_return = self._execute_module(module_args=new_module_args, task_vars=task_vars) | ||
result.update(module_return) | ||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright: (c) 2023, genofire <[email protected]> | ||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
|
||
from __future__ import (absolute_import, division, print_function) | ||
__metaclass__ = type | ||
|
||
from base64 import b64encode | ||
from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase | ||
|
||
SYSTEMPATCH_ARGUMENT_SPEC = dict( | ||
state=dict(default='present', choices=['present', 'absent']), | ||
run=dict(default='no', choices=['apply', 'revert', 'no']), | ||
# attributes | ||
id=dict(type='str', required=True), | ||
description=dict(type='str'), | ||
content=dict(type='str'), | ||
# patch or patch_file | ||
src=dict(type='path'), | ||
location=dict(type='str', default=""), | ||
pathstrip=dict(type='int', default=2), | ||
basedir=dict(type='str', default="/"), | ||
ignore_whitespace=dict(type='bool', default=True), | ||
auto_apply=dict(type='bool', default=False), | ||
) | ||
|
||
SYSTEMPATCH_MUTUALLY_EXCLUSIVE = [ | ||
['content', 'path'], | ||
] | ||
|
||
SYSTEMPATCH_REQUIRED_IF = [ | ||
['state', 'present', ['description']], | ||
['state', 'present', ['content', 'src'], True], | ||
['run', 'apply', ['content', 'src'], True], | ||
['run', 'revert', ['content', 'src'], True], | ||
] | ||
|
||
|
||
class PFSenseSystemPatchModule(PFSenseModuleBase): | ||
""" module managing pfsense system patches """ | ||
|
||
@staticmethod | ||
def get_argument_spec(): | ||
""" return argument spec """ | ||
return SYSTEMPATCH_ARGUMENT_SPEC | ||
|
||
############################## | ||
# init | ||
# | ||
def __init__(self, module, pfsense=None): | ||
super(PFSenseSystemPatchModule, self).__init__(module, pfsense) | ||
self.name = "pfsense_systempatch" | ||
|
||
self.root_elt = None | ||
installedpackages_elt = self.pfsense.get_element('installedpackages') | ||
if installedpackages_elt is not None: | ||
self.root_elt = self.pfsense.get_element('patches', root_elt=installedpackages_elt, create_node=True) | ||
|
||
self.target_elt = None # unknown | ||
self.obj = dict() # The object to work on | ||
|
||
############################## | ||
# params processing | ||
# | ||
|
||
def _validate_params(self): | ||
""" do some extra checks on input parameters """ | ||
pass | ||
|
||
def _create_target(self): | ||
""" create the XML target_elt """ | ||
return self.pfsense.new_element('item') | ||
|
||
def _find_target(self): | ||
""" find the XML target_elt """ | ||
return self.pfsense.find_elt('item', self.params['id'], 'uniqid', root_elt=self.root_elt) | ||
|
||
def _get_obj_name(self): | ||
return "'{0}'".format(self.obj['uniqid']) | ||
|
||
def _log_fields(self, before=None): | ||
""" generate pseudo-CLI command fields parameters to create an obj """ | ||
values = '' | ||
fields = [ | ||
'uniqid', | ||
'descr', | ||
'location', | ||
'pathstrip', | ||
'basedir', | ||
'ignorewhitespace', | ||
'autoapply', | ||
'patch', | ||
] | ||
if before is None: | ||
for field in fields: | ||
values += self.format_cli_field(self.obj, field) | ||
else: | ||
for field in fields: | ||
values += self.format_updated_cli_field(self.obj, before, field, add_comma=(values)) | ||
return values | ||
|
||
@staticmethod | ||
def _get_params_to_remove(): | ||
""" returns the list of params to remove if they are not set """ | ||
return ['ignorewhitespace', 'autoapply'] | ||
|
||
def _params_to_obj(self): | ||
""" return a dict from module params """ | ||
obj = dict() | ||
|
||
self._get_ansible_param(obj, 'id', 'uniqid') | ||
self._get_ansible_param(obj, 'description', 'descr') | ||
self._get_ansible_param(obj, 'location') | ||
self._get_ansible_param(obj, 'pathstrip') | ||
self._get_ansible_param(obj, 'basedir') | ||
|
||
if self.params['ignore_whitespace']: | ||
obj['ignorewhitespace'] = "" | ||
if self.params['auto_apply']: | ||
obj['autoapply'] = "" | ||
|
||
# src copied to content by action | ||
if self.params['content'] is not None: | ||
obj['patch'] = b64encode(bytes(self.params['content'], 'utf-8')).decode('ascii') | ||
|
||
if self.params['run'] != 'no': | ||
# want to run _update so change manipulate | ||
self.result['changed'] = True | ||
|
||
return obj | ||
|
||
############################## | ||
# run | ||
# | ||
|
||
def _update(self): | ||
run = self.params['run'] | ||
if run == "no": | ||
return ('0', 'Patch is stored but not installed', '') | ||
|
||
other_direction = 'revert' if run == 'apply' else 'apply' | ||
|
||
cmd = ''' | ||
require_once('functions.inc'); | ||
require_once('patches.inc'); | ||
''' | ||
cmd += self.pfsense.dict_to_php(self.obj, 'thispatch') | ||
cmd += ''' | ||
$retval = 0; | ||
$test = patch_test_''' + run + '''($thispatch); | ||
$retval |= $test; | ||
$retval = $retval << 1; | ||
if ($test) { | ||
$retval |= patch_''' + run + '''($thispatch); | ||
} else { | ||
$rerun = patch_test_''' + other_direction + '''($thispatch); | ||
if($rerun) { | ||
patch_''' + other_direction + '''($thispatch); | ||
$retval |= patch_''' + run + '''($thispatch); | ||
} | ||
} | ||
exit($retval);''' | ||
(code, out, err) = self.pfsense.phpshell(cmd) | ||
self.result['rc_merged'] = code | ||
|
||
# patch_'''+ run | ||
rc_run = (code % 2) == 1 | ||
self.result['rc_run'] = rc_run | ||
|
||
# patch_test_'''+ other_direction | ||
# restore test code, so if revert (other direction) not works - patch was already applyied | ||
rc_test = ((code >> 1) % 2) == 1 | ||
self.result['rc_test'] = rc_test | ||
|
||
# recalc changed after overwritten to run _update | ||
self.result['changed'] = (rc_run and rc_test) | ||
if not rc_run: | ||
self.result['failed'] = True | ||
self.result['msg'] = "Patch was not possible to run (even after try other direction previously)" | ||
return ('', out, err) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
#!/usr/bin/python | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright: (c) 2023, genofire <[email protected]> | ||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
|
||
from __future__ import absolute_import, division, print_function | ||
__metaclass__ = type | ||
|
||
ANSIBLE_METADATA = {'metadata_version': '1.1', | ||
'status': ['preview'], | ||
'supported_by': 'community'} | ||
|
||
DOCUMENTATION = """ | ||
--- | ||
module: pfsense_system_patch | ||
version_added: 0.6.0 | ||
author: Geno (@genofire) | ||
short_description: System Patch | ||
description: | ||
- Manage System Patch | ||
notes: | ||
options: | ||
id: | ||
description: ID of Patch - for update / delete the correct | ||
type: str | ||
required: yes | ||
description: | ||
description: The name of the patch in the "System Patch" menu. | ||
type: str | ||
required: yes | ||
content: | ||
description: The contents of the patch. | ||
type: str | ||
required: yes | ||
location: | ||
description: Location. | ||
type: str | ||
required: no | ||
default: "" | ||
pathstrip: | ||
description: The number of levels to strip from the front of the path in the patch header. | ||
type: int | ||
required: no | ||
default: 2 | ||
basedir: | ||
description: | | ||
Enter the base directory for the patch, default is /. | ||
Patches from github are all based in /. | ||
Custom patches may need a full path here such as /usr/local/www/. | ||
type: str | ||
required: no | ||
default: "/" | ||
ignore_whitespace: | ||
description: Ignore whitespace in the patch. | ||
type: bool | ||
required: no | ||
default: true | ||
auto_apply: | ||
description: Apply the patch automatically when possible, useful for patches to survive after updates. | ||
type: bool | ||
required: no | ||
default: false | ||
state: | ||
description: State in which to leave the interface group. | ||
choices: [ "present", "absent" ] | ||
default: present | ||
type: str | ||
run: | ||
description: State in which to leave the interface group. | ||
choices: [ "no", "apply", "revert" ] | ||
type: str | ||
""" | ||
|
||
EXAMPLES = """ | ||
- name: Try Systempatch | ||
pfsense_system_patch: | ||
id: "3f60a103a613" | ||
description: "Hello Welt Patch" | ||
content: > | ||
--- b/tmp/test.txt | ||
+++ a/tmp/test.txt | ||
@@ -0,0 +1 @@ | ||
+Hello Welt | ||
location: "" | ||
pathstrip: 1 | ||
basedir: "/" | ||
ignore_whitespace: true | ||
auto_apply: true | ||
""" | ||
|
||
from ansible.module_utils.basic import AnsibleModule | ||
from ansible_collections.pfsensible.core.plugins.module_utils.system_patch import ( | ||
PFSenseSystemPatchModule, | ||
SYSTEMPATCH_ARGUMENT_SPEC, | ||
SYSTEMPATCH_MUTUALLY_EXCLUSIVE, | ||
SYSTEMPATCH_REQUIRED_IF | ||
) | ||
|
||
|
||
def main(): | ||
module = AnsibleModule( | ||
argument_spec=SYSTEMPATCH_ARGUMENT_SPEC, | ||
mutually_exclusive=SYSTEMPATCH_MUTUALLY_EXCLUSIVE, | ||
required_if=SYSTEMPATCH_REQUIRED_IF, | ||
supports_check_mode=True) | ||
|
||
pfmodule = PFSenseSystemPatchModule(module) | ||
pfmodule.run(module.params) | ||
pfmodule.commit_changes() | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |