Update homebrew module.

This commit is contained in:
Daniel Jaouen 2014-02-19 14:46:44 -05:00
parent 54eeac3696
commit bea496b47c

View file

@ -2,6 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# (c) 2013, Andrew Dunham <andrew@du.nham.ca> # (c) 2013, Andrew Dunham <andrew@du.nham.ca>
# (c) 2013, Daniel Jaouen <dcj24@cornell.edu>
#
# Based on macports (Jimmy Tang <jcftang@gmail.com>) # Based on macports (Jimmy Tang <jcftang@gmail.com>)
# #
# This module is free software: you can redistribute it and/or modify # This module is free software: you can redistribute it and/or modify
@ -24,7 +26,7 @@ author: Andrew Dunham
short_description: Package manager for Homebrew short_description: Package manager for Homebrew
description: description:
- Manages Homebrew packages - Manages Homebrew packages
version_added: "1.4" version_added: "1.1"
options: options:
name: name:
description: description:
@ -33,7 +35,7 @@ options:
state: state:
description: description:
- state of the package - state of the package
choices: [ 'present', 'absent' ] choices: [ 'head', 'latest', 'installed', 'linked', 'uninstalled' ]
required: false required: false
default: present default: present
update_homebrew: update_homebrew:
@ -42,135 +44,739 @@ options:
required: false required: false
default: "no" default: "no"
choices: [ "yes", "no" ] choices: [ "yes", "no" ]
install_options:
description:
- options flags to install a package
required: false
default: null
notes: [] notes: []
''' '''
EXAMPLES = ''' EXAMPLES = '''
- homebrew: name=foo state=present - homebrew: name=foo state=present
- homebrew: name=foo state=present update_homebrew=yes - homebrew: name=foo state=present update_homebrew=yes
- homebrew: name=foo state=latest update_homebrew=yes
- homebrew: update_homebrew=yes upgrade=yes
- homebrew: name=foo state=head
- homebrew: name=foo state=linked
- homebrew: name=foo state=absent - homebrew: name=foo state=absent
- homebrew: name=foo,bar state=absent - homebrew: name=foo,bar state=absent
- homebrew: name=foo state=present install_options=with-baz,enable-debug
''' '''
import os.path
def update_homebrew(module, brew_path): import re
""" Updates packages list. """
rc, out, err = module.run_command("%s update" % brew_path)
if rc != 0:
module.fail_json(msg="could not update homebrew")
def query_package(module, brew_path, name, state="present"): # exceptions -------------------------------------------------------------- {{{
""" Returns whether a package is installed or not. """ class HomebrewException(Exception):
pass
# /exceptions ------------------------------------------------------------- }}}
if state == "present":
rc, out, err = module.run_command("%s list %s" % (brew_path, name)) # utils ------------------------------------------------------------------- {{{
if rc == 0: def _create_regex_group(s):
lines = (line.strip() for line in s.split('\n') if line.strip())
chars = filter(None, (line.split('#')[0].strip() for line in lines))
group = r'[^' + r''.join(chars) + r']'
return re.compile(group)
# /utils ------------------------------------------------------------------ }}}
class Homebrew(object):
'''A class to manage Homebrew packages.'''
# class regexes ------------------------------------------------ {{{
VALID_PATH_CHARS = r'''
\w # alphanumeric characters (i.e., [a-zA-Z0-9_])
\s # spaces
: # colons
{sep} # the OS-specific path separator
- # dashes
'''.format(sep=os.path.sep)
VALID_BREW_PATH_CHARS = r'''
\w # alphanumeric characters (i.e., [a-zA-Z0-9_])
\s # spaces
{sep} # the OS-specific path separator
- # dashes
'''.format(sep=os.path.sep)
VALID_PACKAGE_CHARS = r'''
\w # alphanumeric characters (i.e., [a-zA-Z0-9_])
- # dashes
'''
INVALID_PATH_REGEX = _create_regex_group(VALID_PATH_CHARS)
INVALID_BREW_PATH_REGEX = _create_regex_group(VALID_BREW_PATH_CHARS)
INVALID_PACKAGE_REGEX = _create_regex_group(VALID_PACKAGE_CHARS)
# /class regexes ----------------------------------------------- }}}
# class validations -------------------------------------------- {{{
@classmethod
def valid_path(cls, path):
'''
`path` must be one of:
- list of paths
- a string containing only:
- alphanumeric characters
- dashes
- spaces
- colons
- os.path.sep
'''
if isinstance(path, basestring):
return not cls.INVALID_PATH_REGEX.search(path)
try:
iter(path)
except TypeError:
return False
else:
paths = path
return all(cls.valid_brew_path(path_) for path_ in paths)
@classmethod
def valid_brew_path(cls, brew_path):
'''
`brew_path` must be one of:
- None
- a string containing only:
- alphanumeric characters
- dashes
- spaces
- os.path.sep
'''
if brew_path is None:
return True return True
return (
isinstance(brew_path, basestring)
and not cls.INVALID_BREW_PATH_REGEX.search(brew_path)
)
@classmethod
def valid_package(cls, package):
'''A valid package is either None or alphanumeric.'''
if package is None:
return True
return (
isinstance(package, basestring)
and not cls.INVALID_PACKAGE_REGEX.search(package)
)
@classmethod
def valid_state(cls, state):
'''
A valid state is one of:
- None
- installed
- upgraded
- head
- linked
- absent
'''
if state is None:
return True
else:
return (
isinstance(state, basestring)
and state.lower() in (
'installed',
'upgraded',
'head',
'linked',
'absent',
)
)
@classmethod
def valid_module(cls, module):
'''A valid module is an instance of AnsibleModule.'''
return isinstance(module, AnsibleModule)
# /class validations ------------------------------------------- }}}
# class properties --------------------------------------------- {{{
@property
def module(self):
return self._module
@module.setter
def module(self, module):
if not self.valid_module(module):
self._module = None
self.failed = True
self.message = 'Invalid module: {0}.'.format(module)
raise HomebrewException(self.message)
else:
self._module = module
return module
@property
def path(self):
return self._path
@path.setter
def path(self, path):
if not self.valid_path(path):
self._path = []
self.failed = True
self.message = 'Invalid path: {0}.'.format(path)
raise HomebrewException(self.message)
else:
if isinstance(path, basestring):
self._path = path.split(':')
else:
self._path = path
return path
@property
def brew_path(self):
return self._brew_path
@brew_path.setter
def brew_path(self, brew_path):
if not self.valid_brew_path(brew_path):
self._brew_path = None
self.failed = True
self.message = 'Invalid brew_path: {0}.'.format(brew_path)
raise HomebrewException(self.message)
else:
self._brew_path = brew_path
return brew_path
@property
def params(self):
return self._params
@params.setter
def params(self, params):
self._params = self.module.params
return self._params
@property
def current_package(self):
return self._current_package
@current_package.setter
def current_package(self, package):
if not self.valid_package(package):
self._current_package = None
self.failed = True
self.message = 'Invalid package: {0}.'.format(package)
raise HomebrewException(self.message)
else:
self._current_package = package
return package
# /class properties -------------------------------------------- }}}
def __init__(self, module, path=None, packages=None, state=None,
update_homebrew=False, ):
self._setup_status_vars()
self._setup_instance_vars(module=module, path=path, packages=packages,
state=state, update_homebrew=update_homebrew, )
self._prep()
# prep --------------------------------------------------------- {{{
def _setup_status_vars(self):
self.failed = False
self.changed = False
self.changed_count = 0
self.unchanged_count = 0
self.message = ''
def _setup_instance_vars(self, **kwargs):
for key, val in kwargs.iteritems():
setattr(self, key, val)
def _prep(self):
self._prep_path()
self._prep_brew_path()
def _prep_path(self):
if not self.path:
self.path = ['/usr/local/bin']
def _prep_brew_path(self):
if not self.module:
self.brew_path = None
self.failed = True
self.message = 'AnsibleModule not set.'
raise HomebrewException(self.message)
self.brew_path = self.module.get_bin_path(
'brew',
required=True,
opt_dirs=self.path,
)
if not self.brew_path:
self.brew_path = None
self.failed = True
self.message = 'Unable to locate homebrew executable.'
raise HomebrewException('Unable to locate homebrew executable.')
return self.brew_path
def _status(self):
return (self.failed, self.changed, self.message)
# /prep -------------------------------------------------------- }}}
def run(self):
try:
self._run()
except HomebrewException:
pass
if not self.failed and (self.changed_count + self.unchanged_count > 1):
self.message = "Changed: %d, Unchanged: %d" % (
self.changed_count,
self.unchanged_count,
)
(failed, changed, message) = self._status()
return (failed, changed, message)
# checks ------------------------------------------------------- {{{
def _current_package_is_installed(self):
if not self.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
rc, out, err = self.module.run_command(
"{brew_path} list -m1 | grep -q '^{package}$'".format(
brew_path=self.brew_path,
package=self.current_package,
)
)
if rc == 0:
return True
else:
return False return False
def _outdated_packages(self):
rc, out, err = self.module.run_command([
self.brew_path,
'outdated',
])
return [line.split(' ')[0].strip() for line in out.split('\n') if line]
def remove_packages(module, brew_path, packages): def _current_package_is_outdated(self):
""" Uninstalls one or more packages if installed. """ if not self.valid_package(self.current_package):
return False
removed_count = 0 return self.current_package in self._outdated_packages()
# Using a for loop incase of error, we can report the package that failed def _current_package_is_installed_from_head(self):
for package in packages: if not Homebrew.valid_package(self.current_package):
# Query the package first, to see if we even need to remove. return False
if not query_package(module, brew_path, package): elif not self._current_package_is_installed():
continue return False
if module.check_mode: rc, out, err = self.module.run_command([
module.exit_json(changed=True) self.brew_path,
rc, out, err = module.run_command([brew_path, 'remove', package]) 'info',
self.current_package,
])
if query_package(module, brew_path, package): try:
module.fail_json(msg="failed to remove %s: %s" % (package, out.strip())) version_info = [line for line in out.split('\n') if line][0]
except IndexError:
return False
removed_count += 1 return version_info.split(' ')[-1] == 'HEAD'
# /checks ------------------------------------------------------ }}}
if removed_count > 0: # commands ----------------------------------------------------- {{{
module.exit_json(changed=True, msg="removed %d package(s)" % removed_count) def _run(self):
if self.update_homebrew:
self._update_homebrew()
module.exit_json(changed=False, msg="package(s) already absent") if self.packages:
if self.state == 'installed':
return self._install_packages()
elif self.state == 'upgraded':
return self._upgrade_packages()
elif self.state == 'head':
return self._install_packages()
# elif self.state == 'linked':
# return self._linked()
elif self.state == 'absent':
return self._uninstall_packages()
# updated -------------------------------- {{{
def _update_homebrew(self):
rc, out, err = self.module.run_command([
self.brew_path,
'update',
])
if rc == 0:
if out and isinstance(out, basestring):
already_updated = any(
re.search(r'Already up-to-date.', s.strip(), re.IGNORECASE)
for s in out.split('\n')
if s
)
if not already_updated:
self.changed = True
self.message = 'Homebrew updated successfully.'
else:
self.message = 'Homebrew already up-to-date.'
return True
else:
self.failed = True
self.message = err.strip()
raise HomebrewException(self.message)
# /updated ------------------------------- }}}
# installed ------------------------------ {{{
def _install_current_package(self):
if not self.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
if self._current_package_is_installed():
self.unchanged_count += 1
self.message = 'Package already installed: {0}'.format(
self.current_package,
)
return True
if self.module.check_mode:
self.changed = True
self.message = 'Package would be installed: {0}'.format(
self.current_package
)
raise HomebrewException(self.message)
if self.state == 'head':
head = '--HEAD'
else:
head = None
cmd = [opt
for opt in (self.brew_path, 'install', self.current_package, head)
if opt]
rc, out, err = self.module.run_command(cmd)
if self._current_package_is_installed():
self.changed_count += 1
self.changed = True
self.message = 'Package installed: {0}'.format(self.current_package)
return True
else:
self.failed = True
self.message = err.strip()
raise HomebrewException(self.message)
def _install_packages(self):
for package in self.packages:
self.current_package = package
self._install_current_package()
return True
# /installed ----------------------------- }}}
# upgraded ------------------------------- {{{
def _upgrade_current_package(self):
command = 'upgrade'
if not self.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
if not self._current_package_is_installed():
command = 'install'
if self._current_package_is_installed() and not self._current_package_is_outdated():
self.message = 'Package is already upgraded: {0}'.format(
self.current_package,
)
self.unchanged_count += 1
return True
if self.module.check_mode:
self.changed = True
self.message = 'Package would be upgraded: {0}'.format(
self.current_package
)
raise HomebrewException(self.message)
rc, out, err = self.module.run_command([
self.brew_path,
command,
self.current_package,
])
if not self._current_package_is_outdated():
self.changed_count += 1
self.changed = True
self.message = 'Package upgraded: {0}'.format(self.current_package)
return True
else:
self.failed = True
self.message = err.strip()
raise HomebrewException(self.message)
def _upgrade_all_packages(self):
rc, out, err = self.module.run_command([
self.brew_path,
'upgrade',
])
if rc == 0:
self.changed = True
self.message = 'All packages upgraded.'
return True
else:
self.failed = True
self.message = err.strip()
raise HomebrewException(self.message)
def _upgrade_packages(self):
if not self.packages:
self._upgrade_all_packages()
else:
for package in self.packages:
self.current_package = package
self._upgrade_current_package()
return True
# /upgraded ------------------------------ }}}
# uninstalled ---------------------------- {{{
def _uninstall_current_package(self):
if not self.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
if not self._current_package_is_installed():
self.unchanged_count += 1
self.message = 'Package already uninstalled: {0}'.format(
self.current_package,
)
return True
if self.module.check_mode:
self.changed = True
self.message = 'Package would be uninstalled: {0}'.format(
self.current_package
)
raise HomebrewException(self.message)
cmd = [opt
for opt in (self.brew_path, 'uninstall', self.current_package)
if opt]
rc, out, err = self.module.run_command(cmd)
if not self._current_package_is_installed():
self.changed_count += 1
self.changed = True
self.message = 'Package uninstalled: {0}'.format(self.current_package)
return True
else:
self.failed = True
self.message = err.strip()
raise HomebrewException(self.message)
def _uninstall_packages(self):
for package in self.packages:
self.current_package = package
self._uninstall_current_package()
return True
# /uninstalled ----------------------------- }}}
# /commands ---------------------------------------------------- }}}
def install_packages(module, brew_path, packages, options): # def link_package(module, brew_path, package):
""" Installs one or more packages if not already installed. """ # """ Links a single homebrew package. """
#
installed_count = 0 # failed, changed, msg = False, False, ''
#
for package in packages: # if not a_valid_package(package):
if query_package(module, brew_path, package): # failed = True
continue # msg = 'invalid package'
#
if module.check_mode: # elif not query_package(module, brew_path, package):
module.exit_json(changed=True) # failed = True
# msg = 'not installed'
cmd = [brew_path, 'install', package] #
if options: # else:
cmd.extend(options) # if module.check_mode:
rc, out, err = module.run_command(cmd) # module.exit_json(changed=True)
#
if not query_package(module, brew_path, package): # rc, out, err = module.run_command([
module.fail_json(msg="failed to install %s: '%s' %s" % (package, cmd, out.strip())) # brew_path,
# 'link',
installed_count += 1 # package,
# ])
if installed_count > 0: #
module.exit_json(changed=True, msg="installed %d package(s)" % (installed_count,)) # if rc:
# failed = True
module.exit_json(changed=False, msg="package(s) already present") # msg = out.strip()
# else:
def generate_options_string(install_options): # if err.strip().lower().find('already linked') != -1:
if install_options is None: # msg = 'already linked'
return None # else:
# changed = True
options = [] # msg = 'linked'
#
for option in install_options: # return (failed, changed, msg)
options.append('--%s' % option) #
#
return options # def link_packages(module, brew_path, packages):
# """ Upgrades one or more packages. """
#
# failed, linked, unchanged, msg = False, 0, 0, ''
#
# for package in packages:
# failed, changed, msg = link_package(module, brew_path, package)
# if failed:
# break
# if changed:
# linked += 1
# else:
# unchanged += 1
#
# if failed:
# msg = 'installed: %d, unchanged: %d, error: ' + msg
# msg = msg % (linked, unchanged)
# elif linked:
# changed = True
# msg = 'linked: %d, unchanged: %d' % (linked, unchanged)
# else:
# msg = 'linked: %d, unchanged: %d' % (linked, unchanged)
#
# return (failed, changed, msg)
#
#
# def unlink_package(module, brew_path, package):
# """ Unlinks a single homebrew package. """
#
# failed, changed, msg = False, False, ''
#
# if not a_valid_package(package):
# failed = True
# msg = 'invalid package'
#
# elif not query_package(module, brew_path, package):
# failed = True
# msg = 'not installed'
#
# else:
# if module.check_mode:
# module.exit_json(changed=True)
#
# rc, out, err = module.run_command([
# brew_path,
# 'unlink',
# package,
# ])
#
# if rc:
# failed = True
# msg = out.strip()
# else:
# if out.find('0 links') != -1:
# msg = 'already unlinked'
# else:
# changed = True
# msg = 'linked'
#
# return (failed, changed, msg)
#
#
# def unlink_packages(module, brew_path, packages):
# """ Unlinks one or more packages. """
#
# failed, unlinked, unchanged, msg = False, 0, 0, ''
#
# for package in packages:
# failed, changed, msg = unlink_package(module, brew_path, package)
# if failed:
# break
# if changed:
# unlinked += 1
# else:
# unchanged += 1
#
# if failed:
# msg = 'installed: %d, unchanged: %d, error: ' + msg
# msg = msg % (unlinked, unchanged)
# elif unlinked:
# changed = True
# msg = 'unlinked: %d, unchanged: %d' % (unlinked, unchanged)
# else:
# msg = 'unlinked: %d, unchanged: %d' % (unlinked, unchanged)
#
# return (failed, changed, msg)
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict( argument_spec=dict(
name = dict(aliases=["pkg"], required=True), name=dict(aliases=["pkg"], required=False),
state = dict(default="present", choices=["present", "installed", "absent", "removed"]), path=dict(required=False),
update_homebrew = dict(default="no", aliases=["update-brew"], type='bool'), state=dict(
install_options = dict(default=None, aliases=["options"], type='list') default="present",
choices=[
"present", "installed",
"latest", "upgraded", "head",
"linked", "unlinked",
"absent", "removed", "uninstalled",
],
), ),
supports_check_mode=True update_homebrew=dict(
default="no",
aliases=["update-brew"],
type='bool',
),
),
supports_check_mode=True,
) )
brew_path = module.get_bin_path('brew', True, ['/usr/local/bin'])
p = module.params p = module.params
if p["update_homebrew"]: if p['name']:
update_homebrew(module, brew_path) packages = p['name'].split(',')
else:
packages = None
pkgs = p["name"].split(",") path = p['path']
if path:
path = path.split(':')
else:
path = ['/usr/local/bin']
if p["state"] in ["present", "installed"]: state = p['state']
opt = generate_options_string(p["install_options"]) if state in ('present', 'installed', 'head'):
install_packages(module, brew_path, pkgs, opt) state = 'installed'
if state in ('latest', 'upgraded'):
state = 'upgraded'
if state in ('absent', 'removed', 'uninstalled'):
state = 'absent'
elif p["state"] in ["absent", "removed"]: update_homebrew = p['update_homebrew']
remove_packages(module, brew_path, pkgs)
# import module snippets brew = Homebrew(module=module, path=path, packages=packages,
from ansible.module_utils.basic import * state=state, update_homebrew=update_homebrew)
(failed, changed, message) = brew.run()
if failed:
module.fail_json(msg=message)
else:
module.exit_json(changed=changed, msg=message)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main() main()