diff --git a/.gitignore b/.gitignore index 17de6c82..de71f3ba 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ kitchen-tests/vendor # Visual Studio Code files .vscode +*/**/*.code-workspace # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm .idea @@ -101,3 +102,4 @@ Vagrantfile .rubocop.yml .vagrant data_bags +*.code-workspace diff --git a/README.md b/README.md index 208cc4df..229fcd91 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,31 @@ -# macOS Cookbook +macOS Cookbook +============== -This cookbook provides: -- Resources for configuring and provisioning macOS. -- Recipes that implement common use-cases of the macOS cookbook's recources. +The macOS cookbook is a Chef library cookbook that provides resources for configuring +and provisioning macOS. Additionally, it provides recipes that implement common +use-cases of the macOS cookbook's recources. -## Platforms +||| +|-|-| +| macOS Sierra 10.12 | ![build-status-1012](https://office.visualstudio.com/_apis/public/build/definitions/59d72877-1cea-4eb6-9d06-66716573631a/2143/badge) | +| macOS High Sierra 10.13 | ![build-status-1013](https://office.visualstudio.com/_apis/public/build/definitions/59d72877-1cea-4eb6-9d06-66716573631a/2140/badge) | +||| -- macOS +Requirements +------------ -## Chef +- Only tested on Chef 13 +- Surprisingly, this cookbook is only compatible with macOS -- Chef 13+ +Supported OS Versions +--------------------- -## Attributes +- OS X El Capitan 10.11 +- macOS Sierra 10.12 +- macOS High Sierra 10.13 + +Attributes +---------- ### Admin User and Password @@ -22,33 +35,89 @@ node['macos']['admin_password'] = 'vagrant' ``` Each of these attributes defaults to vagrant since our resources are developed -with the Vagrant paradigm. In other words, the use and password declared here -should be an admin user. +with the Vagrant paradigm. In other words, the user and password declared here +should be an admin user with passwordless super-user rights. + +Recipes +------- + +### Disable Software Updates + +Disables automatic checking and downloading of software updates. + +**Usage:** `include_recipe macos::disable_software_updates` + +No attributes used in this recipe. + +### Keep Awake + +Prevent macOS from falling asleep, disable the screensaver, and +several other settings to always keep macOS on. Uses the `plistbuddy` and `pmset` +resources. + +**Usage:** `include_recipe macos::keep_awake` + +| Attribute used | Default value | +|---------------------------------------|-------------------------| +| `node['macos']['network_time_server']`| `'time.windows.com'` | +| `node['macos']['time_zone']` | `'America/Los_Angeles'` | ### Mono -_TODO_ +Installs [Mono](http://www.mono-project.com/docs/about-mono/). Requires package +name, version number, and checksum in order to override. ---- +**Usage:** `include_recipe macos::mono` -## Resources +| Attribute used | Default value | +|-------------------------------------|----------------------------------------| +| `node['macos']['mono']['package']` | `'MonoFramework-MDK-4.4.2.11.macos10.xamarin.universal.pkg'` | +| `node['macos']['mono']['version']` | `'4.4.2'` | +| `node['macos']['mono']['checksum']` | `'d8bfbee7ae4d0d1facaf0ddfb70c0de4b1a3d94bb1b4c38e8fa4884539f54e23'` | -- `ard` -- `name` -- `defaults` -- `pmset` -- `systemsetup` -- `xcode` +### Xcode -Checkout the [Wiki](https://github.com/Microsoft/macos-cookbook/wiki) for details about the macOS Cookbook resources. +Installs Xcode 9.1 and simulators for iOS 10 and iOS 11. Check out +the documentation for the Xcode resource if you need more flexibility. ---- +:large_orange_diamond: Requires an `apple_id` data bag item. -## Recipes +**Usage:** `include_recipe macos::xcode` -- `disable_software_updates` -- `keep_awake` -- `mono` -- `configurator` +| Attribute Used | Default value | +|---------------------------------------------------------------|---------------| +| `node['macos']['xcode']['version']` | `'9.1'` | +| `node['macos']['xcode']['simulator']['major_version']` | `%w(11 10)` | + +### Apple Configurator 2 + +Installs Apple Configurator 2 using `mas` and links `cfgutil` to +`/usr/local/bin`. + +:large_orange_diamond: Requires an `apple_id` data bag item. + +**Usage:** `include_recipe macos::configurator` + +**Attributes**: No attributes used in this recipe. + +#### Data Bags + +Both the `macos::xcode` and `macos::configurator` recipes require a data bag +item named `apple_id` containing valid Apple ID credentials. For example: + +**Example:** + +```json +{ + "id": "apple_id", + "apple_id": "farva@spurbury.gov", + "password": "0k@yN0cR34m" +} +``` +Resources +--------- +- [ARD (Apple Remote Desktop)](./documentation/resource_ard.md) +- [Plist](./documentation/resource_plist.md) +- [Xcode](./documentation/resource_xcode.md) diff --git a/attributes/default.rb b/attributes/default.rb index e95ab281..fbaa5282 100644 --- a/attributes/default.rb +++ b/attributes/default.rb @@ -5,5 +5,8 @@ default['macos']['mono']['version'] = '4.4.2' default['macos']['mono']['checksum'] = 'd8bfbee7ae4d0d1facaf0ddfb70c0de4b1a3d94bb1b4c38e8fa4884539f54e23' -default['macos']['xcode']['version'] = '9.0' +default['macos']['xcode']['version'] = '9.1' default['macos']['xcode']['simulator']['major_version'] = %w(11 10) + +default['macos']['network_time_server'] = 'time.windows.com' +default['macos']['time_zone'] = 'America/Los_Angeles' diff --git a/documentation/resource_ard.md b/documentation/resource_ard.md new file mode 100644 index 00000000..0f92074a --- /dev/null +++ b/documentation/resource_ard.md @@ -0,0 +1,133 @@ +ard +=== + +Use the **ard** resource to manage Remote Desktop settings and preferences. +Under the hood, an **ard** resource executes the `kickstart` command, located +in ARDAgent.app (one of the CoreServices of macOS). It has some basic actions, +which pertain to the simple `kickstart` subcommands. It also has the more +complicated `:configure` action, which requires some familiarity with +`kickstart`. + +[Learn more about the `kickstart` command](https://support.apple.com/en-us/HT201710). + +Syntax +------ + +A **ard** resource block declares a basic description of the command configuration +and a set of properties depending on the actions executed. For example: + +```ruby +ard 'activate and configure ard' do + action [:activate, :configure] +end +``` + +where + +- `:activate` activates the ARD agent +- `:configure` configures the agent using the `kickstart` defaut commandline arguments. + +The full syntax for all of the properties that are available to the **ard** +resource is: + +```ruby +ard 'description' do + install_package String + uninstall_options Array, # defaults to ['-files', '-settings', '-prefs'] if not specified + restart_options Array, # defaults to ['-agent', '-console', '-menu'] if not specified + users Array + privs Array, # defaults to ['-all'] if not specified + access String, # defaults to '-on' if not specified + allow_access_for String, # defaults to '-allUsers' if not specified + computerinfo Array + clientopts Array + action Symbol # defaults to [:activate, :configure] if not specified +end +``` + +**Note:** Not all properties are compatible with each action. + +Actions +------- + +This resource has the following actions: + +`:activate` + +      Activate the remote desktop agent. + +`:deactivate` + +      Deactivate the remote desktop agent. + +`:uninstall` + +      Uninstall a package from another remotely +managed mac. + +`:stop` + +      Stop the agent. + +`:restart` + +      Restart the remote desktop agent. + +`:configure` + +      Configure the setup of the remote desktop +agent using the default options. If you were to configure the default options, +your settings would look like this in the GUI: + +Properties +---------- + +`install_package` + +      **Ruby Type:** `String` + +`uninstall_options` + +      **Ruby type:** `Array` + +      default options: `['-files', '-settings', '-prefs']` + +`restart_options` + +      **Ruby type:** `Array` + +      default options: `['-agent', '-console', '-menu']` + +`users` + +      **Ruby type:** `Array` + +`privs` + +      **Ruby type:** `Array` + +      default: `['-all']` + +`access` + +      **Ruby type:** `String` + +      default: `'-on'` + +`allow_access_for` + +      **Ruby type:** `String` + +      default: `'-allUsers'` + +`computerinfo` + +      **Ruby type:** `Array` + +`clientopts` + +      **Ruby type:** `Array` + +`action` + +      **Ruby type:** `Symbol` diff --git a/documentation/resource_plist.md b/documentation/resource_plist.md new file mode 100644 index 00000000..cdceb3f8 --- /dev/null +++ b/documentation/resource_plist.md @@ -0,0 +1,64 @@ +plist +===== + +Use the **plist** resource to manage property list files (plists) and their content. +A **plist** resource instance represents the state of a single key-value pair in +the delared plist `path`. Since each plist resource instance represents only one +setting, you may end up with several plist resource calls in a given recipe. Although +this may seem like overkill, it allows us to have a fully idempotent resource with +fine granularity. + +During the `chef-client` run, the client knows to check the state of the plist +before changing any values. It also makes sure that the plist is in binary format +so that the settings can be interpreted correctly by the operating system. + +Prior knowledge of using commandline utilities such as `/usr/bin/defaults` +and `/usr/libexec/PlistBuddy` will be useful when implementing the +**plist** resource. + +[Learn more about property lists.](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/QuickStartPlist/QuickStartPlist.html#//apple_ref/doc/uid/10000048i-CH4-SW5) + +Syntax +------ + +The full syntax for all of the properties that are available to the **plist** +resource is: + +```ruby +plist 'description' do + path String # defaults to 'description' if not specified + entry String + value TrueClass, FalseClass, String, Integer, Float + action Symbol # defaults to :set if not specified +end +``` + +Actions +------- + +This resource has the following actions: + +`:set` + +      Set `entry` to `value` in `path` + +Examples +-------- + +Enabling the setting to show both visible and invisible files. + +```ruby +plist 'show hidden files' do + path '/Users/vagrant/Library/Preferences/com.apple.finder.plist' + entry 'AppleShowAllFiles' + value true +end +``` + +where + +`path` is the absolute path to the `com.apple.finder.plist` plist file + +`entry` is the representing the plist entry `'AppleShowAllFiles'` + +`value` is the entry's value to boolean type: `true` diff --git a/documentation/resource_xcode.md b/documentation/resource_xcode.md new file mode 100644 index 00000000..3d267b45 --- /dev/null +++ b/documentation/resource_xcode.md @@ -0,0 +1,69 @@ +xcode +===== + +Use the **xcode** resource to manage a single installation of Apple's Xcode IDE. +An **xcode** resource instance represents the state of a single Xcode installation +and any simulators that are declared using the `ios_simulators` property. The latest +version of iOS simulators are always installed with Xcode. + +Syntax +------ + +The full syntax for all of the properties that are available to the **xcode** +resource is: + +```ruby +xcode 'description' do + version String # defaults to 'description' if not specified + path String # defaults to '/Applications/Xcode.app' if not specified + ios_simulators Array # defaults to [10, 11] if not specified + action Symbol # defaults to [:install_xcode, :install_simulators] if not specified +end +``` + +Actions +------- + +This resource has the following actions: + +`:install_xcode` + +      Set `entry` to `value` in `path` + +`:install_simulators` + +      Along with a `version` of Xcode, +install the declared array of the major versions of `ios_simulators`. + +Examples +-------- + +### Basic usage + +The **xcode** resource in its simplest form: + +```ruby +xcode '9.2' +``` + +### Using with node attributes + +Install different versions of Xcode based on the macOS version: + +```ruby +if node['platform_version'].match?(/10\.13/) || node['platform_version'].match?(/10\.12/) + execute 'Disable Gatekeeper' do + command 'spctl --master-disable' + end + + xcode '9.2' do + ios_simulators %w(11 10) + end + +elsif node['platform_version'].match?(/10\.11/) + + xcode '8.2.1' do + ios_simulators %w(10 9) + end +end +``` diff --git a/libraries/macos_user.rb b/libraries/macos_user.rb new file mode 100644 index 00000000..7d73a120 --- /dev/null +++ b/libraries/macos_user.rb @@ -0,0 +1,36 @@ +module MacOS + module MacOSUserHelpers + def kcpassword_hash(password) + bits = magic_bits + obfuscated = [] + padded(password).each do |char| + obfuscated.push(bits[0] ^ char) + bits.rotate! + end + obfuscated.pack('C*') + end + + private + + def magic_bits + [125, 137, 82, 35, 210, 188, 221, 234, 163, 185, 31] + end + + def magic_len + magic_bits.length + end + + def padded(password) + padding = magic_len - (password.length % magic_len) + padding_size = padding > 0 ? padding : 0 + translated(password) + ([0] * padding_size) + end + + def translated(password) + password.split('').map(&:ord) + end + end +end + +Chef::Recipe.include(MacOS::MacOSUserHelpers) +Chef::Resource.include(MacOS::MacOSUserHelpers) diff --git a/libraries/plist.rb b/libraries/plist.rb new file mode 100644 index 00000000..611f2730 --- /dev/null +++ b/libraries/plist.rb @@ -0,0 +1,90 @@ +module MacOS + module PlistHelpers + def convert_to_data_type_from_string(type, value) + case type + when 'boolean' + value.to_i == 1 + when 'integer' + value.to_i + when 'float' + value.to_f + when 'string' + value + else + raise "Unknown or unsupported data type: #{type.class}" + end + end + + def convert_to_string_from_data_type(value) + data_type_cases = { Array => "array #{value}", + Integer => "integer #{value}", + TrueClass => "bool #{value}", + FalseClass => "bool #{value}", + Hash => "dict #{value}", + String => "string #{value}", + Float => "float #{value}" } + data_type_cases[value.class] + end + + def type_to_commandline_string(value) + case value + when Array + 'array' + when Integer + 'integer' + when FalseClass + 'bool' + when TrueClass + 'bool' + when Hash + 'dict' + when String + 'string' + when Float + 'float' + else + raise "Unknown or unsupported data type: #{value} of #{value.class}" + end + end + + def hardware_uuid + system_profiler_hardware_output = shell_out('system_profiler', 'SPHardwareDataType').stdout + hardware_overview = Psych.load(system_profiler_hardware_output)['Hardware']['Hardware Overview'] + hardware_overview['Hardware UUID'] + end + + def plistbuddy_command(subcommand, entry, path, value = nil) + arg = case subcommand.to_s + when 'add' + type_to_commandline_string(value) + when 'print' + '' + else + value + end + entry = "\"#{entry}\"" if entry.include?(' ') + entry_with_arg = [entry, arg].join(' ').strip + subcommand = "#{subcommand.capitalize} :#{entry_with_arg}" + [plistbuddy_executable, '-c', "\'#{subcommand}\'", path].join(' ') + end + + def system_preference(path, entry) + defaults_read_type_output = shell_out(defaults_executable, 'read-type', path, entry).stdout + defaults_read_output = shell_out(defaults_executable, 'read', path, entry).stdout + { key_type: defaults_read_type_output.split.last, key_value: defaults_read_output.strip } + end + + private + + def defaults_executable + '/usr/bin/defaults' + end + + def plistbuddy_executable + '/usr/libexec/PlistBuddy' + end + end +end + +Chef::Recipe.include(MacOS::PlistHelpers) +Chef::Resource.include(MacOS::PlistHelpers) diff --git a/libraries/xcode.rb b/libraries/xcode.rb index cdd7f8ca..1fb86794 100644 --- a/libraries/xcode.rb +++ b/libraries/xcode.rb @@ -1,56 +1,64 @@ -module Xcode - module Helper - def xcversion_command - '/opt/chef/embedded/bin/xcversion'.freeze - end +include Chef::Mixin::ShellOut - def xcode_already_installed?(semantic_version) - xcversion_output = shell_out("#{xcversion_command} installed").stdout.split - installed_xcodes = xcversion_output.values_at(*xcversion_output.each_index.select(&:even?)) - installed_xcodes.include?(semantic_version) - end +module MacOS + module Xcode + class << self + def installed?(semantic_version) + xcversion_output = shell_out(XCVersion.installed_xcodes).stdout.split + installed_xcodes = xcversion_output.values_at(*xcversion_output.each_index.select(&:even?)) + installed_xcodes.include?(semantic_version) + end - def xcversion_version(semantic_version) - split_version = semantic_version.split('.') - if split_version.length == 2 && split_version.last == '0' - split_version.first - else - semantic_version + def bundle_version_correct? + xcode_version = '/Applications/Xcode.app/Contents/version.plist CFBundleShortVersionString' + node['macos']['xcode']['version'] == shell_out("defaults read #{xcode_version}").stdout.strip end end - def requested_xcode_not_at_path - xcode_version = '/Applications/Xcode.app/Contents/version.plist CFBundleShortVersionString' - node['macos']['xcode']['version'] != shell_out("defaults read #{xcode_version}").stdout.strip - end + class Simulator + attr_reader :version - def simulator_already_installed?(semantic_version) - available_simulator_versions.include?("#{semantic_version} Simulator (installed)") - end + def initialize(major_version) + while available_list.empty? + Chef::Log.warn('iOS Simulator list not populated yet') + sleep 10 + end + if latest_semantic_version(major_version).nil? + Chef::Application.fatal!("iOS #{major_version} Simulator no longer available from Apple!") + else + @version = latest_semantic_version(major_version).join(' ') + end + end - def highest_semantic_simulator_version(major_version, simulators) - requirement = Gem::Dependency.new('iOS', "~> #{major_version}") - highest = simulators.select { |name, vers| requirement.match?(name, vers) }.max - if highest.nil? - Chef::Application.fatal!("iOS #{major_version} Simulator no longer available from Apple!") - else - highest.join(' ') + def latest_semantic_version(major_version) + requirement = Gem::Dependency.new('iOS', "~> #{major_version}") + available_list.select { |platform, version| requirement.match?(platform, version) }.max end - end - def included_simulator_major_version - version_matcher = /\d{1,2}\.\d{0,2}\.?\d{0,3}/ - sdks = shell_out!('/usr/bin/xcodebuild -showsdks').stdout - included_simulator = sdks.match(/Simulator - iOS (?#{version_matcher})/) - included_simulator[:version].split('.').first.to_i - end + def available_list + available_versions.split(/\n/).map { |version| version.split[0..1] } + end - def simulator_list - available_simulator_versions.split(/\n/).map { |version| version.split[0...2] } - end + def available_versions + shell_out!(XCVersion.list_simulators).stdout + end - def available_simulator_versions - shell_out!("#{xcversion_command} simulators").stdout + class << self + def installed?(semantic_version) + shell_out!(XCVersion.list_simulators) + .stdout.include?("#{semantic_version} Simulator (installed)") + end + + def included_major_version + version_matcher = /\d{1,2}\.\d{0,2}\.?\d{0,3}/ + sdks = shell_out!('/usr/bin/xcodebuild -showsdks').stdout + included_simulator = sdks.match(/Simulator - iOS (?#{version_matcher})/) + included_simulator[:version].split('.').first.to_i + end + end end end end + +Chef::Recipe.include(MacOS) +Chef::Resource.include(MacOS) diff --git a/libraries/xcversion.rb b/libraries/xcversion.rb new file mode 100644 index 00000000..58d9762d --- /dev/null +++ b/libraries/xcversion.rb @@ -0,0 +1,45 @@ +module MacOS + module XCVersion + class << self + def xcversion + '/opt/chef/embedded/bin/xcversion '.freeze + end + + def update + xcversion + 'update' + end + + def list_simulators + xcversion + 'simulators' + end + + def install_simulator(version) + xcversion + "simulators --install='#{version}'" + end + + def list_xcodes + xcversion + 'list' + end + + def install_xcode(version) + xcversion + "install '#{apple_pseudosemantic_version(version)}'" + end + + def installed_xcodes + xcversion + 'installed' + end + + def apple_pseudosemantic_version(semantic_version) + split_version = semantic_version.split('.') + if split_version.length == 2 && split_version.last == '0' + split_version.first + else + semantic_version + end + end + end + end +end + +Chef::Recipe.include(MacOS) +Chef::Resource.include(MacOS) diff --git a/metadata.rb b/metadata.rb index caba8a87..bde1aa31 100644 --- a/metadata.rb +++ b/metadata.rb @@ -5,7 +5,7 @@ description 'Resources for configuring and provisioning macOS' long_description 'Resources for configuring and provisioning macOS' chef_version '~> 13.0' if respond_to?(:chef_version) -version '0.9.0' +version '0.12.3' source_url 'https://github.com/Microsoft/macos-cookbook' issues_url 'https://github.com/Microsoft/macos-cookbook/issues' @@ -13,3 +13,5 @@ supports 'mac_os_x' depends 'homebrew' + +gem 'colorize' diff --git a/recipes/disable_software_updates.rb b/recipes/disable_software_updates.rb index 7d6433c0..d8711060 100644 --- a/recipes/disable_software_updates.rb +++ b/recipes/disable_software_updates.rb @@ -1,4 +1,11 @@ -defaults '/Library/Preferences/com.apple.SoftwareUpdate' do - settings 'AutomaticCheckEnabled' => false, - 'AutomaticDownload' => false +plist 'disable automatic software update downloads' do + path '/Library/Preferences/com.apple.SoftwareUpdate.plist' + entry 'AutomaticDownload' + value false +end + +plist 'disable automatic software update check' do + path '/Library/Preferences/com.apple.SoftwareUpdate.plist' + entry 'AutomaticCheckEnabled' + value false end diff --git a/recipes/keep_awake.rb b/recipes/keep_awake.rb index 162c6889..145abffe 100644 --- a/recipes/keep_awake.rb +++ b/recipes/keep_awake.rb @@ -1,41 +1,80 @@ -systemsetup 'keep awake and set time with systemsetup' do - set sleep: 0, - computersleep: 0, - displaysleep: 0, - harddisksleep: 0, - remoteappleevents: 'On', - allowpowerbuttontosleepcomputer: 'Off', - waitforstartupafterpowerfailure: 0, - restartfreeze: 'On', - networktimeserver: 'time.windows.com', - timezone: 'America/Los_Angeles' -end - -pmset 'keep awake with /usr/bin/pmset' do - settings sleep: 0, - hibernatemode: 0, - womp: 1, # wake on ethernet magic packet - hibernatefile: '/var/vm/sleepimage', - ttyskeepawake: 1 -end - -defaults '/Library/Preferences/com.apple.PowerManagement' do - settings 'DarkWakeBackgroundTasks' => false, # power nap - '"Automatic Restart On Power Loss"' => true, - '"Display Sleep Timer"' => 0, - '"Disk Sleep Timer"' => 0, - '"Wake On LAN"' => true, - '"System Sleep Timer"' => 0 -end - -ruby_block 'set power settings to autoLoginUser' do - block do - loginwindow_plist = '/Library/Preferences/com.apple.loginwindow' - auto_login_user = "defaults read #{loginwindow_plist} autoLoginUser" - node.default['macos']['power']['owner'] = shell_out!(auto_login_user).stdout.strip - end -end - -execute 'disable screensaver' do - command lazy { "sudo -u #{node['macos']['power']['owner']} defaults -currentHost write com.apple.screensaver idleTime 0" } +plist 'disable Power Nap' do + path '/Library/Preferences/com.apple.PowerManagement.plist' + entry 'DarkWakeBackgroundTasks' + value false +end + +plist 'enable automatic restart on power loss' do + path '/Library/Preferences/com.apple.PowerManagement.plist' + entry 'Automatic Restart On Power Loss' + value true +end + +plist 'set display sleep timer to zero' do + path '/Library/Preferences/com.apple.PowerManagement.plist' + entry 'Display Sleep Timer' + value 0 +end + +plist 'enable wake from sleep via network' do + path '/Library/Preferences/com.apple.PowerManagement.plist' + entry 'Wake On LAN' + value true +end + +plist 'set system sleep timer to zero' do + path '/Library/Preferences/com.apple.PowerManagement.plist' + entry 'System Sleep Timer' + value 0 +end + +plist 'disable screensaver' do + path "/Users/#{node['macos']['admin_user']}/Library/Preferences/ByHost/com.apple.screensaver.#{hardware_uuid}.plist" + entry 'idleTime' + value 0 +end + +systemsetup 'Set amount of idle time until computer sleeps to never' do + setting 'computersleep' + value 'Never' +end + +systemsetup 'set amount of idle time until display sleeps to never' do + setting 'displaysleep' + value 'Never' +end + +systemsetup 'set amount of idle time until hard disk sleeps to never' do + setting 'harddisksleep' + value 'Never' +end + +systemsetup 'set remote apple events to on' do + setting 'remoteappleevents' + value 'On' +end + +systemsetup 'disable the power button from being able to sleep the computer.' do + setting 'allowpowerbuttontosleepcomputer' + value 'Off' +end + +systemsetup 'set the number of seconds after which the computer will start up after a power failure to zero.' do + setting 'waitforstartupafterpowerfailure' + value 0 +end + +systemsetup 'set restart on freeze to on' do + setting 'restartfreeze' + value 'On' +end + +systemsetup "set network time server to #{node['macos']['network_time_server']}" do + setting 'networktimeserver' + value node['macos']['network_time_server'] +end + +systemsetup "set current time zone to #{node['macos']['time_zone']}" do + setting 'timezone' + value node['macos']['time_zone'] end diff --git a/resources/name.rb b/resources/machine_name.rb similarity index 67% rename from resources/name.rb rename to resources/machine_name.rb index 93061497..1b52d47b 100644 --- a/resources/name.rb +++ b/resources/machine_name.rb @@ -1,24 +1,24 @@ -resource_name :name +resource_name :machine_name default_action :run BASE_COMMAND = '/usr/sbin/scutil'.freeze SMB_SERVER_PLIST = '/Library/Preferences/SystemConfiguration/com.apple.smb.server'.freeze -property :name, String, name_property: true +property :machine_name, String, name_property: true # We cannot set the LocalHostName here because it does not conform to # the DNS standards outlined in RFC 1034 (section 3.5) action :run do execute BASE_COMMAND do - command "#{BASE_COMMAND} --set HostName '#{new_resource.name}'" + command "#{BASE_COMMAND} --set HostName '#{new_resource.machine_name}'" end execute BASE_COMMAND do - command "#{BASE_COMMAND} --set ComputerName '#{new_resource.name}'" + command "#{BASE_COMMAND} --set ComputerName '#{new_resource.machine_name}'" end defaults SMB_SERVER_PLIST do - settings 'NetBIOSName' => new_resource.name + settings 'NetBIOSName' => new_resource.machine_name end end diff --git a/resources/macos_user.rb b/resources/macos_user.rb new file mode 100644 index 00000000..75da963d --- /dev/null +++ b/resources/macos_user.rb @@ -0,0 +1,78 @@ +resource_name :macos_user +default_action :create + +property :username, String, name_property: true +property :password, String, default: 'password' +property :autologin, [TrueClass] +property :admin, [TrueClass] + +action_class do + def user_home + ::File.join('/Users', new_resource.username) + end + + def setup_assistant_plist + ::File.join(user_home, '/Library/Preferences/com.apple.SetupAssistant.plist') + end + + def setup_assistant_keypair_values + { 'DidSeeSiriSetup' => true, + 'DidSeeCloudSetup' => true, + 'LastSeenCloudProductVersion' => node['platform_version'] } + end + + def sysadminctl + '/usr/sbin/sysadminctl' + end + + def admin_user? + if property_is_set?(:admin) + '-admin' + else + '' + end + end +end + +action :create do + execute "add user #{new_resource.username}" do + command "#{sysadminctl} -addUser #{new_resource.username} -password #{new_resource.password} #{admin_user?}" + not_if { ::File.exist? user_home } + end + + if property_is_set?(:autologin) + setup_assistant_keypair_values.each do |key, setting| + plist "set #{setting} to #{key}" do + path setup_assistant_plist + entry key + value setting + end + end + + plist "set user \"#{new_resource.username}\" to login automatically" do + path '/Library/Preferences/com.apple.loginwindow.plist' + entry 'autoLoginUser' + value new_resource.username + end + + file '/etc/kcpassword' do + content kcpassword_hash(new_resource.password) + owner 'root' + group 'wheel' + mode '0600' + end + end +end + +action :delete do + directory user_home do + recursive true + action :delete + only_if { ::File.exist? user_home } + end + + execute "delete user: #{user}" do + command "#{sysadminctl} -deleteUser #{new_resource.username}" + only_if { "/usr/bin/dscl . -list /users | grep ^#{new_resource.username}$" } + end +end diff --git a/resources/plist.rb b/resources/plist.rb new file mode 100644 index 00000000..26764722 --- /dev/null +++ b/resources/plist.rb @@ -0,0 +1,39 @@ +resource_name :plist + +property :path, String, name_property: true +property :entry, String, desired_state: true +property :value, [TrueClass, FalseClass, String, Integer, Float], desired_state: true + +action_class do + def binary? + file_type_output = shell_out('/usr/bin/file', '--brief', '--mime-encoding', new_resource.path).stdout + file_type_output.strip == 'binary' + end +end + +load_current_value do |desired| + system_preference = system_preference(desired.path, desired.entry) + current_value_does_not_exist! if system_preference[:key_type].nil? + entry desired.entry unless system_preference[:key_type].nil? + value convert_to_data_type_from_string(system_preference[:key_type], system_preference[:key_value]) +end + +action :set do + converge_if_changed :entry do + converge_by "add \"#{new_resource.entry}\" to #{new_resource.path.split('/').last}" do + execute plistbuddy_command(:add, new_resource.entry, new_resource.path, new_resource.value) + end + end + + converge_if_changed :value do + converge_by "set \"#{new_resource.entry}\" to #{new_resource.value} at #{new_resource.path.split('/').last}" do + execute plistbuddy_command(:set, new_resource.entry, new_resource.path, new_resource.value) + end + end + + unless binary? + converge_by "convert \"#{new_resource.path.split('/').last}\" to binary" do + execute "/usr/bin/plutil -convert binary1 #{new_resource.path}" + end + end +end diff --git a/resources/systemsetup.rb b/resources/systemsetup.rb index 278644da..f6bb4202 100644 --- a/resources/systemsetup.rb +++ b/resources/systemsetup.rb @@ -1,26 +1,32 @@ +require 'colorize' + resource_name :systemsetup -default_action :run -BASE_COMMAND = '/usr/sbin/systemsetup'.freeze +property :setting, desired_state: false +property :value, desired_state: true, coerce: proc { |m| m.to_s } -property :get, Array -property :set, Hash +default_action :set -action :run do - if new_resource.get - new_resource.get.each do |flag| - execute BASE_COMMAND do - command "#{BASE_COMMAND} -get#{flag}" - live_stream true - end - end +load_current_value do |desired| + command_output = shell_out('/usr/sbin/systemsetup', "-get#{desired.setting}").stdout.split(': ').last.strip + if command_output.include?('after') + value command_output.split[1] + elsif command_output.end_with?('seconds') + value command_output.split.first + elsif command_output.start_with?('Error') + puts "\n" + warn(command_output.to_s.colorize(:red).bold) + warn(desired.setting.to_s.colorize(:red)) + value desired.value + else + value command_output.split.last end +end - if new_resource.set - new_resource.set.each do |flag, setting| - execute BASE_COMMAND do - command "#{BASE_COMMAND} -set#{flag} #{setting}" - end +action :set do + converge_if_changed do + converge_by "set #{new_resource.setting} to #{new_resource.value}" do + execute "/usr/sbin/systemsetup -set#{new_resource.setting} #{new_resource.value}" end end end diff --git a/resources/xcode.rb b/resources/xcode.rb index e7997c52..575076e9 100644 --- a/resources/xcode.rb +++ b/resources/xcode.rb @@ -1,5 +1,3 @@ -include Xcode::Helper - resource_name :xcode default_action %i(setup install_xcode install_simulators) @@ -11,9 +9,7 @@ chef_gem 'xcode-install' do options('--no-document') end -end -action :install_xcode do CREDENTIALS_DATA_BAG = data_bag_item(:credentials, :apple_id) DEVELOPER_CREDENTIALS = { @@ -23,26 +19,28 @@ execute 'update available Xcode versions' do environment DEVELOPER_CREDENTIALS - command "#{xcversion_command} update" + command XCVersion.update end +end +action :install_xcode do execute "install Xcode #{new_resource.version}" do environment DEVELOPER_CREDENTIALS - command "#{xcversion_command} install '#{xcversion_version(new_resource.version)}'" - not_if { xcode_already_installed?(new_resource.version) } + command XCVersion.install_xcode(new_resource.version) + not_if { Xcode.installed?(new_resource.version) } end end action :install_simulators do if new_resource.ios_simulators new_resource.ios_simulators.each do |major_version| - next if major_version.to_i >= included_simulator_major_version - version = highest_semantic_simulator_version(major_version, simulator_list) + next if major_version.to_i >= Xcode::Simulator.included_major_version + version = Xcode::Simulator.new(major_version).version execute "install latest iOS #{major_version} Simulator" do environment DEVELOPER_CREDENTIALS - command "#{xcversion_command} simulators --install='#{version}'" - not_if { simulator_already_installed?(version) } + command XCVersion.install_simulator(version) + not_if { Xcode::Simulator.installed?(version) } end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1dd5126b..6e059ed4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,2 +1,11 @@ require 'chefspec' require 'chefspec/berkshelf' + +require_relative '../libraries/macos_user' +require_relative '../libraries/plist' +require_relative '../libraries/xcode' + +RSpec.configure do |config| + config.platform = 'mac_os_x' + config.version = '10.12' +end diff --git a/spec/unit/libraries/macos_user_spec.rb b/spec/unit/libraries/macos_user_spec.rb new file mode 100644 index 00000000..a8d54843 --- /dev/null +++ b/spec/unit/libraries/macos_user_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +include MacOS::MacOSUserHelpers + +describe MacOS::MacOSUserHelpers, '#kcpassword_hash' do + context 'When calling the obfuscate method on a short password' do + it 'the password is obfuscated correctly' do + expect(kcpassword_hash('password')).to eq "\r\xE8!P\xA5\xD3\xAF\x8E\xA3\xB9\x1F".force_encoding('ASCII-8BIT') + end + end + + context 'When calling the obfuscate method on a long password' do + xit 'the password hash value matches the binary content of the file' do + expect(kcpassword_hash('correct-horse-battery-staple')).to eq "\x1E\xE6 Q\xB7\xDF\xA9\xC7\xCB\xD6m\x0E\xEC\x7FA\xB3\xC8\xA9\x8F\xD1\xC02\x0E\xFD3S\xBE\xD9\xAE\x9F\xC7\xD6\x1F".force_encoding('ASCII-8BIT') + end + end +end diff --git a/spec/unit/libraries/plist_spec.rb b/spec/unit/libraries/plist_spec.rb new file mode 100644 index 00000000..638357a4 --- /dev/null +++ b/spec/unit/libraries/plist_spec.rb @@ -0,0 +1,139 @@ +require 'spec_helper' + +include MacOS::PlistHelpers + +describe MacOS::PlistHelpers, '#plist_command' do + context 'Adding a value to a plist' do + it 'the bool arguments contain the data type' do + expect(plistbuddy_command(:add, 'FooEntry', 'path/to/file.plist', true)).to eq "/usr/libexec/PlistBuddy -c 'Add :FooEntry bool' path/to/file.plist" + end + + it 'the add command only adds the data type' do + expect(plistbuddy_command(:add, 'QuuxEntry', 'path/to/file.plist', 50)).to eq "/usr/libexec/PlistBuddy -c 'Add :QuuxEntry integer' path/to/file.plist" + end + + it 'the delete command is formatted properly' do + expect(plistbuddy_command(:delete, 'BarEntry', 'path/to/file.plist')).to eq "/usr/libexec/PlistBuddy -c 'Delete :BarEntry' path/to/file.plist" + end + + it 'the set command is formatted properly' do + expect(plistbuddy_command(:set, 'BazEntry', 'path/to/file.plist', false)).to eq "/usr/libexec/PlistBuddy -c 'Set :BazEntry false' path/to/file.plist" + end + + it 'the print command is formatted properly' do + expect(plistbuddy_command(:print, 'QuxEntry', 'path/to/file.plist')).to eq "/usr/libexec/PlistBuddy -c 'Print :QuxEntry' path/to/file.plist" + end + end + + context 'The value provided contains spaces' do + it 'returns the value properly formatted with double quotes' do + expect(plistbuddy_command(:print, 'Foo Bar Baz', 'path/to/file.plist')).to eq "/usr/libexec/PlistBuddy -c 'Print :\"Foo Bar Baz\"' path/to/file.plist" + end + end +end + +describe MacOS::PlistHelpers, '#convert_to_data_type_from_string' do + context 'When the type is boolean and given a 1 or 0' do + it 'returns true if entry is 1' do + expect(convert_to_data_type_from_string('boolean', '1')).to eq true + end + + it 'returns false if entry is 0' do + expect(convert_to_data_type_from_string('boolean', '0')).to eq false + end + end + + context 'When the type is integer and the value is 1' do + it 'returns the value as an integer' do + expect(convert_to_data_type_from_string('integer', '1')).to eq 1 + end + end + + context 'When the type is integer and the value is 0' do + it 'returns the value as an integer' do + expect(convert_to_data_type_from_string('integer', '0')).to eq 0 + end + end + + context 'When the type is integer and the value is 950224' do + it 'returns the correct value as an integer' do + expect(convert_to_data_type_from_string('integer', '950224')).to eq 950224 + end + end + + context 'When the type is string and the value is also a string' do + it 'returns the correct value still as a string' do + expect(convert_to_data_type_from_string('string', 'corge')).to eq 'corge' + end + end + + context 'When the type is float and the value is 3.14159265359' do + it 'returns the correct value as a float' do + expect(convert_to_data_type_from_string('float', '3.14159265359')).to eq 3.14159265359 + end + end + + context 'When the type nor the value is given' do + it 'find the value from somewhere else' do + expect { convert_to_data_type_from_string(nil, '') }.to raise_error(RuntimeError) + end + end +end + +describe MacOS::PlistHelpers, '#type_to_commandline_string' do + context 'When given a certain data type' do + it 'returns the required boolean entry type as a string' do + expect(type_to_commandline_string(true)).to eq 'bool' + end + + it 'returns the required array entry type as a string' do + expect(type_to_commandline_string(%w(foo bar))).to eq 'array' + end + + it 'returns the required dictionary entry type as a string' do + expect(type_to_commandline_string('baz' => 'qux')).to eq 'dict' + end + + it 'returns the required string entry type as a string' do + expect(type_to_commandline_string('quux')).to eq 'string' + end + + it 'returns the required integer entry type as a string' do + expect(type_to_commandline_string(1)).to eq 'integer' + end + + it 'returns the required float entry type as a string' do + expect(type_to_commandline_string(1.0)).to eq 'float' + end + end +end + +describe MacOS::PlistHelpers, '#convert_to_string_from_data_type' do + context 'When given a certain data type' do + it 'returns the required boolean entry' do + expect(convert_to_string_from_data_type(true)).to eq 'bool true' + end + + # TODO: Skip until proper plist array syntax is implemented (i.e. containers) + xit 'returns the required array entry' do + expect(convert_to_string_from_data_type(%w(foo bar))).to eq 'array foo bar' + end + + # TODO: Skip until proper plist dict syntax is implemented (i.e. containers) + xit 'returns the required dictionary entry' do + expect(convert_to_string_from_data_type('baz' => 'qux')).to eq 'dict key value' + end + + it 'returns the required string entry' do + expect(convert_to_string_from_data_type('quux')).to eq 'string quux' + end + + it 'returns the required integer entry' do + expect(convert_to_string_from_data_type(1)).to eq 'integer 1' + end + + it 'returns the required float entry' do + expect(convert_to_string_from_data_type(1.0)).to eq 'float 1.0' + end + end +end diff --git a/spec/unit/recipes/configurator.rb b/spec/unit/recipes/configurator_spec.rb similarity index 56% rename from spec/unit/recipes/configurator.rb rename to spec/unit/recipes/configurator_spec.rb index 96969ac4..90c0a081 100644 --- a/spec/unit/recipes/configurator.rb +++ b/spec/unit/recipes/configurator_spec.rb @@ -5,13 +5,13 @@ before(:each) do stub_data_bag_item('credentials', 'apple_id').and_return( apple_id: 'developer@apple.com', - password: 'apple_id_password') + password: 'apple_id_password' + ) + stub_command('which git').and_return('/usr/local/bin/git') + stub_command('/usr/local/bin/mas account').and_return('developer@apple.com') end - let(:chef_run) do - runner = ChefSpec::SoloRunner.new(platform: 'mac_os_x', version: '10.12') - runner.converge(described_recipe) - end + let(:chef_run) { ChefSpec::SoloRunner.new.converge(described_recipe) } it 'converges successfully' do expect { chef_run }.to_not raise_error diff --git a/spec/unit/recipes/default_spec.rb b/spec/unit/recipes/default_spec.rb index df3ad1c3..87160d6e 100644 --- a/spec/unit/recipes/default_spec.rb +++ b/spec/unit/recipes/default_spec.rb @@ -2,10 +2,7 @@ describe 'macos::default' do context 'When all attributes are default, on macOS 10.12' do - let(:chef_run) do - runner = ChefSpec::ServerRunner.new(platform: 'mac_os_x', version: '10.12') - runner.converge(described_recipe) - end + let(:chef_run) { ChefSpec::SoloRunner.new.converge(described_recipe) } it 'converges successfully' do expect { chef_run }.to_not raise_error diff --git a/spec/unit/recipes/disable_software_updates_spec.rb b/spec/unit/recipes/disable_software_updates_spec.rb new file mode 100644 index 00000000..5a665277 --- /dev/null +++ b/spec/unit/recipes/disable_software_updates_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe 'macos::disable_software_updates' do + context 'When all attributes are default, on macOS 10.12' do + let(:chef_run) { ChefSpec::SoloRunner.new.converge(described_recipe) } + + it 'converges successfully' do + expect { chef_run }.to_not raise_error + end + end +end diff --git a/spec/unit/recipes/keep_awake_spec.rb b/spec/unit/recipes/keep_awake_spec.rb index 4e731e2e..3f7ed0fb 100644 --- a/spec/unit/recipes/keep_awake_spec.rb +++ b/spec/unit/recipes/keep_awake_spec.rb @@ -2,10 +2,7 @@ describe 'macos::keep_awake' do context 'When all attributes are default, on macOS 10.12' do - let(:chef_run) do - runner = ChefSpec::ServerRunner.new(platform: 'mac_os_x', version: '10.12') - runner.converge(described_recipe) - end + let(:chef_run) { ChefSpec::SoloRunner.new.converge(described_recipe) } it 'converges successfully' do expect { chef_run }.to_not raise_error diff --git a/test/cookbooks/macos_test/.gitignore b/test/cookbooks/macos_test/.gitignore new file mode 100644 index 00000000..17de6c82 --- /dev/null +++ b/test/cookbooks/macos_test/.gitignore @@ -0,0 +1,103 @@ +*~ +*# +.#* +\#*# +.*.sw[a-z] +*.un~ + +# Bundler +Gemfile +Gemfile.lock +bin/* +.bundle/* + +# test kitchen +.kitchen/ +.kitchen.local.yml + +# Chef +Berksfile.lock +.zero-knife.rb +Policyfile.lock.json +.autotest +coverage +.DS_Store +pkg/* +tags +*/tags +.chef +results + +# You should check in your Gemfile.lock in applications, and not in gems +external_tests/*.lock +/Gemfile.local + +# ignore some common Bundler 'binstubs' directory names +# http://gembundler.com/man/bundle-exec.1.html +b/ +binstubs/ +.bundle +# RVM and RBENV ruby version files +.rbenv-version +.rvmrc +.ruby-version +.ruby-gemset + +# IDE files +.project + +# Documentation +_site/* +.yardoc/ +doc/ + +# Kitchen Tests Local Mode Data +kitchen-tests/nodes/* + +# Temporary files present during spec runs +spec/data/test-dir +spec/data/nodes +/config/ + +# acceptance binstubs +acceptance/bin/* + +vendor/ +acceptance/vendor +kitchen-tests/vendor + +# Visual Studio Code files +.vscode +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +.idea + +# CMake +cmake-build-debug/ + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Testing +*.box +berks-cookbooks +Vagrantfile +.rubocop.yml +.vagrant +data_bags diff --git a/test/cookbooks/macos_test/.kitchen.yml b/test/cookbooks/macos_test/.kitchen.yml new file mode 100644 index 00000000..91711293 --- /dev/null +++ b/test/cookbooks/macos_test/.kitchen.yml @@ -0,0 +1,36 @@ +--- +driver: + name: vagrant + provider: parallels + +provisioner: + name: chef_zero + always_update_cookbooks: true + +verifier: + name: inspec + sudo: true + +platforms: + - name: apex/macos-10.11.6 + - name: apex/macos-10.12.6 + - name: apex/macos-10.13 + +suites: + - name: default + run_list: + - recipe[macos::keep_awake] + - recipe[macos_test::new_users] + - recipe[macos::disable_software_updates] + - recipe[macos_test::preferences] + - recipe[macos_test::machine_name] + verifier: + inspec_tests: + - test/smoke/default + + - name: xcode + run_list: + - recipe[macos_test::xcode] + verifier: + inspec_tests: + - test/smoke/xcode diff --git a/test/cookbooks/macos_test/Berksfile b/test/cookbooks/macos_test/Berksfile new file mode 100644 index 00000000..18a2bfb2 --- /dev/null +++ b/test/cookbooks/macos_test/Berksfile @@ -0,0 +1,5 @@ +source 'https://supermarket.chef.io' + +metadata + +cookbook 'macos', path: './../../../' diff --git a/test/cookbooks/macos_test/metadata.rb b/test/cookbooks/macos_test/metadata.rb new file mode 100644 index 00000000..bc2cd37a --- /dev/null +++ b/test/cookbooks/macos_test/metadata.rb @@ -0,0 +1,4 @@ +name 'macos_test' +version '1.0.1' + +depends 'macos' diff --git a/test/cookbooks/macos_test/recipes/machine_name.rb b/test/cookbooks/macos_test/recipes/machine_name.rb new file mode 100644 index 00000000..cce79fa7 --- /dev/null +++ b/test/cookbooks/macos_test/recipes/machine_name.rb @@ -0,0 +1 @@ +machine_name "New#{node['platform_version']}_Washing_Machine" diff --git a/test/cookbooks/macos_test/recipes/new_users.rb b/test/cookbooks/macos_test/recipes/new_users.rb new file mode 100644 index 00000000..a12c4d6e --- /dev/null +++ b/test/cookbooks/macos_test/recipes/new_users.rb @@ -0,0 +1,11 @@ +macos_user 'create admin user randall and enable automatic login' do + username 'randall' + password 'correct-horse-battery-staple' + autologin true + admin true +end + +macos_user 'create non-admin user johnny' do + username 'johnny' + password 'yang-yolked-cordon-karate' +end diff --git a/test/cookbooks/macos_test/recipes/preferences.rb b/test/cookbooks/macos_test/recipes/preferences.rb new file mode 100644 index 00000000..66379db0 --- /dev/null +++ b/test/cookbooks/macos_test/recipes/preferences.rb @@ -0,0 +1,17 @@ +plist 'show hidden files' do + path '/Users/vagrant/Library/Preferences/com.apple.finder.plist' + entry 'AppleShowAllFiles' + value true +end + +plist 'put the Dock on the left side' do + path '/Users/vagrant/Library/Preferences/com.apple.dock.plist' + entry 'orientation' + value 'left' +end + +plist 'disable window animations and Get Info animations' do + path '/Users/vagrant/Library/Preferences/com.apple.dock.plist' + entry 'DisableAllAnimations' + value true +end diff --git a/test/cookbooks/macos_test/recipes/xcode.rb b/test/cookbooks/macos_test/recipes/xcode.rb new file mode 100644 index 00000000..197a4891 --- /dev/null +++ b/test/cookbooks/macos_test/recipes/xcode.rb @@ -0,0 +1,15 @@ +if node['platform_version'].match?(/10\.13/) || node['platform_version'].match?(/10\.12/) + execute 'Disable Gatekeeper' do + command 'spctl --master-disable' + end + + xcode '9.2' do + ios_simulators %w(11 10) + end + +elsif node['platform_version'].match?(/10\.11/) + + xcode '8.2.1' do + ios_simulators %w(10 9) + end +end diff --git a/test/cookbooks/macos_test/test/smoke/default/disable_software_updates_test.rb b/test/cookbooks/macos_test/test/smoke/default/disable_software_updates_test.rb new file mode 100644 index 00000000..28c88f19 --- /dev/null +++ b/test/cookbooks/macos_test/test/smoke/default/disable_software_updates_test.rb @@ -0,0 +1,27 @@ +software_update_plist = '/Library/Preferences/com.apple.SoftwareUpdate.plist' +automatic_check_enabled = 'AutomaticCheckEnabled' +automatic_download = 'AutomaticDownload' + +describe command("/usr/libexec/PlistBuddy -c 'Print :#{automatic_check_enabled}' #{software_update_plist}") do + its('stdout') { should match('false') } +end + +describe command("/usr/libexec/PlistBuddy -c 'Print :#{automatic_download}' #{software_update_plist}") do + its('stdout') { should match('false') } +end + +describe command("/usr/bin/defaults read-type #{software_update_plist} #{automatic_download}") do + its('stdout') { should match('boolean') } +end + +describe command("/usr/bin/defaults read-type #{software_update_plist} #{automatic_check_enabled}") do + its('stdout') { should match('boolean') } +end + +describe command("/usr/bin/defaults read #{software_update_plist} #{automatic_download}") do + its('stdout') { should match('0') } +end + +describe command("/usr/bin/defaults read #{software_update_plist} #{automatic_check_enabled}") do + its('stdout') { should match('0') } +end diff --git a/test/cookbooks/macos_test/test/smoke/default/keep_awake_test.rb b/test/cookbooks/macos_test/test/smoke/default/keep_awake_test.rb new file mode 100644 index 00000000..798b3cd0 --- /dev/null +++ b/test/cookbooks/macos_test/test/smoke/default/keep_awake_test.rb @@ -0,0 +1,60 @@ +control 'power' do + desc 'Display, machine and hard disk never sleep, the machine automatically restarts + after a freeze or power outage, and Power Nap' + + %w(sleep computersleep displaysleep harddisksleep).each do |setting| + describe command("systemsetup -get#{setting}") do + its('stdout') { should match('Never') } + end + end + + %w(restartfreeze remoteappleevents).each do |setting| + describe command("systemsetup -get#{setting}") do + its('stdout') { should match('On') } + end + end + + describe command('systemsetup -getwaitforstartupafterpowerfailure') do + its('stdout') { should match('0 seconds') } + end + + describe command('pmset -g') do + its('stdout') { should match(/sleep\s*0/) } + its('stdout') { should match(/hibernatemode\s*0$/) } + its('stdout') { should match(/ttyskeepawake\s*1/) } + its('stdout') { should match(%r{hibernatefile\s*\/var\/vm\/sleepimage}) } + its('stdout') { should match(/disksleep\s*0/) } + its('stdout') { should match(/displaysleep\s*0/) } + its('stdout') { should match(/ttyskeepawake\s*1/) } + end + + describe command("/usr/libexec/PlistBuddy -c 'Print :DarkWakeBackgroundTasks' '/Library/Preferences/com.apple.PowerManagement.plist'") do + its('stdout') { should match('false') } + end +end + +control 'screensaver' do + desc 'screensaver is disabled' + + def hardware_uuid + system_profiler_hardware_output = `system_profiler SPHardwareDataType` + hardware_overview = Psych.load(system_profiler_hardware_output)['Hardware']['Hardware Overview'] + hardware_overview['Hardware UUID'] + end + + describe command("/usr/libexec/PlistBuddy -c 'Print :idleTime' /Users/vagrant/Library/Preferences/ByHost/com.apple.screensaver.#{hardware_uuid}.plist"), :skip do + its('stdout') { should match(/0/) } + end + + describe command('su vagrant -c "/usr/bin/defaults -currentHost read com.apple.screensaver idleTime"') do + its('stdout') { should match(/0/) } + end + + describe command('su vagrant -c "/usr/bin/defaults -currentHost read-type com.apple.screensaver idleTime"') do + its('stdout') { should match(/integer/) } + end + + describe command("file --brief --mime /Users/vagrant/Library/Preferences/ByHost/com.apple.screensaver.#{hardware_uuid}.plist"), :skip do + its('stdout') { should match(/binary/) } + end +end diff --git a/test/cookbooks/macos_test/test/smoke/default/machine_name_test.rb b/test/cookbooks/macos_test/test/smoke/default/machine_name_test.rb new file mode 100644 index 00000000..f215c295 --- /dev/null +++ b/test/cookbooks/macos_test/test/smoke/default/machine_name_test.rb @@ -0,0 +1,16 @@ +control 'machine name' do + desc 'machine name is set to the format "New#{macos_semantic_version}_Washing_Machine"' + + macos_semantic_version = command('sw_vers -productVersion').stdout.strip + hostname_pattern = /New#{macos_semantic_version}_Washing_Machine/ + + hostname_commands = ['hostname', + 'scutil --get ComputerName', + 'scutil --get HostName'] + + hostname_commands.each do |hostname_command| + describe command(hostname_command) do + its('stdout') { should match hostname_pattern } + end + end +end diff --git a/test/cookbooks/macos_test/test/smoke/default/new_users_test.rb b/test/cookbooks/macos_test/test/smoke/default/new_users_test.rb new file mode 100644 index 00000000..4d6a0a53 --- /dev/null +++ b/test/cookbooks/macos_test/test/smoke/default/new_users_test.rb @@ -0,0 +1,17 @@ +control 'new macOS users' do + desc 'they exist with the expected characteristics' + + describe user('randall') do + it { should exist } + its('uid') { should eq 503 } + its('gid') { should eq 20 } + its('home') { should eq '/Users/randall' } + end + + describe user('johnny') do + it { should exist } + its('uid') { should eq 504 } + its('gid') { should eq 20 } + its('home') { should eq '/Users/johnny' } + end +end diff --git a/test/cookbooks/macos_test/test/smoke/default/preferences_test.rb b/test/cookbooks/macos_test/test/smoke/default/preferences_test.rb new file mode 100644 index 00000000..e82b509e --- /dev/null +++ b/test/cookbooks/macos_test/test/smoke/default/preferences_test.rb @@ -0,0 +1,15 @@ +control 'macOS user preferences' do + desc 'they are set to the correct values' + + describe command("/usr/libexec/PlistBuddy -c 'Print :orientation' /Users/vagrant/Library/Preferences/com.apple.dock.plist") do + its('stdout') { should match 'left' } + end + + describe command("/usr/libexec/PlistBuddy -c 'Print :AppleShowAllFiles' /Users/vagrant/Library/Preferences/com.apple.finder.plist") do + its('stdout') { should match 'true' } + end + + describe command("/usr/libexec/PlistBuddy -c 'Print :DisableAllAnimations' /Users/vagrant/Library/Preferences/com.apple.dock.plist") do + its('stdout') { should match 'true' } + end +end diff --git a/test/cookbooks/macos_test/test/smoke/xcode/xcode_test.rb b/test/cookbooks/macos_test/test/smoke/xcode/xcode_test.rb new file mode 100644 index 00000000..7f81d8eb --- /dev/null +++ b/test/cookbooks/macos_test/test/smoke/xcode/xcode_test.rb @@ -0,0 +1,27 @@ +control 'xcode' do + desc 'application Xcode exists and Developer mode is enabled' + + describe file('/Applications/Xcode.app') do + it { should exist } + it { should be_symlink } + end + + if os[:release].match?(/10\.13/) || os[:release].match?(/10\.12/) + describe directory('/Applications/Xcode-9.2.app') do + it { should exist } + end + + describe command('/opt/chef/embedded/bin/xcversion simulators') do + its('stdout') { should match(/iOS 10\.3\.1 Simulator \(installed\)/) } + end + + elsif os[:release].match?(/10\.11/) + describe directory('/Applications/Xcode-8.2.1.app') do + it { should exist } + end + + describe command('/opt/chef/embedded/bin/xcversion simulators') do + its('stdout') { should match(/iOS 9\.3 Simulator \(installed\)/) } + end + end +end diff --git a/test/smoke/default/xcode_test.rb b/test/smoke/default/xcode_test.rb deleted file mode 100644 index b34ada6a..00000000 --- a/test/smoke/default/xcode_test.rb +++ /dev/null @@ -1,16 +0,0 @@ -control 'xcode' do - desc 'application Xcode exists and Developer mode is enabled' - - describe file '/Applications/Xcode.app' do - it { should exist } - it { should be_symlink } - end - - describe directory '/Applications/Xcode-9.app' do - it { should exist } - end - - describe command('/usr/local/bin/xcversion simulators') do - its('stdout') { should match /iOS 10\.3\.1 Simulator \(installed\)/ } - end -end