#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2013, Yap Sok Ann # Written by Yap Sok Ann # Based on apt module written by Matthew Williams # # This module 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. # # This software 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 this software. If not, see . DOCUMENTATION = ''' --- module: portage short_description: Package manager for Gentoo description: - Manages Gentoo packages version_added: "1.4" options: package: description: - Package atom or set, e.g. C(sys-apps/foo) or C(>foo-2.13) or C(@world) required: false default: null state: description: - State of the package atom required: false default: "present" choices: [ "present", "installed", "emerged", "absent", "removed", "unmerged" ] update: description: - Update packages to the best version available (--update) required: false default: null choices: [ "yes" ] deep: description: - Consider the entire dependency tree of packages (--deep) required: false default: null choices: [ "yes" ] newuse: description: - Include installed packages where USE flags have changed (--newuse) required: false default: null choices: [ "yes" ] oneshot: description: - Do not add the packages to the world file (--oneshot) required: false default: null choices: [ "yes" ] noreplace: description: - Do not re-emerge installed packages (--noreplace) required: false default: null choices: [ "yes" ] nodeps: description: - Only merge packages but not their dependencies (--nodeps) required: false default: null choices: [ "yes" ] onlydeps: description: - Only merge packages' dependencies but not the packages (--onlydeps) required: false default: null choices: [ "yes" ] depclean: description: - Remove packages not needed by explicitly merged packages (--depclean) - If no package is specified, clean up the world's dependencies - Otherwise, --depclean serves as a dependency aware version of --unmerge required: false default: null choices: [ "yes" ] quiet: description: - Run emerge in quiet mode (--quiet) required: false default: null choices: [ "yes" ] verbose: description: - Run emerge in verbose mode (--verbose) required: false default: null choices: [ "yes" ] sync: description: - Sync package repositories first - If yes, perform "emerge --sync" - If web, perform "emerge-webrsync" required: false default: null choices: [ "yes", "web" ] requirements: [ gentoolkit ] author: Yap Sok Ann notes: [] ''' EXAMPLES = ''' # Make sure package foo is installed - portage: package=foo state=present # Make sure package foo is not installed - portage: package=foo state=absent # Update package foo to the "best" version - portage: package=foo update=yes # Sync repositories and update world - portage: package=@world update=yes deep=yes sync=yes # Remove unneeded packages - portage: depclean=yes # Remove package foo if it is not explicitly needed - portage: package=foo state=absent depclean=yes ''' import os import pipes def query_package(module, package, action): if package.startswith('@'): return query_set(module, package, action) return query_atom(module, package, action) def query_atom(module, atom, action): cmd = '%s list %s' % (module.equery_path, atom) rc, out, err = module.run_command(cmd) return rc == 0 def query_set(module, set, action): system_sets = [ '@live-rebuild', '@module-rebuild', '@preserved-rebuild', '@security', '@selected', '@system', '@world', '@x11-module-rebuild', ] if set in system_sets: if action == 'unmerge': module.fail_json(msg='set %s cannot be removed' % set) return False world_sets_path = '/var/lib/portage/world_sets' if not os.path.exists(world_sets_path): return False cmd = 'grep %s %s' % (set, world_sets_path) rc, out, err = module.run_command(cmd) return rc == 0 def sync_repositories(module, webrsync=False): if module.check_mode: module.fail_json(msg='check mode not supported by sync') if webrsync: webrsync_path = module.get_bin_path('emerge-webrsync', required=True) cmd = '%s --quiet' % webrsync_path else: cmd = '%s --sync --quiet' % module.emerge_path rc, out, err = module.run_command(cmd) if rc != 0: module.fail_json(msg='could not sync package repositories') # Note: In the 3 functions below, equery is done one-by-one, but emerge is done # in one go. If that is not desirable, split the packages into multiple tasks # instead of joining them together with comma. def emerge_packages(module, packages): p = module.params if not (p['update'] or p['noreplace']): for package in packages: if not query_package(module, package, 'emerge'): break else: module.exit_json(changed=False, msg='Packages already present.') args = [] for flag in [ 'update', 'deep', 'newuse', 'oneshot', 'noreplace', 'nodeps', 'onlydeps', 'quiet', 'verbose', ]: if p[flag]: args.append('--%s' % flag) cmd, (rc, out, err) = run_emerge(module, packages, *args) if rc != 0: module.fail_json( cmd=cmd, rc=rc, stdout=out, stderr=err, msg='Packages not installed.', ) changed = True for line in out.splitlines(): if line.startswith('>>> Emerging (1 of'): break else: changed = False module.exit_json( changed=changed, cmd=cmd, rc=rc, stdout=out, stderr=err, msg='Packages installed.', ) def unmerge_packages(module, packages): p = module.params for package in packages: if query_package(module, package, 'unmerge'): break else: module.exit_json(changed=False, msg='Packages already absent.') args = ['--unmerge'] for flag in ['quiet', 'verbose']: if p[flag]: args.append('--%s' % flag) cmd, (rc, out, err) = run_emerge(module, packages, *args) if rc != 0: module.fail_json( cmd=cmd, rc=rc, stdout=out, stderr=err, msg='Packages not removed.', ) module.exit_json( changed=True, cmd=cmd, rc=rc, stdout=out, stderr=err, msg='Packages removed.', ) def cleanup_packages(module, packages): p = module.params if packages: for package in packages: if query_package(module, package, 'unmerge'): break else: module.exit_json(changed=False, msg='Packages already absent.') args = ['--depclean'] for flag in ['quiet', 'verbose']: if p[flag]: args.append('--%s' % flag) cmd, (rc, out, err) = run_emerge(module, packages, *args) if rc != 0: module.fail_json(cmd=cmd, rc=rc, stdout=out, stderr=err) removed = 0 for line in out.splitlines(): if not line.startswith('Number removed:'): continue parts = line.split(':') removed = int(parts[1].strip()) changed = removed > 0 module.exit_json( changed=changed, cmd=cmd, rc=rc, stdout=out, stderr=err, msg='Depclean completed.', ) def run_emerge(module, packages, *args): args = list(args) if module.check_mode: args.append('--pretend') cmd = [module.emerge_path] + args + packages return cmd, module.run_command(cmd) portage_present_states = ['present', 'emerged', 'installed'] portage_absent_states = ['absent', 'unmerged', 'removed'] def main(): module = AnsibleModule( argument_spec=dict( package=dict(default=None, aliases=['name']), state=dict( default=portage_present_states[0], choices=portage_present_states + portage_absent_states, ), update=dict(default=None, choices=['yes']), deep=dict(default=None, choices=['yes']), newuse=dict(default=None, choices=['yes']), oneshot=dict(default=None, choices=['yes']), noreplace=dict(default=None, choices=['yes']), nodeps=dict(default=None, choices=['yes']), onlydeps=dict(default=None, choices=['yes']), depclean=dict(default=None, choices=['yes']), quiet=dict(default=None, choices=['yes']), verbose=dict(default=None, choices=['yes']), sync=dict(default=None, choices=['yes', 'web']), ), required_one_of=[['package', 'sync', 'depclean']], mutually_exclusive=[['nodeps', 'onlydeps'], ['quiet', 'verbose']], supports_check_mode=True, ) module.emerge_path = module.get_bin_path('emerge', required=True) module.equery_path = module.get_bin_path('equery', required=True) p = module.params if p['sync']: sync_repositories(module, webrsync=(p['sync'] == 'web')) if not p['package']: return packages = p['package'].split(',') if p['package'] else [] if p['depclean']: if packages and p['state'] not in portage_absent_states: module.fail_json( msg='Depclean can only be used with package when the state is ' 'one of: %s' % portage_absent_states, ) cleanup_packages(module, packages) elif p['state'] in portage_present_states: emerge_packages(module, packages) elif p['state'] in portage_absent_states: unmerge_packages(module, packages) # this is magic, see lib/ansible/module_common.py #<> main()