ansible/packaging/portage
Yap Sok Ann 0ccafc5255 Add packaging module for Gentoo Portage.
This is in no way comprehensive enough to cover all use cases, but hopefully
is sufficient to cover the common ones.
2013-10-14 18:57:48 +08:00

389 lines
10 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2013, Yap Sok Ann
# Written by Yap Sok Ann <sokann@gmail.com>
# Based on apt module written by Matthew Williams <matthew@flowroute.com>
#
# 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 <http://www.gnu.org/licenses/>.
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
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()