#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2014, GeekChimp - Franck Nijhof # # Originally developed for Macable: https://github.com/GeekChimp/macable # # Ansible 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. # # Ansible 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 Ansible. If not, see . DOCUMENTATION = ''' --- module: mac_defaults author: Franck Nijhof short_description: mac_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible description: - mac_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible scripts. Mac OS X applications and other programs use the defaults system to record user preferences and other information that must be maintained when the applications aren't running (such as default font for new documents, or the position of an Info panel). version_added: 1.8 options: domain: description: - The domain is a domain name of the form com.companyname.appname. required: false default: NSGlobalDomain key: description: - The key of the user preference required: true type: description: - The type of value to write. required: false default: string choices: [ "array", "bool", "boolean", "date", "float", "int", "integer", "string" ] array_add: description: - Add new elements to the array for a key which has an array as its value. required: false default: string choices: [ "true", "false" ] value: description: - The value to write. Only required when state = present. required: false default: null state: description: - The state of the user defaults required: false default: present choices: [ "present", "absent" ] notes: - Apple Mac caches defaults. You may need to logout and login to apply the changes. ''' EXAMPLES = ''' - mac_defaults: domain=com.apple.Safari key=IncludeInternalDebugMenu type=bool value=true state=present - mac_defaults: domain=NSGlobalDomain key=AppleMeasurementUnits type=string value=Centimeters state=present - mac_defaults: key=AppleMeasurementUnits type=string value=Centimeters - mac_defaults: key: AppleLanguages type: array value: ["en", "nl"] - mac_defaults: domain=com.geekchimp.macable key=ExampleKeyToRemove state=absent ''' from datetime import datetime # exceptions --------------------------------------------------------------- {{{ class MacDefaultsException(Exception): pass # /exceptions -------------------------------------------------------------- }}} # class MacDefaults -------------------------------------------------------- {{{ class MacDefaults(object): """ Class to manage Mac OS user defaults """ # init ---------------------------------------------------------------- {{{ """ Initialize this module. Finds 'defaults' executable and preps the parameters """ def __init__(self, **kwargs): # Initial var for storing current defaults value self.current_value = None # Just set all given parameters for key, val in kwargs.iteritems(): setattr(self, key, val) # Try to find the defaults executable self.executable = self.module.get_bin_path( 'defaults', required=False, opt_dirs=self.path.split(':'), ) if not self.executable: raise MacDefaultsException("Unable to locate defaults executable.") # When state is present, we require a parameter if self.state == "present" and self.value is None: raise MacDefaultsException("Missing value parameter") # Ensure the value is the correct type self.value = self._convert_type(self.type, self.value) # /init --------------------------------------------------------------- }}} # tools --------------------------------------------------------------- {{{ """ Converts value to given type """ def _convert_type(self, type, value): if type == "string": return str(value) elif type in ["bool", "boolean"]: if value in [True, 1, "true", "1", "yes"]: return True elif value in [False, 0, "false", "0", "no"]: return False raise MacDefaultsException("Invalid boolean value: {0}".format(repr(value))) elif type == "date": try: return datetime.strptime(value.split("+")[0].strip(), "%Y-%m-%d %H:%M:%S") except ValueError: raise MacDefaultsException( "Invalid date value: {0}. Required format yyy-mm-dd hh:mm:ss.".format(repr(value)) ) elif type in ["int", "integer"]: if not str(value).isdigit(): raise MacDefaultsException("Invalid integer value: {0}".format(repr(value))) return int(value) elif type == "float": try: value = float(value) except ValueError: raise MacDefaultsException("Invalid float value: {0}".format(repr(value))) return value elif type == "array": if not isinstance(value, list): raise MacDefaultsException("Invalid value. Expected value to be an array") return value raise MacDefaultsException('Type is not supported: {0}'.format(type)) """ Converts array output from defaults to an list """ @staticmethod def _convert_defaults_str_to_list(value): # Split output of defaults. Every line contains a value value = value.splitlines() # Remove first and last item, those are not actual values value.pop(0) value.pop(-1) # Remove extra spaces and comma (,) at the end of values value = [re.sub(',$', '', x.strip(' ')) for x in value] return value # /tools -------------------------------------------------------------- }}} # commands ------------------------------------------------------------ {{{ """ Reads value of this domain & key from defaults """ def read(self): # First try to find out the type rc, out, err = self.module.run_command([self.executable, "read-type", self.domain, self.key]) # If RC is 1, the key does not exists if rc == 1: return None # If the RC is not 0, then terrible happened! Ooooh nooo! if rc != 0: raise MacDefaultsException("An error occurred while reading key type from defaults: " + out) # Ok, lets parse the type from output type = out.strip().replace('Type is ', '') # Now get the current value rc, out, err = self.module.run_command([self.executable, "read", self.domain, self.key]) # Strip output # out = out.strip() # An non zero RC at this point is kinda strange... if rc != 0: raise MacDefaultsException("An error occurred while reading key value from defaults: " + out) # Convert string to list when type is array if type == "array": out = self._convert_defaults_str_to_list(out) # Store the current_value self.current_value = self._convert_type(type, out) """ Writes value to this domain & key to defaults """ def write(self): # We need to convert some values so the defaults commandline understands it if type(self.value) is bool: value = "TRUE" if self.value else "FALSE" elif type(self.value) is int or type(self.value) is float: value = str(self.value) elif self.array_add and self.current_value is not None: value = list(set(self.value) - set(self.current_value)) elif isinstance(self.value, datetime): value = self.value.strftime('%Y-%m-%d %H:%M:%S') else: value = self.value # When the type is array and array_add is enabled, morph the type :) if self.type == "array" and self.array_add: self.type = "array-add" # All values should be a list, for easy passing it to the command if not isinstance(value, list): value = [value] rc, out, err = self.module.run_command([self.executable, 'write', self.domain, self.key, '-' + self.type] + value) if rc != 0: raise MacDefaultsException('An error occurred while writing value to defaults: ' + out) """ Deletes defaults key from domain """ def delete(self): rc, out, err = self.module.run_command([self.executable, 'delete', self.domain, self.key]) if rc != 0: raise MacDefaultsException("An error occurred while deleting key from defaults: " + out) # /commands ----------------------------------------------------------- }}} # run ----------------------------------------------------------------- {{{ """ Does the magic! :) """ def run(self): # Get the current value from defaults self.read() # Handle absent state if self.state == "absent": print "Absent state detected!" if self.current_value is None: return False self.delete() return True # There is a type mismatch! Given type does not match the type in defaults if self.current_value is not None and type(self.current_value) is not type(self.value): raise MacDefaultsException("Type mismatch. Type in defaults: " + type(self.current_value).__name__) # Current value matches the given value. Nothing need to be done. Arrays need extra care if self.type == "array" and self.current_value is not None and not self.array_add and \ set(self.current_value) == set(self.value): return False elif self.type == "array" and self.current_value is not None and self.array_add and \ len(list(set(self.value) - set(self.current_value))) == 0: return False elif self.current_value == self.value: return False # Change/Create/Set given key/value for domain in defaults self.write() return True # /run ---------------------------------------------------------------- }}} # /class MacDefaults ------------------------------------------------------ }}} # main -------------------------------------------------------------------- {{{ def main(): module = AnsibleModule( argument_spec=dict( domain=dict( default="NSGlobalDomain", required=False, ), key=dict( default=None, ), type=dict( default="string", required=False, choices=[ "array", "bool", "boolean", "date", "float", "int", "integer", "string", ], ), array_add=dict( default=False, required=False, choices=BOOLEANS, ), value=dict( default=None, required=False, ), state=dict( default="present", required=False, choices=[ "absent", "present" ], ), path=dict( default="/usr/bin:/usr/local/bin", required=False, ) ), supports_check_mode=True, ) domain = module.params['domain'] key = module.params['key'] type = module.params['type'] array_add = module.params['array_add'] value = module.params['value'] state = module.params['state'] path = module.params['path'] try: defaults = MacDefaults(module=module, domain=domain, key=key, type=type, array_add=array_add, value=value, state=state, path=path) changed = defaults.run() module.exit_json(changed=changed) except MacDefaultsException as e: module.fail_json(msg=e.message) # /main ------------------------------------------------------------------- }}} from ansible.module_utils.basic import * main()