1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Copyright 2016 Dino Occhialini <dino.occhialini@gmail.com> 5# 6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 7 8from __future__ import absolute_import, division, print_function 9__metaclass__ = type 10 11 12ANSIBLE_METADATA = {'metadata_version': '1.1', 13 'status': ['preview'], 14 'supported_by': 'community'} 15 16 17DOCUMENTATION = ''' 18--- 19module: xbps 20short_description: Manage packages with XBPS 21description: 22 - Manage packages with the XBPS package manager. 23author: 24 - "Dino Occhialini (@dinoocch)" 25 - "Michael Aldridge (@the-maldridge)" 26version_added: "2.3" 27options: 28 name: 29 description: 30 - Name of the package to install, upgrade, or remove. 31 state: 32 description: 33 - Desired state of the package. 34 default: "present" 35 choices: ["present", "absent", "latest"] 36 recurse: 37 description: 38 - When removing a package, also remove its dependencies, provided 39 that they are not required by other packages and were not 40 explicitly installed by a user. 41 type: bool 42 default: 'no' 43 update_cache: 44 description: 45 - Whether or not to refresh the master package lists. This can be 46 run as part of a package installation or as a separate step. 47 type: bool 48 default: 'yes' 49 upgrade: 50 description: 51 - Whether or not to upgrade whole system 52 type: bool 53 default: 'no' 54''' 55 56EXAMPLES = ''' 57# Install package foo 58- xbps: name=foo state=present 59# Upgrade package foo 60- xbps: name=foo state=latest update_cache=yes 61# Remove packages foo and bar 62- xbps: name=foo,bar state=absent 63# Recursively remove package foo 64- xbps: name=foo state=absent recurse=yes 65# Update package cache 66- xbps: update_cache=yes 67# Upgrade packages 68- xbps: upgrade=yes 69''' 70 71RETURN = ''' 72msg: 73 description: Message about results 74 returned: success 75 type: str 76 sample: "System Upgraded" 77packages: 78 description: Packages that are affected/would be affected 79 type: list 80 sample: ["ansible"] 81 returned: success 82''' 83 84 85import os 86 87from ansible.module_utils.basic import AnsibleModule 88 89 90def is_installed(xbps_output): 91 """Returns package install state""" 92 return bool(len(xbps_output)) 93 94 95def query_package(module, xbps_path, name, state="present"): 96 """Returns Package info""" 97 if state == "present": 98 lcmd = "%s %s" % (xbps_path['query'], name) 99 lrc, lstdout, lstderr = module.run_command(lcmd, check_rc=False) 100 if not is_installed(lstdout): 101 # package is not installed locally 102 return False, False 103 104 rcmd = "%s -Sun" % (xbps_path['install']) 105 rrc, rstdout, rstderr = module.run_command(rcmd, check_rc=False) 106 if rrc == 0 or rrc == 17: 107 """Return True to indicate that the package is installed locally, 108 and the result of the version number comparison to determine if the 109 package is up-to-date""" 110 return True, name not in rstdout 111 112 return False, False 113 114 115def update_package_db(module, xbps_path): 116 """Returns True if update_package_db changed""" 117 cmd = "%s -S" % (xbps_path['install']) 118 rc, stdout, stderr = module.run_command(cmd, check_rc=False) 119 120 if rc != 0: 121 module.fail_json(msg="Could not update package db") 122 if "avg rate" in stdout: 123 return True 124 else: 125 return False 126 127 128def upgrade(module, xbps_path): 129 """Returns true is full upgrade succeeds""" 130 cmdupgrade = "%s -uy" % (xbps_path['install']) 131 cmdneedupgrade = "%s -un" % (xbps_path['install']) 132 133 rc, stdout, stderr = module.run_command(cmdneedupgrade, check_rc=False) 134 if rc == 0: 135 if(len(stdout.splitlines()) == 0): 136 module.exit_json(changed=False, msg='Nothing to upgrade') 137 else: 138 rc, stdout, stderr = module.run_command(cmdupgrade, check_rc=False) 139 if rc == 0: 140 module.exit_json(changed=True, msg='System upgraded') 141 else: 142 module.fail_json(msg="Could not upgrade") 143 else: 144 module.fail_json(msg="Could not upgrade") 145 146 147def remove_packages(module, xbps_path, packages): 148 """Returns true if package removal succeeds""" 149 changed_packages = [] 150 # Using a for loop in case of error, we can report the package that failed 151 for package in packages: 152 # Query the package first, to see if we even need to remove 153 installed, updated = query_package(module, xbps_path, package) 154 if not installed: 155 continue 156 157 cmd = "%s -y %s" % (xbps_path['remove'], package) 158 rc, stdout, stderr = module.run_command(cmd, check_rc=False) 159 160 if rc != 0: 161 module.fail_json(msg="failed to remove %s" % (package)) 162 163 changed_packages.append(package) 164 165 if len(changed_packages) > 0: 166 167 module.exit_json(changed=True, msg="removed %s package(s)" % 168 len(changed_packages), packages=changed_packages) 169 170 module.exit_json(changed=False, msg="package(s) already absent") 171 172 173def install_packages(module, xbps_path, state, packages): 174 """Returns true if package install succeeds.""" 175 toInstall = [] 176 for i, package in enumerate(packages): 177 """If the package is installed and state == present or state == latest 178 and is up-to-date then skip""" 179 installed, updated = query_package(module, xbps_path, package) 180 if installed and (state == 'present' or 181 (state == 'latest' and updated)): 182 continue 183 184 toInstall.append(package) 185 186 if len(toInstall) == 0: 187 module.exit_json(changed=False, msg="Nothing to Install") 188 189 cmd = "%s -y %s" % (xbps_path['install'], " ".join(toInstall)) 190 rc, stdout, stderr = module.run_command(cmd, check_rc=False) 191 192 if rc != 0 and not (state == 'latest' and rc == 17): 193 module.fail_json(msg="failed to install %s" % (package)) 194 195 module.exit_json(changed=True, msg="installed %s package(s)" 196 % (len(toInstall)), 197 packages=toInstall) 198 199 module.exit_json(changed=False, msg="package(s) already installed", 200 packages=[]) 201 202 203def check_packages(module, xbps_path, packages, state): 204 """Returns change status of command""" 205 would_be_changed = [] 206 for package in packages: 207 installed, updated = query_package(module, xbps_path, package) 208 if ((state in ["present", "latest"] and not installed) or 209 (state == "absent" and installed) or 210 (state == "latest" and not updated)): 211 would_be_changed.append(package) 212 if would_be_changed: 213 if state == "absent": 214 state = "removed" 215 module.exit_json(changed=True, msg="%s package(s) would be %s" % ( 216 len(would_be_changed), state), 217 packages=would_be_changed) 218 else: 219 module.exit_json(changed=False, msg="package(s) already %s" % state, 220 packages=[]) 221 222 223def main(): 224 """Returns, calling appropriate command""" 225 226 module = AnsibleModule( 227 argument_spec=dict( 228 name=dict(default=None, aliases=['pkg', 'package'], type='list'), 229 state=dict(default='present', choices=['present', 'installed', 230 'latest', 'absent', 231 'removed']), 232 recurse=dict(default=False, type='bool'), 233 force=dict(default=False, type='bool'), 234 upgrade=dict(default=False, type='bool'), 235 update_cache=dict(default=True, aliases=['update-cache'], 236 type='bool') 237 ), 238 required_one_of=[['name', 'update_cache', 'upgrade']], 239 supports_check_mode=True) 240 241 xbps_path = dict() 242 xbps_path['install'] = module.get_bin_path('xbps-install', True) 243 xbps_path['query'] = module.get_bin_path('xbps-query', True) 244 xbps_path['remove'] = module.get_bin_path('xbps-remove', True) 245 246 if not os.path.exists(xbps_path['install']): 247 module.fail_json(msg="cannot find xbps, in path %s" 248 % (xbps_path['install'])) 249 250 p = module.params 251 252 # normalize the state parameter 253 if p['state'] in ['present', 'installed']: 254 p['state'] = 'present' 255 elif p['state'] in ['absent', 'removed']: 256 p['state'] = 'absent' 257 258 if p["update_cache"] and not module.check_mode: 259 changed = update_package_db(module, xbps_path) 260 if p['name'] is None and not p['upgrade']: 261 if changed: 262 module.exit_json(changed=True, 263 msg='Updated the package master lists') 264 else: 265 module.exit_json(changed=False, 266 msg='Package list already up to date') 267 268 if (p['update_cache'] and module.check_mode and not 269 (p['name'] or p['upgrade'])): 270 module.exit_json(changed=True, 271 msg='Would have updated the package cache') 272 273 if p['upgrade']: 274 upgrade(module, xbps_path) 275 276 if p['name']: 277 pkgs = p['name'] 278 279 if module.check_mode: 280 check_packages(module, xbps_path, pkgs, p['state']) 281 282 if p['state'] in ['present', 'latest']: 283 install_packages(module, xbps_path, p['state'], pkgs) 284 elif p['state'] == 'absent': 285 remove_packages(module, xbps_path, pkgs) 286 287 288if __name__ == "__main__": 289 main() 290