mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2024-09-14 20:13:21 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			513 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			513 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/python
 | 
						|
# -*- coding: utf-8 -*-
 | 
						|
 | 
						|
# (c) 2013, Daniel Jaouen <dcj24@cornell.edu>
 | 
						|
#
 | 
						|
# 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: homebrew_cask
 | 
						|
author: Daniel Jaouen
 | 
						|
short_description: Install/uninstall homebrew casks.
 | 
						|
description:
 | 
						|
    - Manages Homebrew casks.
 | 
						|
version_added: "1.6"
 | 
						|
options:
 | 
						|
    name:
 | 
						|
        description:
 | 
						|
            - name of cask to install/remove
 | 
						|
        required: true
 | 
						|
    state:
 | 
						|
        description:
 | 
						|
            - state of the cask
 | 
						|
        choices: [ 'installed', 'uninstalled' ]
 | 
						|
        required: false
 | 
						|
        default: present
 | 
						|
'''
 | 
						|
EXAMPLES = '''
 | 
						|
- homebrew_cask: name=alfred state=present
 | 
						|
- homebrew_cask: name=alfred state=absent
 | 
						|
'''
 | 
						|
 | 
						|
import os.path
 | 
						|
import re
 | 
						|
 | 
						|
 | 
						|
# exceptions -------------------------------------------------------------- {{{
 | 
						|
class HomebrewCaskException(Exception):
 | 
						|
    pass
 | 
						|
# /exceptions ------------------------------------------------------------- }}}
 | 
						|
 | 
						|
 | 
						|
# utils ------------------------------------------------------------------- {{{
 | 
						|
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 HomebrewCask(object):
 | 
						|
    '''A class to manage Homebrew casks.'''
 | 
						|
 | 
						|
    # 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_CASK_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_CASK_REGEX        = _create_regex_group(VALID_CASK_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 (
 | 
						|
            isinstance(brew_path, basestring)
 | 
						|
            and not cls.INVALID_BREW_PATH_REGEX.search(brew_path)
 | 
						|
        )
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def valid_cask(cls, cask):
 | 
						|
        '''A valid cask is either None or alphanumeric + backslashes.'''
 | 
						|
 | 
						|
        if cask is None:
 | 
						|
            return True
 | 
						|
 | 
						|
        return (
 | 
						|
            isinstance(cask, basestring)
 | 
						|
            and not cls.INVALID_CASK_REGEX.search(cask)
 | 
						|
        )
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def valid_state(cls, state):
 | 
						|
        '''
 | 
						|
        A valid state is one of:
 | 
						|
            - installed
 | 
						|
            - absent
 | 
						|
        '''
 | 
						|
 | 
						|
        if state is None:
 | 
						|
            return True
 | 
						|
        else:
 | 
						|
            return (
 | 
						|
                isinstance(state, basestring)
 | 
						|
                and state.lower() in (
 | 
						|
                    'installed',
 | 
						|
                    '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 HomebrewCaskException(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 HomebrewCaskException(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 HomebrewCaskException(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_cask(self):
 | 
						|
        return self._current_cask
 | 
						|
 | 
						|
    @current_cask.setter
 | 
						|
    def current_cask(self, cask):
 | 
						|
        if not self.valid_cask(cask):
 | 
						|
            self._current_cask = None
 | 
						|
            self.failed = True
 | 
						|
            self.message = 'Invalid cask: {0}.'.format(cask)
 | 
						|
            raise HomebrewCaskException(self.message)
 | 
						|
 | 
						|
        else:
 | 
						|
            self._current_cask = cask
 | 
						|
            return cask
 | 
						|
    # /class properties -------------------------------------------- }}}
 | 
						|
 | 
						|
    def __init__(self, module, path=None, casks=None, state=None):
 | 
						|
        self._setup_status_vars()
 | 
						|
        self._setup_instance_vars(module=module, path=path, casks=casks,
 | 
						|
                                  state=state)
 | 
						|
 | 
						|
        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 HomebrewCaskException(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 HomebrewCaskException('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 HomebrewCaskException:
 | 
						|
            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_cask_is_installed(self):
 | 
						|
        if not self.valid_cask(self.current_cask):
 | 
						|
            self.failed = True
 | 
						|
            self.message = 'Invalid cask: {0}.'.format(self.current_cask)
 | 
						|
            raise HomebrewCaskException(self.message)
 | 
						|
 | 
						|
        cmd = [self.brew_path, 'cask', 'list']
 | 
						|
        rc, out, err = self.module.run_command(cmd)
 | 
						|
 | 
						|
        if 'nothing to list' in err:
 | 
						|
            return False
 | 
						|
        elif rc == 0:
 | 
						|
            casks = [cask_.strip() for cask_ in out.split('\n') if cask_.strip()]
 | 
						|
            return self.current_cask in casks
 | 
						|
        else:
 | 
						|
            self.failed = True
 | 
						|
            self.message = err.strip()
 | 
						|
            raise HomebrewCaskException(self.message)
 | 
						|
    # /checks ------------------------------------------------------ }}}
 | 
						|
 | 
						|
    # commands ----------------------------------------------------- {{{
 | 
						|
    def _run(self):
 | 
						|
        if self.state == 'installed':
 | 
						|
            return self._install_casks()
 | 
						|
        elif self.state == 'absent':
 | 
						|
            return self._uninstall_casks()
 | 
						|
 | 
						|
        if self.command:
 | 
						|
            return self._command()
 | 
						|
 | 
						|
    # 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 HomebrewCaskException(self.message)
 | 
						|
    # /updated ------------------------------- }}}
 | 
						|
 | 
						|
    # installed ------------------------------ {{{
 | 
						|
    def _install_current_cask(self):
 | 
						|
        if not self.valid_cask(self.current_cask):
 | 
						|
            self.failed = True
 | 
						|
            self.message = 'Invalid cask: {0}.'.format(self.current_cask)
 | 
						|
            raise HomebrewCaskException(self.message)
 | 
						|
 | 
						|
        if self._current_cask_is_installed():
 | 
						|
            self.unchanged_count += 1
 | 
						|
            self.message = 'Cask already installed: {0}'.format(
 | 
						|
                self.current_cask,
 | 
						|
            )
 | 
						|
            return True
 | 
						|
 | 
						|
        if self.module.check_mode:
 | 
						|
            self.changed = True
 | 
						|
            self.message = 'Cask would be installed: {0}'.format(
 | 
						|
                self.current_cask
 | 
						|
            )
 | 
						|
            raise HomebrewCaskException(self.message)
 | 
						|
 | 
						|
        cmd = [opt
 | 
						|
               for opt in (self.brew_path, 'cask', 'install', self.current_cask)
 | 
						|
               if opt]
 | 
						|
 | 
						|
        rc, out, err = self.module.run_command(cmd)
 | 
						|
 | 
						|
        if self._current_cask_is_installed():
 | 
						|
            self.changed_count += 1
 | 
						|
            self.changed = True
 | 
						|
            self.message = 'Cask installed: {0}'.format(self.current_cask)
 | 
						|
            return True
 | 
						|
        else:
 | 
						|
            self.failed = True
 | 
						|
            self.message = err.strip()
 | 
						|
            raise HomebrewCaskException(self.message)
 | 
						|
 | 
						|
    def _install_casks(self):
 | 
						|
        for cask in self.casks:
 | 
						|
            self.current_cask = cask
 | 
						|
            self._install_current_cask()
 | 
						|
 | 
						|
        return True
 | 
						|
    # /installed ----------------------------- }}}
 | 
						|
 | 
						|
    # uninstalled ---------------------------- {{{
 | 
						|
    def _uninstall_current_cask(self):
 | 
						|
        if not self.valid_cask(self.current_cask):
 | 
						|
            self.failed = True
 | 
						|
            self.message = 'Invalid cask: {0}.'.format(self.current_cask)
 | 
						|
            raise HomebrewCaskException(self.message)
 | 
						|
 | 
						|
        if not self._current_cask_is_installed():
 | 
						|
            self.unchanged_count += 1
 | 
						|
            self.message = 'Cask already uninstalled: {0}'.format(
 | 
						|
                self.current_cask,
 | 
						|
            )
 | 
						|
            return True
 | 
						|
 | 
						|
        if self.module.check_mode:
 | 
						|
            self.changed = True
 | 
						|
            self.message = 'Cask would be uninstalled: {0}'.format(
 | 
						|
                self.current_cask
 | 
						|
            )
 | 
						|
            raise HomebrewCaskException(self.message)
 | 
						|
 | 
						|
        cmd = [opt
 | 
						|
               for opt in (self.brew_path, 'cask', 'uninstall', self.current_cask)
 | 
						|
               if opt]
 | 
						|
 | 
						|
        rc, out, err = self.module.run_command(cmd)
 | 
						|
 | 
						|
        if not self._current_cask_is_installed():
 | 
						|
            self.changed_count += 1
 | 
						|
            self.changed = True
 | 
						|
            self.message = 'Cask uninstalled: {0}'.format(self.current_cask)
 | 
						|
            return True
 | 
						|
        else:
 | 
						|
            self.failed = True
 | 
						|
            self.message = err.strip()
 | 
						|
            raise HomebrewCaskException(self.message)
 | 
						|
 | 
						|
    def _uninstall_casks(self):
 | 
						|
        for cask in self.casks:
 | 
						|
            self.current_cask = cask
 | 
						|
            self._uninstall_current_cask()
 | 
						|
 | 
						|
        return True
 | 
						|
    # /uninstalled ----------------------------- }}}
 | 
						|
    # /commands ---------------------------------------------------- }}}
 | 
						|
 | 
						|
 | 
						|
def main():
 | 
						|
    module = AnsibleModule(
 | 
						|
        argument_spec=dict(
 | 
						|
            name=dict(aliases=["cask"], required=False),
 | 
						|
            path=dict(required=False),
 | 
						|
            state=dict(
 | 
						|
                default="present",
 | 
						|
                choices=[
 | 
						|
                    "present", "installed",
 | 
						|
                    "absent", "removed", "uninstalled",
 | 
						|
                ],
 | 
						|
            ),
 | 
						|
        ),
 | 
						|
        supports_check_mode=True,
 | 
						|
    )
 | 
						|
    p = module.params
 | 
						|
 | 
						|
    if p['name']:
 | 
						|
        casks = p['name'].split(',')
 | 
						|
    else:
 | 
						|
        casks = None
 | 
						|
 | 
						|
    path = p['path']
 | 
						|
    if path:
 | 
						|
        path = path.split(':')
 | 
						|
    else:
 | 
						|
        path = ['/usr/local/bin']
 | 
						|
 | 
						|
    state = p['state']
 | 
						|
    if state in ('present', 'installed'):
 | 
						|
        state = 'installed'
 | 
						|
    if state in ('absent', 'removed', 'uninstalled'):
 | 
						|
        state = 'absent'
 | 
						|
 | 
						|
    brew_cask = HomebrewCask(module=module, path=path, casks=casks,
 | 
						|
                             state=state)
 | 
						|
    (failed, changed, message) = brew_cask.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()
 |