diff --git a/packaging/portage b/packaging/portage new file mode 100644 index 00000000000..c68dc0ebfa4 --- /dev/null +++ b/packaging/portage @@ -0,0 +1,389 @@ +#!/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()