diff --git a/lib/ansible/modules/packaging/os/homebrew_cask.py b/lib/ansible/modules/packaging/os/homebrew_cask.py index 0d3ebd590b..a7df020ab3 100644 --- a/lib/ansible/modules/packaging/os/homebrew_cask.py +++ b/lib/ansible/modules/packaging/os/homebrew_cask.py @@ -42,7 +42,7 @@ options: state: description: - state of the cask - choices: [ 'present', 'absent' ] + choices: [ 'present', 'absent', 'upgraded' ] required: false default: present update_homebrew: @@ -61,6 +61,24 @@ options: default: null aliases: ['options'] version_added: "2.2" + accept_external_apps: + description: + - allow external apps + required: false + default: False + version_added: "2.5.0" + upgrade_all: + description: + - upgrade all casks (mutually exclusive with `upgrade`) + required: False + default: False + version_added: "2.5.0" + upgrade: + description: + - upgrade all casks (mutually exclusive with `upgrade_all`) + required: False + default: False + version_added: "2.5.0" ''' EXAMPLES = ''' - homebrew_cask: @@ -81,10 +99,24 @@ EXAMPLES = ''' state: present install_options: 'debug,appdir=/Applications' +- homebrew_cask: + name: alfred + state: present + allow_external_apps: True + - homebrew_cask: name: alfred state: absent install_options: force + +- homebrew_cask: + upgrade_all: true + +- homebrew_cask: + name: alfred + state: upgraded + install_options: force + ''' import os.path @@ -225,7 +257,6 @@ class HomebrewCask(object): '''A valid module is an instance of AnsibleModule.''' return isinstance(module, AnsibleModule) - # /class validations ------------------------------------------- }}} # class properties --------------------------------------------- {{{ @@ -308,13 +339,16 @@ class HomebrewCask(object): # /class properties -------------------------------------------- }}} def __init__(self, module, path=path, casks=None, state=None, - update_homebrew=False, install_options=None): + update_homebrew=False, install_options=None, + accept_external_apps=False, upgrade_all=False): if not install_options: install_options = list() self._setup_status_vars() self._setup_instance_vars(module=module, path=path, casks=casks, state=state, update_homebrew=update_homebrew, - install_options=install_options,) + install_options=install_options, + accept_external_apps=accept_external_apps, + upgrade_all=upgrade_all, ) self._prep() @@ -373,6 +407,19 @@ class HomebrewCask(object): return (failed, changed, message) # checks ------------------------------------------------------- {{{ + def _current_cask_is_outdated(self): + if not self.valid_cask(self.current_cask): + return False + + rc, out, err = self.module.run_command([ + self.brew_path, + 'cask', + 'outdated', + self.current_cask, + ]) + + return out != "" + def _current_cask_is_installed(self): if not self.valid_cask(self.current_cask): self.failed = True @@ -382,33 +429,33 @@ class HomebrewCask(object): cmd = [ "{brew_path}".format(brew_path=self.brew_path), "cask", - "list" + "list", + self.current_cask ] rc, out, err = self.module.run_command(cmd) - if 'nothing to list' in err: + if re.search(r'Error: Cask .* is not installed.', 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) + return True # /checks ------------------------------------------------------ }}} # commands ----------------------------------------------------- {{{ def _run(self): - if self.update_homebrew: - self._update_homebrew() + if self.upgrade_all: + return self._upgrade_all() - if self.state == 'installed': - return self._install_casks() - elif self.state == 'absent': - return self._uninstall_casks() + if self.casks: + if self.state == 'installed': + return self._install_casks() + elif self.state == 'upgraded': + return self._upgrade_casks() + elif self.state == 'absent': + return self._uninstall_casks() - if self.command: - return self._command() + self.failed = True + self.message = "You must select a cask to install." + raise HomebrewCaskException(self.message) # updated -------------------------------- {{{ def _update_homebrew(self): @@ -436,6 +483,33 @@ class HomebrewCask(object): raise HomebrewCaskException(self.message) # /updated ------------------------------- }}} + # _upgrade_all --------------------------- {{{ + def _upgrade_all(self): + if self.module.check_mode: + self.changed = True + self.message = 'Casks would be upgraded.' + raise HomebrewCaskException(self.message) + + rc, out, err = self.module.run_command([ + self.brew_path, + 'cask', + 'upgrade', + ]) + if rc == 0: + if re.search(r'==> No Casks to upgrade', out.strip(), re.IGNORECASE): + self.message = 'Homebrew casks already upgraded.' + + else: + self.changed = True + self.message = 'Homebrew casks upgraded.' + + return True + else: + self.failed = True + self.message = err.strip() + raise HomebrewCaskException(self.message) + # /_upgrade_all -------------------------- }}} + # installed ------------------------------ {{{ def _install_current_cask(self): if not self.valid_cask(self.current_cask): @@ -470,6 +544,12 @@ class HomebrewCask(object): self.changed = True self.message = 'Cask installed: {0}'.format(self.current_cask) return True + elif self.accept_external_apps and re.search(r"Error: It seems there is already an App at", err): + self.unchanged_count += 1 + self.message = 'Cask already installed: {0}'.format( + self.current_cask, + ) + return True else: self.failed = True self.message = err.strip() @@ -483,6 +563,58 @@ class HomebrewCask(object): return True # /installed ----------------------------- }}} + # upgraded ------------------------------- {{{ + def _upgrade_current_cask(self): + command = 'upgrade' + + 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(): + command = 'install' + + if self._current_cask_is_installed() and not self._current_cask_is_outdated(): + self.message = 'Cask is already upgraded: {0}'.format( + self.current_cask, + ) + self.unchanged_count += 1 + return True + + if self.module.check_mode: + self.changed = True + self.message = 'Cask would be upgraded: {0}'.format( + self.current_cask + ) + raise HomebrewCaskException(self.message) + + opts = ( + [self.brew_path, 'cask', command] + + self.install_options + + [self.current_cask] + ) + cmd = [opt for opt in opts if opt] + rc, out, err = self.module.run_command(cmd) + + if self._current_cask_is_installed() and not self._current_cask_is_outdated(): + self.changed_count += 1 + self.changed = True + self.message = 'Cask upgraded: {0}'.format(self.current_cask) + return True + else: + self.failed = True + self.message = err.strip() + raise HomebrewCaskException(self.message) + + def _upgrade_casks(self): + for cask in self.casks: + self.current_cask = cask + self._upgrade_current_cask() + + return True + # /upgraded ------------------------------ }}} + # uninstalled ---------------------------- {{{ def _uninstall_current_cask(self): if not self.valid_cask(self.current_cask): @@ -526,7 +658,7 @@ class HomebrewCask(object): self._uninstall_current_cask() return True - # /uninstalled ----------------------------- }}} + # /uninstalled --------------------------- }}} # /commands ---------------------------------------------------- }}} @@ -547,6 +679,7 @@ def main(): default="present", choices=[ "present", "installed", + "latest", "upgraded", "absent", "removed", "uninstalled", ], ), @@ -559,7 +692,16 @@ def main(): default=None, aliases=['options'], type='list', - ) + ), + accept_external_apps=dict( + default=False, + type='bool', + ), + upgrade_all=dict( + default=False, + aliases=["upgrade"], + type='bool', + ), ), supports_check_mode=True, ) @@ -580,17 +722,25 @@ def main(): state = p['state'] if state in ('present', 'installed'): state = 'installed' + if state in ('latest', 'upgraded'): + state = 'upgraded' if state in ('absent', 'removed', 'uninstalled'): state = 'absent' update_homebrew = p['update_homebrew'] + upgrade_all = p['upgrade_all'] p['install_options'] = p['install_options'] or [] install_options = ['--{0}'.format(install_option) for install_option in p['install_options']] + accept_external_apps = p['accept_external_apps'] + brew_cask = HomebrewCask(module=module, path=path, casks=casks, state=state, update_homebrew=update_homebrew, - install_options=install_options) + install_options=install_options, + accept_external_apps=accept_external_apps, + upgrade_all=upgrade_all, + ) (failed, changed, message) = brew_cask.run() if failed: module.fail_json(msg=message)