1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# (c) 2013, bleader 5# Written by bleader <bleader@ratonland.org> 6# Based on pkgin module written by Shaun Zinck <shaun.zinck at gmail.com> 7# that was based on pacman module written by Afterburn <https://github.com/afterburn> 8# that was based on apt module written by Matthew Williams <matthew@flowroute.com> 9# 10# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 11 12from __future__ import absolute_import, division, print_function 13__metaclass__ = type 14 15 16ANSIBLE_METADATA = {'metadata_version': '1.1', 17 'status': ['preview'], 18 'supported_by': 'community'} 19 20 21DOCUMENTATION = ''' 22--- 23module: pkgng 24short_description: Package manager for FreeBSD >= 9.0 25description: 26 - Manage binary packages for FreeBSD using 'pkgng' which is available in versions after 9.0. 27version_added: "1.2" 28options: 29 name: 30 description: 31 - Name or list of names of packages to install/remove. 32 required: true 33 state: 34 description: 35 - State of the package. 36 - 'Note: "latest" added in 2.7' 37 choices: [ 'present', 'latest', 'absent' ] 38 required: false 39 default: present 40 cached: 41 description: 42 - Use local package base instead of fetching an updated one. 43 type: bool 44 required: false 45 default: no 46 annotation: 47 description: 48 - A comma-separated list of keyvalue-pairs of the form 49 C(<+/-/:><key>[=<value>]). A C(+) denotes adding an annotation, a 50 C(-) denotes removing an annotation, and C(:) denotes modifying an 51 annotation. 52 If setting or modifying annotations, a value must be provided. 53 required: false 54 version_added: "1.6" 55 pkgsite: 56 description: 57 - For pkgng versions before 1.1.4, specify packagesite to use 58 for downloading packages. If not specified, use settings from 59 C(/usr/local/etc/pkg.conf). 60 - For newer pkgng versions, specify a the name of a repository 61 configured in C(/usr/local/etc/pkg/repos). 62 required: false 63 rootdir: 64 description: 65 - For pkgng versions 1.5 and later, pkg will install all packages 66 within the specified root directory. 67 - Can not be used together with I(chroot) or I(jail) options. 68 required: false 69 chroot: 70 version_added: "2.1" 71 description: 72 - Pkg will chroot in the specified environment. 73 - Can not be used together with I(rootdir) or I(jail) options. 74 required: false 75 jail: 76 version_added: "2.4" 77 description: 78 - Pkg will execute in the given jail name or id. 79 - Can not be used together with I(chroot) or I(rootdir) options. 80 autoremove: 81 version_added: "2.2" 82 description: 83 - Remove automatically installed packages which are no longer needed. 84 required: false 85 type: bool 86 default: no 87author: "bleader (@bleader)" 88notes: 89 - When using pkgsite, be careful that already in cache packages won't be downloaded again. 90 - When used with a `loop:` each package will be processed individually, 91 it is much more efficient to pass the list directly to the `name` option. 92''' 93 94EXAMPLES = ''' 95- name: Install package foo 96 pkgng: 97 name: foo 98 state: present 99 100- name: Annotate package foo and bar 101 pkgng: 102 name: foo,bar 103 annotation: '+test1=baz,-test2,:test3=foobar' 104 105- name: Remove packages foo and bar 106 pkgng: 107 name: foo,bar 108 state: absent 109 110# "latest" support added in 2.7 111- name: Upgrade package baz 112 pkgng: 113 name: baz 114 state: latest 115''' 116 117 118import re 119from ansible.module_utils.basic import AnsibleModule 120 121 122def query_package(module, pkgng_path, name, dir_arg): 123 124 rc, out, err = module.run_command("%s %s info -g -e %s" % (pkgng_path, dir_arg, name)) 125 126 if rc == 0: 127 return True 128 129 return False 130 131 132def query_update(module, pkgng_path, name, dir_arg, old_pkgng, pkgsite): 133 134 # Check to see if a package upgrade is available. 135 # rc = 0, no updates available or package not installed 136 # rc = 1, updates available 137 if old_pkgng: 138 rc, out, err = module.run_command("%s %s upgrade -g -n %s" % (pkgsite, pkgng_path, name)) 139 else: 140 rc, out, err = module.run_command("%s %s upgrade %s -g -n %s" % (pkgng_path, dir_arg, pkgsite, name)) 141 142 if rc == 1: 143 return True 144 145 return False 146 147 148def pkgng_older_than(module, pkgng_path, compare_version): 149 150 rc, out, err = module.run_command("%s -v" % pkgng_path) 151 version = [int(x) for x in re.split(r'[\._]', out)] 152 153 i = 0 154 new_pkgng = True 155 while compare_version[i] == version[i]: 156 i += 1 157 if i == min(len(compare_version), len(version)): 158 break 159 else: 160 if compare_version[i] > version[i]: 161 new_pkgng = False 162 return not new_pkgng 163 164 165def remove_packages(module, pkgng_path, packages, dir_arg): 166 167 remove_c = 0 168 # Using a for loop in case of error, we can report the package that failed 169 for package in packages: 170 # Query the package first, to see if we even need to remove 171 if not query_package(module, pkgng_path, package, dir_arg): 172 continue 173 174 if not module.check_mode: 175 rc, out, err = module.run_command("%s %s delete -y %s" % (pkgng_path, dir_arg, package)) 176 177 if not module.check_mode and query_package(module, pkgng_path, package, dir_arg): 178 module.fail_json(msg="failed to remove %s: %s" % (package, out)) 179 180 remove_c += 1 181 182 if remove_c > 0: 183 184 return (True, "removed %s package(s)" % remove_c) 185 186 return (False, "package(s) already absent") 187 188 189def install_packages(module, pkgng_path, packages, cached, pkgsite, dir_arg, state): 190 191 install_c = 0 192 193 # as of pkg-1.1.4, PACKAGESITE is deprecated in favor of repository definitions 194 # in /usr/local/etc/pkg/repos 195 old_pkgng = pkgng_older_than(module, pkgng_path, [1, 1, 4]) 196 if pkgsite != "": 197 if old_pkgng: 198 pkgsite = "PACKAGESITE=%s" % (pkgsite) 199 else: 200 pkgsite = "-r %s" % (pkgsite) 201 202 # This environment variable skips mid-install prompts, 203 # setting them to their default values. 204 batch_var = 'env BATCH=yes' 205 206 if not module.check_mode and not cached: 207 if old_pkgng: 208 rc, out, err = module.run_command("%s %s update" % (pkgsite, pkgng_path)) 209 else: 210 rc, out, err = module.run_command("%s %s update" % (pkgng_path, dir_arg)) 211 if rc != 0: 212 module.fail_json(msg="Could not update catalogue [%d]: %s %s" % (rc, out, err)) 213 214 for package in packages: 215 already_installed = query_package(module, pkgng_path, package, dir_arg) 216 if already_installed and state == "present": 217 continue 218 219 update_available = query_update(module, pkgng_path, package, dir_arg, old_pkgng, pkgsite) 220 if not update_available and already_installed and state == "latest": 221 continue 222 223 if not module.check_mode: 224 if already_installed: 225 action = "upgrade" 226 else: 227 action = "install" 228 if old_pkgng: 229 rc, out, err = module.run_command("%s %s %s %s -g -U -y %s" % (batch_var, pkgsite, pkgng_path, action, package)) 230 else: 231 rc, out, err = module.run_command("%s %s %s %s %s -g -U -y %s" % (batch_var, pkgng_path, dir_arg, action, pkgsite, package)) 232 233 if not module.check_mode and not query_package(module, pkgng_path, package, dir_arg): 234 module.fail_json(msg="failed to %s %s: %s" % (action, package, out), stderr=err) 235 236 install_c += 1 237 238 if install_c > 0: 239 return (True, "added %s package(s)" % (install_c)) 240 241 return (False, "package(s) already %s" % (state)) 242 243 244def annotation_query(module, pkgng_path, package, tag, dir_arg): 245 rc, out, err = module.run_command("%s %s info -g -A %s" % (pkgng_path, dir_arg, package)) 246 match = re.search(r'^\s*(?P<tag>%s)\s*:\s*(?P<value>\w+)' % tag, out, flags=re.MULTILINE) 247 if match: 248 return match.group('value') 249 return False 250 251 252def annotation_add(module, pkgng_path, package, tag, value, dir_arg): 253 _value = annotation_query(module, pkgng_path, package, tag, dir_arg) 254 if not _value: 255 # Annotation does not exist, add it. 256 rc, out, err = module.run_command('%s %s annotate -y -A %s %s "%s"' 257 % (pkgng_path, dir_arg, package, tag, value)) 258 if rc != 0: 259 module.fail_json(msg="could not annotate %s: %s" 260 % (package, out), stderr=err) 261 return True 262 elif _value != value: 263 # Annotation exists, but value differs 264 module.fail_json( 265 mgs="failed to annotate %s, because %s is already set to %s, but should be set to %s" 266 % (package, tag, _value, value)) 267 return False 268 else: 269 # Annotation exists, nothing to do 270 return False 271 272 273def annotation_delete(module, pkgng_path, package, tag, value, dir_arg): 274 _value = annotation_query(module, pkgng_path, package, tag, dir_arg) 275 if _value: 276 rc, out, err = module.run_command('%s %s annotate -y -D %s %s' 277 % (pkgng_path, dir_arg, package, tag)) 278 if rc != 0: 279 module.fail_json(msg="could not delete annotation to %s: %s" 280 % (package, out), stderr=err) 281 return True 282 return False 283 284 285def annotation_modify(module, pkgng_path, package, tag, value, dir_arg): 286 _value = annotation_query(module, pkgng_path, package, tag, dir_arg) 287 if not value: 288 # No such tag 289 module.fail_json(msg="could not change annotation to %s: tag %s does not exist" 290 % (package, tag)) 291 elif _value == value: 292 # No change in value 293 return False 294 else: 295 rc, out, err = module.run_command('%s %s annotate -y -M %s %s "%s"' 296 % (pkgng_path, dir_arg, package, tag, value)) 297 if rc != 0: 298 module.fail_json(msg="could not change annotation annotation to %s: %s" 299 % (package, out), stderr=err) 300 return True 301 302 303def annotate_packages(module, pkgng_path, packages, annotation, dir_arg): 304 annotate_c = 0 305 annotations = map(lambda _annotation: 306 re.match(r'(?P<operation>[\+-:])(?P<tag>\w+)(=(?P<value>\w+))?', 307 _annotation).groupdict(), 308 re.split(r',', annotation)) 309 310 operation = { 311 '+': annotation_add, 312 '-': annotation_delete, 313 ':': annotation_modify 314 } 315 316 for package in packages: 317 for _annotation in annotations: 318 if operation[_annotation['operation']](module, pkgng_path, package, _annotation['tag'], _annotation['value']): 319 annotate_c += 1 320 321 if annotate_c > 0: 322 return (True, "added %s annotations." % annotate_c) 323 return (False, "changed no annotations") 324 325 326def autoremove_packages(module, pkgng_path, dir_arg): 327 rc, out, err = module.run_command("%s %s autoremove -n" % (pkgng_path, dir_arg)) 328 329 autoremove_c = 0 330 331 match = re.search('^Deinstallation has been requested for the following ([0-9]+) packages', out, re.MULTILINE) 332 if match: 333 autoremove_c = int(match.group(1)) 334 335 if autoremove_c == 0: 336 return False, "no package(s) to autoremove" 337 338 if not module.check_mode: 339 rc, out, err = module.run_command("%s %s autoremove -y" % (pkgng_path, dir_arg)) 340 341 return True, "autoremoved %d package(s)" % (autoremove_c) 342 343 344def main(): 345 module = AnsibleModule( 346 argument_spec=dict( 347 state=dict(default="present", choices=["present", "latest", "absent"], required=False), 348 name=dict(aliases=["pkg"], required=True, type='list'), 349 cached=dict(default=False, type='bool'), 350 annotation=dict(default="", required=False), 351 pkgsite=dict(default="", required=False), 352 rootdir=dict(default="", required=False, type='path'), 353 chroot=dict(default="", required=False, type='path'), 354 jail=dict(default="", required=False, type='str'), 355 autoremove=dict(default=False, type='bool')), 356 supports_check_mode=True, 357 mutually_exclusive=[["rootdir", "chroot", "jail"]]) 358 359 pkgng_path = module.get_bin_path('pkg', True) 360 361 p = module.params 362 363 pkgs = p["name"] 364 365 changed = False 366 msgs = [] 367 dir_arg = "" 368 369 if p["rootdir"] != "": 370 old_pkgng = pkgng_older_than(module, pkgng_path, [1, 5, 0]) 371 if old_pkgng: 372 module.fail_json(msg="To use option 'rootdir' pkg version must be 1.5 or greater") 373 else: 374 dir_arg = "--rootdir %s" % (p["rootdir"]) 375 376 if p["chroot"] != "": 377 dir_arg = '--chroot %s' % (p["chroot"]) 378 379 if p["jail"] != "": 380 dir_arg = '--jail %s' % (p["jail"]) 381 382 if p["state"] in ("present", "latest"): 383 _changed, _msg = install_packages(module, pkgng_path, pkgs, p["cached"], p["pkgsite"], dir_arg, p["state"]) 384 changed = changed or _changed 385 msgs.append(_msg) 386 387 elif p["state"] == "absent": 388 _changed, _msg = remove_packages(module, pkgng_path, pkgs, dir_arg) 389 changed = changed or _changed 390 msgs.append(_msg) 391 392 if p["autoremove"]: 393 _changed, _msg = autoremove_packages(module, pkgng_path, dir_arg) 394 changed = changed or _changed 395 msgs.append(_msg) 396 397 if p["annotation"]: 398 _changed, _msg = annotate_packages(module, pkgng_path, pkgs, p["annotation"], dir_arg) 399 changed = changed or _changed 400 msgs.append(_msg) 401 402 module.exit_json(changed=changed, msg=", ".join(msgs)) 403 404 405if __name__ == '__main__': 406 main() 407