diff --git a/plugins/modules/system/sysupgrade.py b/plugins/modules/system/sysupgrade.py new file mode 100644 index 0000000000..a1956129df --- /dev/null +++ b/plugins/modules/system/sysupgrade.py @@ -0,0 +1,152 @@ +#!/usr/bin/python + +# Copyright: (c) 2020, Andrew Klaus +# 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 + +DOCUMENTATION = r''' +--- +module: sysupgrade +short_description: Manage OpenBSD system upgrades +version_added: 1.1.0 +description: + - Manage OpenBSD system upgrades using sysupgrade. +options: + snapshot: + description: + - Apply the latest snapshot. + - Otherwise release will be applied. + default: no + type: bool + force: + description: + - Force upgrade (for snapshots only). + default: no + type: bool + keep_files: + description: + - Keep the files under /home/_sysupgrade. + - By default, the files will be deleted after the upgrade. + default: no + type: bool + fetch_only: + description: + - Fetch and verify files and create /bsd.upgrade but do not reboot. + - Set to C(false) if you want sysupgrade to reboot. This will cause Ansible to error, as it expects the module to exit gracefully. See the examples. + default: yes + type: bool + installurl: + description: + - OpenBSD mirror top-level URL for fetching an upgrade. + - By default, the mirror URL is pulled from /etc/installurl. + type: str +author: + - Andrew Klaus (@precurse) +''' + +EXAMPLES = r''' +- name: Upgrade to latest release + community.general.sysupgrade: + register: sysupgrade + +- name: Upgrade to latest snapshot + community.general.sysupgrade: + snapshot: yes + installurl: https://cloudflare.cdn.openbsd.org/pub/OpenBSD + register: sysupgrade + +- name: Reboot to apply upgrade if needed + ansible.builtin.reboot: + when: sysupgrade.changed + +# Note: Ansible will error when running this way due to how +# the reboot is forcefully handled by sysupgrade: + +- name: Have sysupgrade automatically reboot + community.general.sysupgrade: + fetch_only: no + ignore_errors: yes +''' + +RETURN = r''' +rc: + description: The command return code (0 means success). + returned: always + type: int +stdout: + description: Sysupgrade standard output. + returned: always + type: str +stderr: + description: Sysupgrade standard error. + returned: always + type: str + sample: "sysupgrade: need root privileges" +''' + +from ansible.module_utils.basic import AnsibleModule + + +def sysupgrade_run(module): + sysupgrade_bin = module.get_bin_path('/usr/sbin/sysupgrade', required=True) + cmd = [sysupgrade_bin] + changed = False + warnings = [] + + # Setup command flags + if module.params['snapshot']: + run_flag = ['-s'] + if module.params['force']: + # Force only applies to snapshots + run_flag.append('-f') + else: + # release flag + run_flag = ['-r'] + + if module.params['keep_files']: + run_flag.append('-k') + + if module.params['fetch_only']: + run_flag.append('-n') + + # installurl must be the last argument + if module.params['installurl']: + run_flag.append(module.params['installurl']) + + rc, out, err = module.run_command(cmd + run_flag) + + if rc != 0: + module.fail_json(msg="Command %s failed rc=%d, out=%s, err=%s" % (cmd, rc, out, err)) + elif out.lower().find('already on latest snapshot') >= 0: + changed = False + elif out.lower().find('upgrade on next reboot') >= 0: + changed = True + + return dict( + changed=changed, + rc=rc, + stderr=err, + stdout=out, + warnings=warnings + ) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + snapshot=dict(type='bool', default=False), + fetch_only=dict(type='bool', default=True), + force=dict(type='bool', default=False), + keep_files=dict(type='bool', default=False), + installurl=dict(type='str'), + ), + supports_check_mode=False, + ) + return_dict = sysupgrade_run(module) + module.exit_json(**return_dict) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/sysupgrade.py b/plugins/modules/sysupgrade.py new file mode 120000 index 0000000000..5aa9adb36e --- /dev/null +++ b/plugins/modules/sysupgrade.py @@ -0,0 +1 @@ +system/sysupgrade.py \ No newline at end of file diff --git a/tests/unit/plugins/modules/system/test_sysupgrade.py b/tests/unit/plugins/modules/system/test_sysupgrade.py new file mode 100644 index 0000000000..1ea8bf208c --- /dev/null +++ b/tests/unit/plugins/modules/system/test_sysupgrade.py @@ -0,0 +1,67 @@ +# 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 ansible.module_utils import basic +from ansible_collections.community.general.tests.unit.compat.mock import patch +from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase +from ansible_collections.community.general.plugins.modules.system import sysupgrade + + +class TestSysupgradeModule(ModuleTestCase): + + def setUp(self): + super(TestSysupgradeModule, self).setUp() + self.module = sysupgrade + self.mock_get_bin_path = (patch('ansible.module_utils.basic.AnsibleModule.get_bin_path')) + self.get_bin_path = self.mock_get_bin_path.start() + + def tearDown(self): + super(TestSysupgradeModule, self).tearDown() + self.mock_get_bin_path.stop() + + def test_upgrade_success(self): + """ Upgrade was successful """ + + rc = 0 + stdout = """ + SHA256.sig 100% |*************************************| 2141 00:00 + Signature Verified + INSTALL.amd64 100% |************************************| 43512 00:00 + base67.tgz 100% |*************************************| 238 MB 02:16 + bsd 100% |*************************************| 18117 KB 00:24 + bsd.mp 100% |*************************************| 18195 KB 00:17 + bsd.rd 100% |*************************************| 10109 KB 00:14 + comp67.tgz 100% |*************************************| 74451 KB 00:53 + game67.tgz 100% |*************************************| 2745 KB 00:03 + man67.tgz 100% |*************************************| 7464 KB 00:04 + xbase67.tgz 100% |*************************************| 22912 KB 00:30 + xfont67.tgz 100% |*************************************| 39342 KB 00:28 + xserv67.tgz 100% |*************************************| 16767 KB 00:24 + xshare67.tgz 100% |*************************************| 4499 KB 00:06 + Verifying sets. + Fetching updated firmware. + Will upgrade on next reboot + """ + stderr = "" + + with patch.object(basic.AnsibleModule, "run_command") as run_command: + run_command.return_value = (rc, stdout, stderr) + with self.assertRaises(AnsibleExitJson) as result: + self.module.main() + self.assertTrue(result.exception.args[0]['changed']) + + def test_upgrade_failed(self): + """ Upgrade failed """ + + rc = 1 + stdout = "" + stderr = "sysupgrade: need root privileges" + + with patch.object(basic.AnsibleModule, "run_command") as run_command_mock: + run_command_mock.return_value = (rc, stdout, stderr) + with self.assertRaises(AnsibleFailJson) as result: + self.module.main() + self.assertTrue(result.exception.args[0]['failed']) + self.assertIn('need root', result.exception.args[0]['msg'])