diff --git a/lib/ansible/modules/packaging/os/swupd.py b/lib/ansible/modules/packaging/os/swupd.py new file mode 100644 index 0000000000..656fcdd034 --- /dev/null +++ b/lib/ansible/modules/packaging/os/swupd.py @@ -0,0 +1,337 @@ +#!/usr/bin/python + +# (c) 2017, Alberto Murillo +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +ANSIBLE_METADATA = { + 'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0' +} + +DOCUMENTATION = ''' +--- +module: swupd +short_description: Manages updates and bundles in ClearLinux systems. +description: + - Manages updates and bundles with the swupd bundle manager, which is used by the + Clear Linux Project for Intel Architecture. +version_added: "2.3" +author: Alberto Murillo (@albertomurillo) +options: + contenturl: + description: + - URL pointing to the contents of available bundles. + If not specified, the contents are retrieved from clearlinux.org. + required: false + default: null + format: + description: + - The format suffix for version file downloads. For example [1,2,3,staging,etc]. + If not specified, the default format is used. + required: false + default: null + manifest: + description: + - The manifest contains information about the bundles at certaion version of the OS. + Specify a Manifest version to verify against that version or leave unspecified to + verify against the current version. + required: false + default: null + aliases: [release, version] + name: + description: + - Name of the (I)bundle to install or remove. + required: false + default: null + aliases: [bundle] + state: + description: + - Indicates the desired (I)bundle state. C(present) ensures the bundle + is installed while C(absent) ensures the (I)bundle is not installed. + required: false + default: present + choices: [present, absent] + update: + description: + - Updates the OS to the latest version. + required: false + default: no + url: + description: + - Overrides both I(contenturl) and I(versionurl). + required: false + default: null + verify: + description: + - Verify content for OS version. + required: false + default: null + versionurl: + description: + - URL for version string download. + required: false + default: null +''' + +EXAMPLES = ''' +- name Update the OS to the latest version + swupd: + update: yes + +- name: Installs the "foo" bundle + swupd: + name: foo + state: present + +- name: Removes the "foo" bundle + swupd: + name: foo + state: absent + +- name: Check integrity of filesystem + swupd: + verify: yes + +- name: Downgrade OS to release 12920 + swupd: + verify: yes + manifest: 12920 +''' + +RETURN = ''' +stdout: + description: stdout of swupd + returned: always + type: string +stderr: + description: stderr of swupd + returned: always + type: string +''' + +import os +from ansible.module_utils.basic import AnsibleModule + + +class Swupd(object): + FILES_NOT_MATCH = "files did not match" + FILES_REPLACED = "missing files were replaced" + FILES_FIXED = "files were fixed" + FILES_DELETED = "files were deleted" + + def __init__(self, module): + # Fail if swupd is not found + self.module = module + self.swupd_cmd = module.get_bin_path("swupd", False) + if not self.swupd_cmd: + module.fail_json(msg="Could not find swupd.") + + # Initialize parameters + for key in module.params.keys(): + setattr(self, key, module.params[key]) + + # Initialize return values + self.changed = False + self.failed = False + self.msg = None + self.rc = None + self.stderr = "" + self.stdout = "" + + def _run_cmd(self, cmd): + self.rc, self.stdout, self.stderr = self.module.run_command(cmd, check_rc=False) + + def _get_cmd(self, command): + cmd = "%s %s" % (self.swupd_cmd, command) + + if self.format: + cmd += " --format=%s" % self.format + if self.manifest: + cmd += " --manifest=%s" % self.manifest + if self.url: + cmd += " --url=%s" % self.url + else: + if self.contenturl and command != "check-update": + cmd += " --contenturl=%s" % self.contenturl + if self.versionurl: + cmd += " --versionurl=%s" % self.versionurl + + return cmd + + def _is_bundle_installed(self, bundle): + try: + os.stat("/usr/share/clear/bundles/%s" % bundle) + except OSError: + return False + + return True + + def _needs_update(self): + cmd = self._get_cmd("check-update") + self._run_cmd(cmd) + + if self.rc == 0: + return True + + if self.rc == 1: + return False + + self.failed = True + self.msg = "Failed to check for updates" + + def _needs_verify(self): + cmd = self._get_cmd("verify") + self._run_cmd(cmd) + + if self.rc != 0: + self.failed = True + self.msg = "Failed to check for filesystem inconsistencies." + + if self.FILES_NOT_MATCH in self.stdout: + return True + + return False + + def install_bundle(self, bundle): + """Installs a bundle with `swupd bundle-add bundle`""" + if self.module.check_mode: + self.module.exit_json(changed=not self._is_bundle_installed(bundle)) + + if self._is_bundle_installed(bundle): + self.msg = "Bundle %s is already installed" % bundle + return + + cmd = self._get_cmd("bundle-add %s" % bundle) + self._run_cmd(cmd) + + if self.rc == 0: + self.changed = True + self.msg = "Bundle %s installed" % bundle + return + + if self.rc == 18: + self.msg = "Bundle name %s is invalid" % bundle + return + + self.failed = True + self.msg = "Failed to install bundle %s" % bundle + + def remove_bundle(self, bundle): + """Removes a bundle with `swupd bundle-remove bundle`""" + if self.module.check_mode: + self.module.exit_json(changed=self._is_bundle_installed(bundle)) + + if not self._is_bundle_installed(bundle): + self.msg = "Bundle %s not installed" + return + + cmd = self._get_cmd("bundle-remove %s" % bundle) + self._run_cmd(cmd) + + if self.rc == 0: + self.changed = True + self.msg = "Bundle %s removed" % bundle + return + + self.failed = True + self.msg = "Failed to remove bundle %s" % bundle + + def update_os(self): + """Updates the os with `swupd update`""" + if self.module.check_mode: + self.module.exit_json(changed=self._needs_update()) + + if not self._needs_update(): + self.msg = "There are no updates available" + return + + cmd = self._get_cmd("update") + self._run_cmd(cmd) + + if self.rc == 0: + self.changed = True + self.msg = "Update successful" + return + + self.failed = True + self.msg = "Failed to check for updates" + + def verify_os(self): + """Verifies filesystem agains specified or current version""" + if self.module.check_mode: + self.module.exit_json(changed=self._needs_verify()) + + if not self._needs_verify(): + self.msg = "No files where changed" + return + + cmd = self._get_cmd("verify --fix") + self._run_cmd(cmd) + + if self.rc == 0 and (self.FILES_REPLACED in self.stdout or self.FILES_FIXED in self.stdout or self.FILES_DELETED in self.stdout): + self.changed = True + self.msg = "Fix successful" + return + + self.failed = True + self.msg = "Failed to verify the OS" + + +def main(): + """The main function.""" + module = AnsibleModule( + argument_spec=dict( + contenturl=dict(type="str"), + format=dict(type="str"), + manifest=dict(aliases=["release", "version"], type="int"), + name=dict(aliases=["bundle"], type="str"), + state=dict(default="present", choices=["present", "absent"], type="str"), + update=dict(default=False, type="bool"), + url=dict(type="str"), + verify=dict(default=False, type="bool"), + versionurl=dict(type="str"), + ), + required_one_of=[["name", "update", "verify"]], + mutually_exclusive=[["name", "update", "verify"]], + supports_check_mode=True + ) + + swupd = Swupd(module) + + name = module.params["name"] + state = module.params["state"] + update = module.params["update"] + verify = module.params["verify"] + + if update: + swupd.update_os() + elif verify: + swupd.verify_os() + elif state == "present": + swupd.install_bundle(name) + elif state == "absent": + swupd.remove_bundle(name) + else: + swupd.failed = True + + if swupd.failed: + module.fail_json(msg=swupd.msg, stdout=swupd.stdout, stderr=swupd.stderr) + else: + module.exit_json(changed=swupd.changed, msg=swupd.msg, stdout=swupd.stdout, stderr=swupd.stderr) + + +if __name__ == '__main__': + main()