diff --git a/.github/ISSUE_TEMPLATE/setup.md b/.github/ISSUE_TEMPLATE/setup.md index 59b757d..83664b1 100644 --- a/.github/ISSUE_TEMPLATE/setup.md +++ b/.github/ISSUE_TEMPLATE/setup.md @@ -30,11 +30,15 @@ ansible-playbook main.yml --ask-pass --ask-become-pass ``` -- [ ] Set Keyboard preferences - - [ ] Modifiers - - [ ] Function keys +- [ ] macOS preferences + - [ ] Displays + - [ ] Enable Night Shift + - [ ] Keyboard + - [ ] Modifiers + - [ ] Function keys - [ ] Setup programs - [ ] 1Password - [ ] Firefox + - [ ] Fantastical - [ ] Arq diff --git a/README.md b/README.md index 2dada3d..6135ba6 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,11 @@ provision new machines. # Usage -First, some steps need to be performed on the remote machine that I couldn't -figure out how to automate: +Use a [tracking issue][tracking-issue] as a checklist for local provisioning. -- [ ] Enable Remote Login in System Preferences -> Sharing. -- [ ] Install [Xcode][xcode]: `open 'https://itunes.apple.com/us/app/xcode/id497799835?mt=12'` -- [ ] Accept the Xcode license: `sudo xcodebuild -license` -- [ ] Open Xcode -- [ ] Install the command line developer tools: `xcode-select --install`. (It looks like the - Homebrew installer [_should_][xcode-select-cli] be able to handle this, but I haven't been able to - get it to work headless.) +[tracking-issue]: https://github.com/kejadlen/dotfiles/issues/new?template=setup.md -[xcode]: https://itunes.apple.com/us/app/xcode/id497799835?mt=12 -[xcode-select-cli]: https://github.com/Homebrew/install/blob/master/install#L207-L216 +## Remote provisioning On the control machine: @@ -50,34 +42,6 @@ manually run post-provisioning: # Symlink ~/.dotfiles to Dropbox rm -rf ~/.dotfiles ln -s ~/Dropbox/dotfiles ~/.dotfiles - -# Add private SSH keys -ruby ~/.dotfiles/scripts/setup_ssh_keys.rb - -# Install apps from the Mac App Store -open 'https://itunes.apple.com/us/app/reeder-3/id880001334?ls=1&mt=12' -open 'https://itunes.apple.com/us/app/paprika-recipe-manager/id451907568?mt=12' -``` - -## Provisioning Locally - -You can also use this to provision a machine by itself, although this won't -be able to copy over OS X application settings, for obvious reasons. - -``` shell -# Install Homebrew -ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" - -# Install ansible -brew install ansible - -# Clone dotfiles -git clone --recursive https://github.com/kejadlen/dotfiles.git ~/.dotfiles - -# Run Ansible -cd ~/.dotfiles/ansible -echo "localhost ansible_connection=local" > hosts.private -ansible-playbook main.yml --ask-pass --ask-become-pass ``` # Development diff --git a/ansible/macbook_pro.yml b/ansible/macbook_pro.yml deleted file mode 100644 index bfd8be1..0000000 --- a/ansible/macbook_pro.yml +++ /dev/null @@ -1,11 +0,0 @@ -- hosts: all - tasks: - - name: register keyboard_id - shell: ioreg -n IOHIDKeyboard -r | grep -e VendorID\" -e ProductID | ruby -e 'print ARGF.read.scan(/\d+/).join(?-)' - register: keyboard_id - - - name: s/caps lock/ctrl/ - command: defaults -currentHost write -g com.apple.keyboard.modifiermapping.{{ keyboard_id.stdout }}-0 -array-add 'HIDKeyboardModifierMappingDst2HIDKeyboardModifierMappingSrc0' - - - name: install logitech options - homebrew_cask: name=logitech-options state=installed diff --git a/ansible/main.yml b/ansible/main.yml index 24ef6d3..280fc6d 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -1,4 +1,3 @@ ---- - hosts: all tasks: - group_by: key=os_{{ ansible_distribution }} diff --git a/ansible/playbooks/homebrew.yml b/ansible/playbooks/homebrew.yml index 3e6d25d..f79b59f 100644 --- a/ansible/playbooks/homebrew.yml +++ b/ansible/playbooks/homebrew.yml @@ -14,7 +14,7 @@ - homebrew/cask-versions - homebrew/core - homebrew/services - - kejadlen/personal + - kejadlen/tap - seattle-beach/tap - name: install formulae @@ -87,6 +87,7 @@ with_items: - 1028916583 # iPulse - 451907568 # Paprika Recipe Manager + - 497799835 # Xcode - 880001334 # Reeder - 904280696 # Things3 - 585829637 # Todoist diff --git a/ansible/playbooks/rust.yml b/ansible/playbooks/rust.yml new file mode 100644 index 0000000..fcc5ad2 --- /dev/null +++ b/ansible/playbooks/rust.yml @@ -0,0 +1,5 @@ +# - hosts: os_MacOSX +- hosts: all + tasks: + - name: install rustup + shell: curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path --default-toolchain nightly diff --git a/ansible/roles/osx/library/my_defaults.py b/ansible/roles/osx/library/my_defaults.py deleted file mode 100644 index 9ff179c..0000000 --- a/ansible/roles/osx/library/my_defaults.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2014, GeekChimp - Franck Nijhof -# -# 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: osx_defaults -author: Franck Nijhof (@frenck) -short_description: osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible -description: - - osx_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: "2.0" -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: false - 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 = ''' -- osx_defaults: domain=com.apple.Safari key=IncludeInternalDebugMenu type=bool value=true state=present -- osx_defaults: domain=NSGlobalDomain key=AppleMeasurementUnits type=string value=Centimeters state=present -- osx_defaults: key=AppleMeasurementUnits type=string value=Centimeters -- osx_defaults: - key: AppleLanguages - type: array - value: ["en", "nl"] -- osx_defaults: domain=com.geekchimp.macable key=ExampleKeyToRemove state=absent -''' - -from datetime import datetime - -# exceptions --------------------------------------------------------------- {{{ -class OSXDefaultsException(Exception): - pass -# /exceptions -------------------------------------------------------------- }}} - -# class MacDefaults -------------------------------------------------------- {{{ -class OSXDefaults(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 OSXDefaultsException("Unable to locate defaults executable.") - - # When state is present, we require a parameter - if self.state == "present" and self.value is None: - raise OSXDefaultsException("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 isinstance(value, basestring): - value = value.lower() - if value in [True, 1, "true", "1", "yes"]: - return True - elif value in [False, 0, "false", "0", "no"]: - return False - raise OSXDefaultsException("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 OSXDefaultsException( - "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 OSXDefaultsException("Invalid integer value: {0}".format(repr(value))) - return int(value) - elif type == "float": - try: - value = float(value) - except ValueError: - raise OSXDefaultsException("Invalid float value: {0}".format(repr(value))) - return value - elif type == "array": - if not isinstance(value, list): - raise OSXDefaultsException("Invalid value. Expected value to be an array") - return value - - raise OSXDefaultsException('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 OSXDefaultsException("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 OSXDefaultsException("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: - if self.value: - value = "TRUE" - else: - value = "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 OSXDefaultsException('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 OSXDefaultsException("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 OSXDefaultsException("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, - type='bool', - ), - 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 = OSXDefaults(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 OSXDefaultsException, e: - module.fail_json(msg=e.message) - -# /main ------------------------------------------------------------------- }}} - -from ansible.module_utils.basic import * -main()