summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2020-04-10 15:08:31 -0700
committerGitHub <noreply@github.com>2020-04-10 15:08:31 -0700
commit59779bd4a5405a26da025e2772a7878133df2baa (patch)
treeaf61b55656a91b5be5770bb89bc4ba7f1eade0b4
parentd2cfd047bd3b77f56230efef9f1bef569992800c (diff)
parente009583afea176de8a9619aca17cd162b01db4ea (diff)
downloadchef-59779bd4a5405a26da025e2772a7878133df2baa.tar.gz
Merge pull request #9642 from chef/macos-resources
Add the plist resource from the macos cookbook
-rw-r--r--lib/chef/provider/launchd.rb2
-rw-r--r--lib/chef/provider/osx_profile.rb3
-rw-r--r--lib/chef/provider/user/dscl.rb6
-rw-r--r--lib/chef/provider/user/mac.rb5
-rw-r--r--lib/chef/resource/build_essential.rb2
-rw-r--r--lib/chef/resource/plist.rb207
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--spec/unit/resource/plist_spec.rb130
8 files changed, 348 insertions, 8 deletions
diff --git a/lib/chef/provider/launchd.rb b/lib/chef/provider/launchd.rb
index 3516cadda7..fbb9307712 100644
--- a/lib/chef/provider/launchd.rb
+++ b/lib/chef/provider/launchd.rb
@@ -161,7 +161,7 @@ class Chef
def content
plist_hash = new_resource.plist_hash || gen_hash
- Plist::Emit.dump(plist_hash) unless plist_hash.nil?
+ ::Plist::Emit.dump(plist_hash) unless plist_hash.nil?
end
def gen_hash
diff --git a/lib/chef/provider/osx_profile.rb b/lib/chef/provider/osx_profile.rb
index ba63d4b02a..07d35e633c 100644
--- a/lib/chef/provider/osx_profile.rb
+++ b/lib/chef/provider/osx_profile.rb
@@ -21,6 +21,7 @@ require_relative "../provider"
require_relative "../resource"
require_relative "../resource/file"
require "uuidtools"
+require "plist"
class Chef
class Provider
@@ -232,7 +233,7 @@ class Chef
end
def read_plist(xml_file)
- Plist.parse_xml(xml_file)
+ ::Plist.parse_xml(xml_file)
end
def profile_installed?
diff --git a/lib/chef/provider/user/dscl.rb b/lib/chef/provider/user/dscl.rb
index 687fc021da..a46ecb3a62 100644
--- a/lib/chef/provider/user/dscl.rb
+++ b/lib/chef/provider/user/dscl.rb
@@ -118,7 +118,7 @@ in 'password', with the associated 'salt' and 'iterations'.")
# Calling shell_out directly since we want to give an input stream
shadow_hash_xml = convert_binary_plist_to_xml(shadow_hash_binary.string)
- shadow_hash = Plist.parse_xml(shadow_hash_xml)
+ shadow_hash = ::Plist.parse_xml(shadow_hash_xml)
if shadow_hash["SALTED-SHA512-PBKDF2"] # 10.7+ contains this, but we retain the check in case it goes away in the future
@password_shadow_conversion_algorithm = "SALTED-SHA512-PBKDF2"
@@ -541,7 +541,7 @@ in 'password', with the associated 'salt' and 'iterations'.")
begin
user_plist_file = "#{USER_PLIST_DIRECTORY}/#{new_resource.username}.plist"
user_plist_info = run_plutil("convert", "xml1", "-o", "-", user_plist_file)
- user_info = Plist.parse_xml(user_plist_info)
+ user_info = ::Plist.parse_xml(user_plist_info)
rescue Chef::Exceptions::PlistUtilCommandFailed
end
@@ -554,7 +554,7 @@ in 'password', with the associated 'salt' and 'iterations'.")
#
def save_user_info(user_info)
user_plist_file = "#{USER_PLIST_DIRECTORY}/#{new_resource.username}.plist"
- Plist::Emit.save_plist(user_info, user_plist_file)
+ ::Plist::Emit.save_plist(user_info, user_plist_file)
run_plutil("convert", "binary1", user_plist_file)
end
diff --git a/lib/chef/provider/user/mac.rb b/lib/chef/provider/user/mac.rb
index 312d2a7b47..104cf51abe 100644
--- a/lib/chef/provider/user/mac.rb
+++ b/lib/chef/provider/user/mac.rb
@@ -22,6 +22,7 @@ require_relative "../../mixin/shell_out"
require_relative "../../mixin/which"
require_relative "../user"
require_relative "../../resource/user/mac_user"
+require "plist"
class Chef
class Provider
@@ -79,7 +80,7 @@ class Chef
admin_group_xml = run_dscl("read", "/Groups/admin")
return nil unless admin_group_xml && admin_group_xml != ""
- @admin_group_plist = Plist.new(::Plist.parse_xml(admin_group_xml))
+ @admin_group_plist = ::Plist.new(::Plist.parse_xml(admin_group_xml))
end
def reload_user_plist
@@ -94,7 +95,7 @@ class Chef
return nil if user_xml.nil? || user_xml == ""
- @user_plist = Plist.new(::Plist.parse_xml(user_xml))
+ @user_plist = ::Plist.new(::Plist.parse_xml(user_xml))
return unless user_plist[:shadow_hash]
diff --git a/lib/chef/resource/build_essential.rb b/lib/chef/resource/build_essential.rb
index 5a41f3895b..ab1be3bcfd 100644
--- a/lib/chef/resource/build_essential.rb
+++ b/lib/chef/resource/build_essential.rb
@@ -161,7 +161,7 @@ class Chef
#
# @return [true, false]
def xcode_cli_installed?
- packages = Plist.parse_xml(::File.open("/Library/Receipts/InstallHistory.plist", "r"))
+ packages = ::Plist.parse_xml(::File.open("/Library/Receipts/InstallHistory.plist", "r"))
packages.select! { |package| package["displayName"].match? "Command Line Tools" }
!packages.empty?
end
diff --git a/lib/chef/resource/plist.rb b/lib/chef/resource/plist.rb
new file mode 100644
index 0000000000..d5d2ce3c6c
--- /dev/null
+++ b/lib/chef/resource/plist.rb
@@ -0,0 +1,207 @@
+#
+# Copyright:: Copyright 2017-2020, Microsoft Corporation
+# Copyright:: Copyright 2020, Chef Software 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 "../resource"
+require "plist"
+
+class Chef
+ class Resource
+
+ class PlistResource < Chef::Resource # we name this PlistResource to avoid confusion with Plist from the plist gem
+ unified_mode true
+
+ provides :plist
+
+ description "Use the plist resource to set config values in plist files on macOS systems."
+ introduced "16.0"
+
+ property :path, String, name_property: true
+ property :entry, String
+ property :value, [TrueClass, FalseClass, String, Integer, Float, Hash]
+ property :encoding, String, default: "binary"
+ property :owner, String, default: "root"
+ property :group, String, default: "wheel"
+ property :mode, [String, Integer]
+
+ PLISTBUDDY_EXECUTABLE = "/usr/libexec/PlistBuddy".freeze
+ DEFAULTS_EXECUTABLE = "/usr/bin/defaults".freeze
+ PLUTIL_EXECUTABLE = "/usr/bin/plutil".freeze
+ PLUTIL_FORMAT_MAP = { "us-ascii" => "xml1",
+ "text/xml" => "xml1",
+ "utf-8" => "xml1",
+ "binary" => "binary1" }.freeze
+
+ load_current_value do |desired|
+ current_value_does_not_exist! unless ::File.exist? desired.path
+ entry desired.entry if entry_in_plist? desired.entry, desired.path
+
+ setting = setting_from_plist desired.entry, desired.path
+ value convert_to_data_type_from_string(setting[:key_type], setting[:key_value])
+
+ file_type_cmd = shell_out "/usr/bin/file", "--brief", "--mime-encoding", "--preserve-date", desired.path
+ encoding file_type_cmd.stdout.chomp
+
+ file_owner_cmd = shell_out("/usr/bin/stat", "-f", "%Su", desired.path)
+ owner file_owner_cmd.stdout.chomp
+
+ file_group_cmd = shell_out("/usr/bin/stat", "-f", "%Sg", desired.path)
+ group file_group_cmd.stdout.chomp
+ end
+
+ action :set do
+ converge_if_changed :path do
+ converge_by "create new plist: '#{new_resource.path}'" do
+ file new_resource.path do
+ content {}.to_plist
+ owner new_resource.owner
+ group new_resource.group
+ mode new_resource.mode if property_is_set?(:mode)
+ end
+ end
+ end
+
+ plist_file_name = ::File.basename(new_resource.path)
+
+ converge_if_changed :entry do
+ converge_by "add entry \"#{new_resource.entry}\" to #{plist_file_name}" do
+ shell_out!(plistbuddy_command(:add, new_resource.entry, new_resource.path, new_resource.value))
+ end
+ end
+
+ converge_if_changed :value do
+ converge_by "#{plist_file_name}: set #{new_resource.entry} to #{new_resource.value}" do
+ shell_out!(plistbuddy_command(:set, new_resource.entry, new_resource.path, new_resource.value))
+ end
+ end
+
+ converge_if_changed :encoding do
+ converge_by "change format" do
+ unless PLUTIL_FORMAT_MAP.key?(new_resource.encoding)
+ Chef::Application.fatal!(
+ "Option encoding must be equal to one of: #{PLUTIL_FORMAT_MAP.keys}! You passed \"#{new_resource.encoding}\"."
+ )
+ end
+ shell_out!(PLUTIL_EXECUTABLE, "-convert", PLUTIL_FORMAT_MAP[new_resource.encoding], new_resource.path)
+ end
+ end
+
+ converge_if_changed :owner do
+ converge_by "update owner to #{new_resource.owner}" do
+ file new_resource.path do
+ owner new_resource.owner
+ end
+ end
+ end
+
+ converge_if_changed :group do
+ converge_by "update group to #{new_resource.group}" do
+ file new_resource.path do
+ group new_resource.group
+ end
+ end
+ end
+ end
+
+ ### Question: Should I refactor these methods into an action_class?
+ ### Answer: NO
+ ### Why: We need them in both the action and in load_current_value. If you put them in the
+ ### action class then they're only in the Provider class and are not available to load_current_value
+
+ def convert_to_data_type_from_string(type, value)
+ case type
+ when "boolean"
+ # Since we've determined this is a boolean data type, we can assume that:
+ # If the value as an int is 1, return true
+ # If the value as an int is 0 (not 1), return false
+ value.to_i == 1
+ when "integer"
+ value.to_i
+ when "float"
+ value.to_f
+ when "string"
+ value
+ when "dictionary"
+ value
+ when nil
+ ""
+ else
+ raise "Unknown or unsupported data type: #{type.class}"
+ end
+ 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 entry_in_plist?(entry, path)
+ print_entry = plistbuddy_command :print, entry, path
+ cmd = shell_out print_entry
+ cmd.exitstatus == 0
+ end
+
+ def plistbuddy_command(subcommand, entry, path, value = nil)
+ sep = " "
+ arg = case subcommand.to_s
+ when "add"
+ type_to_commandline_string(value)
+ when "set"
+ if value.is_a?(Hash)
+ sep = ":"
+ value.map { |k, v| "#{k} #{v}" }
+ else
+ value
+ end
+ else
+ ""
+ end
+ entry_with_arg = ["\"#{entry}\"", arg].join(sep).strip
+ subcommand = "#{subcommand.capitalize} :#{entry_with_arg}"
+ [PLISTBUDDY_EXECUTABLE, "-c", "\'#{subcommand}\'", "\"#{path}\""].join(" ")
+ end
+
+ def setting_from_plist(entry, path)
+ defaults_read_type_output = shell_out(DEFAULTS_EXECUTABLE, "read-type", path, entry).stdout
+ data_type = defaults_read_type_output.split.last
+
+ if value.class == Hash
+ plutil_output = shell_out(PLUTIL_EXECUTABLE, "-extract", entry, "xml1", "-o", "-", path).stdout.chomp
+ { key_type: data_type, key_value: ::Plist.parse_xml(plutil_output) }
+ else
+ defaults_read_output = shell_out(DEFAULTS_EXECUTABLE, "read", path, entry).stdout
+ { key_type: data_type, key_value: defaults_read_output.strip }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index 0d1ffeb273..c7191ef69f 100644
--- a/lib/chef/resources.rb
+++ b/lib/chef/resources.rb
@@ -87,6 +87,7 @@ require_relative "resource/package"
require_relative "resource/pacman_package"
require_relative "resource/paludis_package"
require_relative "resource/perl"
+require_relative "resource/plist"
require_relative "resource/portage_package"
require_relative "resource/powershell_package_source"
require_relative "resource/powershell_script"
diff --git a/spec/unit/resource/plist_spec.rb b/spec/unit/resource/plist_spec.rb
new file mode 100644
index 0000000000..ab5b35b6c3
--- /dev/null
+++ b/spec/unit/resource/plist_spec.rb
@@ -0,0 +1,130 @@
+#
+# Author:: Tim Smith (<tsmith@chef.io>)
+# Copyright:: 2020, Chef Software Inc.
+# Copyright:: 2017-2020, Microsoft Corporation
+# 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 "spec_helper"
+
+describe Chef::Resource::PlistResource do
+ let(:resource) { Chef::Resource::PlistResource.new("fakey_fakerton") }
+
+ it "sets the default action as :set" do
+ expect(resource.action).to eql([:set])
+ end
+
+ it "path is the name property" do
+ expect(resource.path).to eql("fakey_fakerton")
+ end
+
+ describe "#plistbuddy_command" do
+ it "the bool arguments contain the data type" do
+ expect(resource.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(resource.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(resource.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(resource.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(resource.plistbuddy_command(:print, "QuxEntry", "path/to/file.plist")).to eq "/usr/libexec/PlistBuddy -c 'Print :\"QuxEntry\"' \"path/to/file.plist\""
+ end
+
+ it "the command to set a dictionary data type is formatted properly" do
+ expect(resource.plistbuddy_command(:set, "AppleFirstWeekday", "path/to/file.plist", gregorian: 4)).to eq "/usr/libexec/PlistBuddy -c 'Set :\"AppleFirstWeekday\":gregorian 4' \"path/to/file.plist\""
+ end
+
+ it "returns the value properly formatted with double quotes when the value has spaces" do
+ expect(resource.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
+
+ it "returns the value properly formatted with double quotes when The value to be added contains spaces" do
+ expect(resource.plistbuddy_command(:add, "Foo Bar Baz", "path/to/file.plist", true)).to eq "/usr/libexec/PlistBuddy -c 'Add :\"Foo Bar Baz\" bool' \"path/to/file.plist\""
+ end
+
+ it "returns the value properly formatted with double quotes when the plist itself contains spaces" do
+ expect(resource.plistbuddy_command(:print, "Foo Bar Baz", "Library/Preferences/com.parallels.Parallels Desktop.plist")).to eq "/usr/libexec/PlistBuddy -c 'Print :\"Foo Bar Baz\"' \"Library/Preferences/com.parallels.Parallels Desktop.plist\""
+ end
+ end
+
+ describe "#convert_to_data_type_from_string" do
+ it "returns true if entry is 1 and the type is boolean" do
+ expect(resource.convert_to_data_type_from_string("boolean", "1")).to eq true
+ end
+
+ it "returns false if entry is 0 and the type is boolean" do
+ expect(resource.convert_to_data_type_from_string("boolean", "0")).to eq false
+ end
+
+ it "returns the value as an integer when the type is integer and the value is 1" do
+ expect(resource.convert_to_data_type_from_string("integer", "1")).to eq 1
+ end
+
+ it "returns the value as an integer when the type is integer and the value is 0" do
+ expect(resource.convert_to_data_type_from_string("integer", "0")).to eq 0
+ end
+
+ it "returns the correct value as an integer when the type is integer and the value is 950224" do
+ expect(resource.convert_to_data_type_from_string("integer", "950224")).to eq 950224
+ end
+
+ it "returns the correct value still as a string when the type is string and the value is also a string" do
+ expect(resource.convert_to_data_type_from_string("string", "corge")).to eq "corge"
+ end
+
+ it "returns the correct value as a float when the type is float and the value is 3.14159265359" do
+ expect(resource.convert_to_data_type_from_string("float", "3.14159265359")).to eq 3.14159265359
+ end
+
+ it "returns an empty string when the type nor the value is given" do
+ expect(resource.convert_to_data_type_from_string(nil, "")).to eq ""
+ end
+ end
+
+ describe "#type_to_commandline_string" do
+ it "returns the required boolean entry type as a string" do
+ expect(resource.type_to_commandline_string(true)).to eq "bool"
+ end
+
+ it "returns the required array entry type as a string" do
+ expect(resource.type_to_commandline_string(%w{foo bar})).to eq "array"
+ end
+
+ it "returns the required dictionary entry type as a string" do
+ expect(resource.type_to_commandline_string("baz" => "qux")).to eq "dict"
+ end
+
+ it "returns the required string entry type as a string" do
+ expect(resource.type_to_commandline_string("quux")).to eq "string"
+ end
+
+ it "returns the required integer entry type as a string" do
+ expect(resource.type_to_commandline_string(1)).to eq "integer"
+ end
+
+ it "returns the required float entry type as a string" do
+ expect(resource.type_to_commandline_string(1.0)).to eq "float"
+ end
+ end
+end