File Manager
Viewing File: osx_profile.rb
#
# Author:: Nate Walck (<nate.walck@gmail.com>)
# Copyright:: Copyright 2015-2016, 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_relative "../log"
require_relative "../provider"
require_relative "../resource"
require_relative "../resource/file"
require "uuidtools"
require "plist"
class Chef
class Provider
class OsxProfile < Chef::Provider
provides :osx_profile
provides :osx_config_profile
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
# FIXME: stop mutating the desired state
new_resource.profile(
new_resource.profile ||
new_resource.profile_name
)
@new_profile_hash = get_profile_hash(new_resource.profile)
if @new_profile_hash
@new_profile_hash["PayloadUUID"] =
config_uuid(@new_profile_hash)
end
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 = nil
if all_profiles && all_profiles.key?("_computerlevel")
current_profile = all_profiles["_computerlevel"].find do |item|
item["ProfileIdentifier"] == @new_profile_identifier
end
end
current_resource.profile(current_profile)
end
def define_resource_requirements
requirements.assert(:remove) do |a|
if @new_profile_identifier
a.assertion do
!@new_profile_identifier.nil? &&
!@new_profile_identifier.end_with?(".mobileconfig") &&
/^\w+(?:(\.| )\w+)+$/.match(@new_profile_identifier)
end
a.failure_message RuntimeError, "when removing using the identifier property, it must match the profile identifier"
else
new_profile_name = new_resource.profile_name
a.assertion do
!new_profile_name.end_with?(".mobileconfig") &&
/^\w+(?:(\.| )\w+)+$/.match(new_profile_name)
end
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 do
@new_profile_hash.include?("PayloadIdentifier")
end
a.failure_message RuntimeError, "The specified profile does not seem to be valid"
end
if @new_profile_hash.is_a?(String)
a.assertion do
@new_profile_hash.end_with?(".mobileconfig")
end
a.failure_message RuntimeError, "#{new_profile_hash}' is not a valid profile"
end
end
end
action :install do
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
action :remove do
# 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}: '#{new_profile}' not found in cookbook"
raise Chef::Exceptions::FileNotFound, error_string
end
cookbook_profile = cache_cookbook_profile(new_profile)
read_plist(cookbook_profile)
else
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)
)
)
# FIXME: should use a real cookbook file, or document what this craziness is
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)
new_profile
elsif new_profile.is_a?(String)
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
# FIXME: use a real chef file resource and stop hacking up tempfiles directly
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 = [ "/usr/bin/profiles", "-I", "-F", profile_path ]
logger.trace("cmd: #{cmd.join(" ")}")
shellout_results = shell_out(*cmd)
shellout_results.exitstatus
end
def remove_profile
cmd = [ "/usr/bin/profiles", "-R", "-p", @new_profile_identifier ]
logger.trace("cmd: #{cmd.join(" ")}")
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)
logger.trace("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)
shell_out!( "/usr/bin/profiles", "-P", "-o", tempfile )
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? || 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