summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith84@gmail.com>2020-04-08 21:38:39 -0700
committerTim Smith <tsmith84@gmail.com>2020-04-10 11:45:15 -0700
commit92ceb8d720d812ad657a1b9e2137baf5a2e838c5 (patch)
treee4193311b21843da97e593a28d05fd6b20d93687
parent85ff5628d8e7841a748f47e07e247256253cd5cb (diff)
downloadchef-92ceb8d720d812ad657a1b9e2137baf5a2e838c5.tar.gz
Add the plist resource from the macos cookbook
This is a very useful base resource for mac systems that allows us to write additional resources for managing macs. I've migrated their library into the resource itself to make it a single self-contained unit. Signed-off-by: Tim Smith <tsmith@chef.io>
-rw-r--r--lib/chef/resource/plist.rb221
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--spec/unit/resource/plist_spec.rb154
3 files changed, 376 insertions, 0 deletions
diff --git a/lib/chef/resource/plist.rb b/lib/chef/resource/plist.rb
new file mode 100644
index 0000000000..d4d6167428
--- /dev/null
+++ b/lib/chef/resource/plist.rb
@@ -0,0 +1,221 @@
+#
+# 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"
+
+class Chef
+ class Resource
+
+ class Plist < Chef::Resource
+ unified_mode true
+
+ provides :plist
+
+ 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]
+
+ 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
+ empty_plist = {}.to_plist
+ content empty_plist
+ owner new_resource.owner
+ group new_resource.group
+ mode new_resource.mode if property_is_set?(:mode)
+ end
+ end
+ end
+
+ plist_file_name = new_resource.path.split("/").last
+
+ converge_if_changed :entry do
+ converge_by "add entry \"#{new_resource.entry}\" to #{plist_file_name}" do
+ execute plistbuddy_command(:add, new_resource.entry, new_resource.path, new_resource.value) do
+ action :run
+ end
+ end
+ end
+
+ converge_if_changed :value do
+ converge_by "#{plist_file_name}: set #{new_resource.entry} to #{new_resource.value}" do
+ execute plistbuddy_command(:set, new_resource.entry, new_resource.path, new_resource.value) do
+ action :run
+ end
+ 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
+ execute [plutil_executable, "-convert", plutil_format_map[new_resource.encoding], new_resource.path] do
+ action :run
+ end
+ 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.class == 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
+
+ def plutil_format_map
+ { "us-ascii" => "xml1",
+ "text/xml" => "xml1",
+ "utf-8" => "xml1",
+ "binary" => "binary1" }
+ end
+
+ def plutil_executable
+ "/usr/bin/plutil"
+ end
+
+ def defaults_executable
+ "/usr/bin/defaults"
+ end
+
+ def plistbuddy_executable
+ "/usr/libexec/PlistBuddy"
+ end
+ end
+ end
+end \ No newline at end of file
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..b7e281b1d9
--- /dev/null
+++ b/spec/unit/resource/plist_spec.rb
@@ -0,0 +1,154 @@
+#
+# 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::Plist do
+ let(:resource) { Chef::Resource::Plist.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
+ context "Adding a value to a plist" 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
+ end
+
+ context "The value provided contains spaces" do
+ it "returns the value properly formatted with double quotes" 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
+ end
+
+ context "The value to be added contains spaces" do
+ it "returns the value properly formatted with double quotes" 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
+ end
+
+ context "The plist itself contains spaces" do
+ it "returns the value properly formatted with double quotes" 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
+ end
+
+ describe "#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(resource.convert_to_data_type_from_string("boolean", "1")).to eq true
+ end
+
+ it "returns false if entry is 0" do
+ expect(resource.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(resource.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(resource.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(resource.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(resource.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(resource.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 "returns an empty string" do
+ expect(resource.convert_to_data_type_from_string(nil, "")).to eq ""
+ end
+ end
+ end
+
+ describe "#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(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
+end