summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorNate Walck <nate.walck@gmail.com>2015-10-05 16:07:02 -0700
committerNate Walck <nate.walck@gmail.com>2015-12-17 08:21:26 -0500
commite05e7b485463ea2c9346a83ad1bbc095cc95d4df (patch)
tree4deb5d987e59ec0854e6ce72c50b88d205e2e84f /lib
parent4b0531742b7e560d04a2fda7bcc24d6da9ea1c60 (diff)
downloadchef-e05e7b485463ea2c9346a83ad1bbc095cc95d4df.tar.gz
Added profile provider for OS X
Fixed resource to use let
Diffstat (limited to 'lib')
-rw-r--r--lib/chef/provider/osx_profile.rb254
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--lib/chef/resource/osx_profile.rb74
-rw-r--r--lib/chef/resources.rb1
4 files changed, 330 insertions, 0 deletions
diff --git a/lib/chef/provider/osx_profile.rb b/lib/chef/provider/osx_profile.rb
new file mode 100644
index 0000000000..ee355fd38e
--- /dev/null
+++ b/lib/chef/provider/osx_profile.rb
@@ -0,0 +1,254 @@
+#
+# Author:: Nate Walck (<nate.walck@gmail.com>)
+# Copyright:: Copyright (c) 2015 Facebook, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/log'
+require 'chef/provider'
+require 'chef/resource'
+require 'chef/resource/file'
+require 'uuidtools'
+
+class Chef
+ class Provider
+ class OsxProfile < Chef::Provider
+ include Chef::Mixin::Command
+ provides :osx_profile, os: "darwin"
+ provides :osx_config_profile, os: "darwin"
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::OsxProfile.new(@new_resource.name)
+ @current_resource.profile_name(@new_resource.profile_name)
+
+ all_profiles = get_installed_profiles
+ @new_resource.profile(
+ @new_resource.profile ||
+ @new_resource.profile_name
+ )
+
+ @new_profile_hash = get_profile_hash(@new_resource.profile)
+ @new_profile_hash['PayloadUUID'] =
+ config_uuid(@new_profile_hash) if @new_profile_hash
+
+ if @new_profile_hash
+ @new_profile_identifier = @new_profile_hash['PayloadIdentifier']
+ else
+ @new_profile_identifier = @new_resource.identifier ||
+ @new_resource.profile_name
+ end
+
+ current_profile = all_profiles['_computerlevel'].find {
+ |item| item['ProfileIdentifier'] ==
+ @new_profile_identifier
+ }
+ @current_resource.profile(current_profile)
+
+ end
+
+ def define_resource_requirements
+ requirements.assert(:remove) do |a|
+ if @new_profile_identifier
+ a.assertion {
+ !@new_profile_identifier.nil? and
+ !@new_profile_identifier.end_with?('.mobileconfig') and
+ /^\w+(?:\.\w+)+$/.match(@new_profile_identifier)
+ }
+ a.failure_message RuntimeError, "when removing using the identifier attribute, it must match the profile identifier"
+ else
+ new_profile_name = @new_resource.profile_name
+ a.assertion {
+ !new_profile_name.end_with?('.mobileconfig') and
+ /^\w+(?:\.\w+)+$/.match(new_profile_name)
+ }
+ a.failure_message RuntimeError, "When removing by resource name, it must match the profile identifier "
+ end
+ end
+
+ requirements.assert(:install) do |a|
+ if @new_profile_hash.is_a?(Hash)
+ a.assertion {
+ @new_profile_hash.include?('PayloadIdentifier')
+ }
+ a.failure_message RuntimeError, "The specified profile does not seem to be valid"
+ end
+ if @new_profile_hash.is_a?(String)
+ a.assertion {
+ @new_profile_hash.end_with?('.mobileconfig')
+ }
+ a.failure_message RuntimeError, "#{new_profile_hash}' is not a valid profile"
+ end
+ end
+ end
+
+ def action_install
+ unless profile_installed?
+ converge_by("install profile #{@new_profile_identifier}") do
+ profile_path = write_profile_to_disk
+ install_profile(profile_path)
+ get_installed_profiles(true)
+ end
+ end
+ end
+
+ def action_remove
+ # Clean up profile after removing it
+ if profile_installed?
+ converge_by("remove profile #{@new_profile_identifier}") do
+ remove_profile
+ get_installed_profiles(true)
+ end
+ end
+ end
+
+ def load_profile_hash(new_profile)
+ # file must exist in cookbook
+ if new_profile.end_with?('.mobileconfig')
+ unless cookbook_file_available?(new_profile)
+ error_string = "#{self.to_s}: '#{new_profile}' not found in cookbook"
+ raise Chef::Exceptions::FileNotFound, error_string
+ end
+ cookbook_profile = cache_cookbook_profile(new_profile)
+ return read_plist(cookbook_profile)
+ else
+ return nil
+ end
+ end
+
+ def cookbook_file_available?(cookbook_file)
+ run_context.has_cookbook_file_in_cookbook?(
+ @new_resource.cookbook_name, cookbook_file
+ )
+ end
+
+ def get_cache_dir
+ cache_dir = Chef::FileCache.create_cache_path(
+ "profiles/#{@new_resource.cookbook_name}"
+ )
+ end
+
+ def cache_cookbook_profile(cookbook_file)
+ Chef::FileCache.create_cache_path(
+ ::File.join(
+ "profiles",
+ @new_resource.cookbook_name,
+ ::File.dirname(cookbook_file)
+ )
+ )
+ remote_file = Chef::Resource::CookbookFile.new(
+ ::File.join(
+ get_cache_dir,
+ "#{cookbook_file}.remote"
+ ),
+ run_context
+ )
+ remote_file.cookbook_name = @new_resource.cookbook_name
+ remote_file.source(cookbook_file)
+ remote_file.backup(false)
+ remote_file.run_action(:create)
+ remote_file.path
+ end
+
+ def get_profile_hash(new_profile)
+ if new_profile.is_a?(Hash)
+ return new_profile
+ elsif new_profile.is_a?(String)
+ return load_profile_hash(new_profile)
+ end
+ end
+
+ def config_uuid(profile)
+ # Make a UUID of the profile contents and return as string
+ UUIDTools::UUID.sha1_create(
+ UUIDTools::UUID_DNS_NAMESPACE,
+ profile.to_s
+ ).to_s
+ end
+
+ def write_profile_to_disk
+ @new_resource.path(Chef::FileCache.create_cache_path("profiles"))
+ tempfile = Chef::FileContentManagement::Tempfile.new(@new_resource).tempfile
+ tempfile.write(@new_profile_hash.to_plist)
+ tempfile.close
+ tempfile.path
+ end
+
+ def install_profile(profile_path)
+ cmd = "profiles -I -F '#{profile_path}'"
+ Chef::Log.debug("cmd: #{cmd}")
+ shellout_results = shell_out(cmd)
+ shellout_results.exitstatus
+ end
+
+ def remove_profile
+ cmd = "profiles -R -p '#{@new_profile_identifier}'"
+ Chef::Log.debug("cmd: #{cmd}")
+ shellout_results = shell_out(cmd)
+ shellout_results.exitstatus
+ end
+
+ def get_installed_profiles(update=nil)
+ if update
+ node.run_state[:config_profiles] = query_installed_profiles
+ else
+ node.run_state[:config_profiles] ||= query_installed_profiles
+ end
+ end
+
+ def query_installed_profiles
+ # Dump all profile metadata to a tempfile
+ tempfile = generate_tempfile
+ write_installed_profiles(tempfile)
+ installed_profiles = read_plist(tempfile)
+ Chef::Log.debug("Saved profiles to run_state")
+ # Clean up the temp file as we do not need it anymore
+ ::File.unlink(tempfile)
+ installed_profiles
+ end
+
+ def generate_tempfile
+ tempfile = ::Dir::Tmpname.create("allprofiles.plist") {}
+ end
+
+ def write_installed_profiles(tempfile)
+ cmd = "profiles -P -o '#{tempfile}'"
+ shell_out!(cmd)
+ end
+
+ def read_plist(xml_file)
+ Plist::parse_xml(xml_file)
+ end
+
+ def profile_installed?
+ # Profile Identifier and UUID must match a currently installed profile
+ if @current_resource.profile.nil? or @current_resource.profile.empty?
+ false
+ else
+ if @new_resource.action.include?(:remove)
+ true
+ else
+ @current_resource.profile['ProfileUUID'] ==
+ @new_profile_hash['PayloadUUID']
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb
index 18500d4669..f5e7a0f989 100644
--- a/lib/chef/providers.rb
+++ b/lib/chef/providers.rb
@@ -41,6 +41,7 @@ require 'chef/provider/mdadm'
require 'chef/provider/mount'
require 'chef/provider/package'
require 'chef/provider/powershell_script'
+require 'chef/provider/osx_profile'
require 'chef/provider/reboot'
require 'chef/provider/remote_directory'
require 'chef/provider/remote_file'
diff --git a/lib/chef/resource/osx_profile.rb b/lib/chef/resource/osx_profile.rb
new file mode 100644
index 0000000000..26b834a9c0
--- /dev/null
+++ b/lib/chef/resource/osx_profile.rb
@@ -0,0 +1,74 @@
+#
+# Author:: Nate Walck (<nate.walck@gmail.com>)
+# Copyright:: Copyright (c) 2015 Facebook, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+
+class Chef
+ class Resource
+ class OsxProfile < Chef::Resource
+ provides :osx_profile, os: "darwin"
+ provides :osx_config_profile, os: "darwin"
+
+ identity_attr :profile_name
+
+ default_action :install
+ allowed_actions :install, :remove
+
+ def initialize(name, run_context=nil)
+ super
+ @profile_name = name
+ @profile = nil
+ @identifier = nil
+ @path = nil
+ end
+
+ def profile_name(arg=nil)
+ set_or_return(
+ :profile_name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def profile(arg=nil)
+ set_or_return(
+ :profile,
+ arg,
+ :kind_of => [ String, Hash ]
+ )
+ end
+
+ def identifier(arg=nil)
+ set_or_return(
+ :identifier,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def path(arg=nil)
+ set_or_return(
+ :path,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index 6db0fc9d8d..f699d95ace 100644
--- a/lib/chef/resources.rb
+++ b/lib/chef/resources.rb
@@ -57,6 +57,7 @@ require 'chef/resource/paludis_package'
require 'chef/resource/perl'
require 'chef/resource/portage_package'
require 'chef/resource/powershell_script'
+require 'chef/resource/osx_profile'
require 'chef/resource/python'
require 'chef/resource/reboot'
require 'chef/resource/registry_key'