Repository: ansible/ansible

Issue:
ClearLinux Support
##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
modules/packaging/os/swupd.py

##### ANSIBLE VERSION
```
ansible 2.3.0 (clearlinux efba8f30af) last updated 2017/01/30 09:56:36 (GMT -500)
  config file =
  configured module search path = Default w/o overrides
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
[Clear Linux Project](https://clearlinux.org/)

##### SUMMARY
ClearLinux has a kind of package manager (It does not install packages but 'bundles') called swupd

##### STEPS TO REPRODUCE
```
---
- hosts: vbox
  tasks:
    - name: Print ansible_os_family
      debug: msg={{ ansible_os_family }}

    - name: Print ansible_distribution
      debug: msg={{ ansible_distribution }}

    - name: Install c-basic bundle
      package: name=c-basic
```

##### EXPECTED RESULTS
Uses swupd module to install 'bundles'
Prints 'ClearLinux' as ansible_distribution and ansible_os_family

##### ACTUAL RESULTS
* swupd module is not implemented

```
TASK [Print ansible_os_family] *************************************************
ok: [vbox] => {
    "msg": "Clear linux software for intel architecture"
}

TASK [Print ansible_distribution] **********************************************
ok: [vbox] => {
    "msg": "Clear linux software for intel architecture"
}
```



Discussion:

Resolving patch:
diff --git a/lib/ansible/module_utils/facts.py b/lib/ansible/module_utils/facts.py
--- a/lib/ansible/module_utils/facts.py
+++ b/lib/ansible/module_utils/facts.py
@@ -156,6 +156,7 @@ class Facts(object):
                  { 'path' : '/usr/bin/pkg',         'name' : 'pkg' },
                  { 'path' : '/usr/bin/xbps-install','name' : 'xbps' },
                  { 'path' : '/usr/local/sbin/pkg',  'name' : 'pkgng' },
+                 { 'path' : '/usr/bin/swupd',       'name' : 'swupd' },
                 ]
 
     def __init__(self, module, load_on_init=True, cached_facts=None):
@@ -651,12 +652,14 @@ class Distribution(object):
         {'path': '/etc/altlinux-release', 'name': 'Altlinux'},
         {'path': '/etc/os-release', 'name': 'NA'},
         {'path': '/etc/coreos/update.conf', 'name': 'Coreos'},
+        {'path': '/usr/lib/os-release', 'name': 'ClearLinux'},
     )
 
     SEARCH_STRING = {
         'OracleLinux': 'Oracle Linux',
         'RedHat': 'Red Hat',
         'Altlinux': 'ALT Linux',
+        'ClearLinux': 'Clear Linux Software for Intel Architecture',
     }
 
     # A list with OS Family members
diff --git a/lib/ansible/modules/packaging/os/swupd.py b/lib/ansible/modules/packaging/os/swupd.py
new file mode 100644
--- /dev/null
+++ b/lib/ansible/modules/packaging/os/swupd.py
@@ -0,0 +1,285 @@
+#!/usr/bin/python
+
+# (c) 2017, Alberto Murillo <alberto.murillo.silva@intel.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+import os
+from ansible.module_utils.basic import AnsibleModule
+
+
+ANSIBLE_METADATA = {'status': ['preview'],
+                    'supported_by': 'community',
+                    'version': '1.0'}
+
+DOCUMENTATION = '''
+---
+module: swupd
+short_description: Manages bundles with I(swupd)
+description:
+    - "Manages bundles with the I(swupd) update manager, which is used by the
+      Clear Linux Project for Intel Architecture"
+version_added: "2.3"
+author:
+    - "Alberto Murillo (@albertomurillo)"
+options:
+    name:
+        description:
+            - Name of the bundle to install or remove.
+        required: false
+        default: null
+        aliases: ['bundle']
+    state:
+        description:
+            - Indicates the desired bundle state. C(present) ensures the bundle
+              is installed while C(absent) ensures the bundle is not installed.
+            required: false
+            default: present
+            choices: ["present", "absent"]
+    update:
+        description:
+            - Updates the OS to the latest version
+        required: false
+        default: no
+        aliases: ['upgrade']
+        choices: ['yes', 'no']
+    verify:
+        description:
+            - Verify file system
+        required: false
+        default: null
+        choices: ['yes', 'no']
+    manifest:
+        description:
+            - Manifest to verify against to
+        required: false
+        default: null
+        aliases: ['release', 'version']
+    format:
+        description:
+            - The format suffix for version file downloads
+        required: false
+        default: null
+    url:
+        description:
+            - Overrides both I(contenturl) and I(versionurl)
+        required: false
+        default: null
+    contenturl:
+        description:
+            - url for content file download
+        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
+'''
+
+FILES_NOT_MATCH = "files did not match"
+FILES_REPLACED = "missing files were replaced"
+FILES_FIXED = "files were fixed"
+FILES_DELETED = "files were deleted"
+
+
+def _get_cmd(module, command):
+    p = module.params
+    cmd = "%s %s" % (SWUPD_CMD, command)
+
+    if p["format"]:
+        cmd += " --format=%s" % (p["format"])
+    if p["manifest"]:
+        cmd += " --manifest=%s" % (p["manifest"])
+    if p["url"]:
+        cmd += " --url=%s" % (p["url"])
+    else:
+        if p["contenturl"] and command != "check-update":
+            cmd += " --contenturl=%s" % (p["contenturl"])
+        if p["versionurl"]:
+            cmd += " --versionurl=%s" % (p["versionurl"])
+
+    return cmd
+
+
+def _is_bundle_installed(module, bundle):
+    try:
+        os.stat("/usr/share/clear/bundles/%s" % bundle)
+    except OSError:
+        return False
+
+    return True
+
+
+def _needs_update(module):
+    cmd = _get_cmd(module, "check-update")
+    rc, stdout, stderr = module.run_command(cmd, check_rc=False)
+
+    if rc == 0:
+        return True
+
+    if rc == 1:
+        return False
+
+    module.fail_json(msg="Unkown Error", stdout=stdout, stderr=stderr)
+
+
+def _needs_verify(module):
+    cmd = _get_cmd(module, "verify")
+    rc, stdout, stderr = module.run_command(cmd, check_rc=False)
+
+    if rc != 0:
+        module.fail_json(msg="Unkown Error", stdout=stdout, stderr=stderr)
+
+    if FILES_NOT_MATCH in stdout:
+        return True
+
+    return False
+
+
+def swupd_install(module, bundle):
+    """Installs a bundle with `swupd bundle-add bundle`"""
+    if module.check_mode:
+        module.exit_json(changed=not _is_bundle_installed(module, bundle))
+
+    if _is_bundle_installed(module, bundle):
+        module.exit_json(changed=False, msg="Bundle %s is already installed" % bundle)
+
+    cmd = _get_cmd(module, "bundle-add %s" % bundle)
+    rc, stdout, stderr = module.run_command(cmd, check_rc=False)
+
+    if rc == 0:
+        module.exit_json(changed=True, msg="Bundle %s installed" % bundle, stdout=stdout, stderr=stderr)
+
+    module.fail_json(msg="Unkown Error", stdout=stdout, stderr=stderr)
+
+
+def swupd_remove(module, bundle):
+    """Removes a bundle with `swupd bundle-remove bundle`"""
+    if module.check_mode:
+        module.exit_json(changed=_is_bundle_installed(module, bundle))
+
+    if not _is_bundle_installed(module, bundle):
+        module.exit_json(changed=False, msg="Bundle %s not installed" % bundle)
+
+    cmd = _get_cmd(module, "bundle-remove %s" % bundle)
+    rc, stdout, stderr = module.run_command(cmd, check_rc=False)
+
+    if rc == 0:
+        module.exit_json(changed=True, msg="Bundle %s removed" % bundle, stdout=stdout, stderr=stderr)
+
+    module.fail_json(msg="Unkown Error", stdout=stdout, stderr=stderr)
+
+
+def swupd_update(module):
+    """Updates the os with `swupd update`"""
+    if module.check_mode:
+        module.exit_json(changed=_needs_update(module))
+
+    if not _needs_update(module):
+        module.exit_json(changed=False, msg="There are no updates available")
+
+    cmd = _get_cmd(module, "update")
+    rc, stdout, stderr = module.run_command(cmd, check_rc=False)
+
+    if rc == 0:
+        module.exit_json(changed=True, msg="Update successful", stdout=stdout, stderr=stderr)
+
+    module.fail_json(msg="Unkown Error", stdout=stdout, stderr=stderr)
+
+
+def swupd_verify(module):
+    """Verifies filesystem agains specified or current version"""
+    if module.check_mode:
+        module.exit_json(changed=_needs_verify(module))
+
+    if not _needs_verify(module):
+        module.exit_json(changed=False, msg="No files where changed")
+
+    cmd = _get_cmd(module, "verify --fix")
+    rc, stdout, stderr = module.run_command(cmd, check_rc=False)
+
+    if rc == 0 and (FILES_REPLACED in stdout or FILES_FIXED in stdout or FILES_DELETED in stdout):
+        module.exit_json(changed=True, msg="Fix successful", stdout=stdout, stderr=stderr)
+
+    module.fail_json(msg="Unkown Error", stdout=stdout, stderr=stderr)
+
+
+def main():
+    """The main function."""
+    module = AnsibleModule(
+        argument_spec=dict(
+            contenturl=dict(default=None, type='str'),
+            format=dict(default=None, type='str'),
+            manifest=dict(default=None, aliases=['release', 'version'], type='int'),
+            name=dict(aliases=['bundle'], type='str'),
+            state=dict(default='present', choices=['present', 'absent']),
+            update=dict(default='no', aliases=['bundle'], type='bool'),
+            url=dict(default=None, type='str'),
+            verify=dict(default='no', type='bool'),
+            versionurl=dict(default=None, type='str'),
+        ),
+        required_one_of=[['name', 'update', 'verify']],
+        mutually_exclusive=[['name', 'update', 'verify']],
+        supports_check_mode=True
+    )
+
+    p = module.params
+
+    global SWUPD_CMD
+    SWUPD_CMD = module.get_bin_path("swupd", False)
+
+    if not SWUPD_CMD:
+        module.fail_json(msg="Could not find swupd.")
+
+    if p['update']:
+        swupd_update(module)
+
+    if p['verify']:
+        swupd_verify(module)
+
+    if p['state'] == "present":
+        swupd_install(module, p['name'])
+
+    if p['state'] == "absent":
+        swupd_remove(module, p['name'])
+
+
+if __name__ == '__main__':
+    main()


Test patch: