1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
|
#
# Copyright:: Copyright 2017-2020, Microsoft Corporation
# Copyright:: Copyright (c) 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"
autoload :Plist, "plist"
class Chef
class Resource
class PlistResource < Chef::Resource # we name this PlistResource to avoid confusion with Plist from the plist gem
provides :plist
description "Use the **plist** resource to set config values in plist files on macOS systems."
introduced "16.0"
examples <<~DOC
**Show hidden files in finder**:
```ruby
plist 'show hidden files' do
path '/Users/vagrant/Library/Preferences/com.apple.finder.plist'
entry 'AppleShowAllFiles'
value true
end
```
DOC
property :path, String, name_property: true,
description: "The path on disk to the plist file."
property :entry, String
property :value, [TrueClass, FalseClass, String, Integer, Float, Hash]
property :encoding, String, default: "binary"
property :owner, String, default: "root",
description: "The owner of the plist file."
property :group, String, default: "wheel",
description: "The group of the plist file."
property :mode, [String, Integer],
description: "The file mode of the plist file. Ex: '644'"
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 |new_resource|
current_value_does_not_exist! unless ::File.exist? new_resource.path
entry new_resource.entry if entry_in_plist? new_resource.entry, new_resource.path
setting = setting_from_plist new_resource.entry, new_resource.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", new_resource.path
encoding file_type_cmd.stdout.chomp
file_owner_cmd = shell_out("/usr/bin/stat", "-f", "%Su", new_resource.path)
owner file_owner_cmd.stdout.chomp
file_group_cmd = shell_out("/usr/bin/stat", "-f", "%Sg", new_resource.path)
group file_group_cmd.stdout.chomp
end
action :set, description: "Set a value in a plist file." 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", "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, 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"
if value.is_a?(Hash)
sep = ":"
value.map { |k, v| "#{k} #{type_to_commandline_string(v)}" }
else
type_to_commandline_string(value)
end
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
|