state_mate 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +7 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +29 -0
  7. data/Rakefile +6 -0
  8. data/ansible/library/state +59 -0
  9. data/lib/state_mate.rb +366 -0
  10. data/lib/state_mate/adapters/defaults.rb +330 -0
  11. data/lib/state_mate/adapters/git_config.rb +35 -0
  12. data/lib/state_mate/adapters/json.rb +66 -0
  13. data/lib/state_mate/adapters/launchd.rb +117 -0
  14. data/lib/state_mate/adapters/nvram.rb +44 -0
  15. data/lib/state_mate/adapters/time_machine.rb +60 -0
  16. data/lib/state_mate/version.rb +3 -0
  17. data/notes/state-set-steps.md +26 -0
  18. data/spec/spec_helper.rb +44 -0
  19. data/spec/state_mate/adapters/defaults/hardware_uuid_spec.rb +15 -0
  20. data/spec/state_mate/adapters/defaults/hash_deep_write_spec.rb +33 -0
  21. data/spec/state_mate/adapters/defaults/read_defaults_spec.rb +57 -0
  22. data/spec/state_mate/adapters/defaults/read_spec.rb +32 -0
  23. data/spec/state_mate/adapters/defaults/read_type_spec.rb +27 -0
  24. data/spec/state_mate/adapters/defaults/write_spec.rb +29 -0
  25. data/spec/state_mate/adapters/git_config/read_spec.rb +36 -0
  26. data/spec/state_mate/adapters/git_config/write_spec.rb +17 -0
  27. data/spec/state_mate/adapters/json/read_spec.rb +56 -0
  28. data/spec/state_mate/adapters/json/write_spec.rb +54 -0
  29. data/spec/state_mate_spec.rb +7 -0
  30. data/state_mate.gemspec +25 -0
  31. data/test/ansible/ansible.cfg +5 -0
  32. data/test/ansible/clear +2 -0
  33. data/test/ansible/hosts +1 -0
  34. data/test/ansible/play +2 -0
  35. data/test/ansible/playbook.yml +38 -0
  36. data/test/ansible/read +2 -0
  37. metadata +167 -0
@@ -0,0 +1,330 @@
1
+ require 'shellwords'
2
+ require 'rexml/document'
3
+ require 'base64'
4
+ require 'time'
5
+ require 'pp'
6
+ require 'tempfile'
7
+
8
+ require 'CFPropertyList'
9
+
10
+ require 'nrser'
11
+ require 'nrser/exec'
12
+
13
+ using NRSER
14
+
15
+ module StateMate; end
16
+ module StateMate::Adapters; end
17
+
18
+ module StateMate::Adapters::Defaults
19
+ KEY_SEP = ':'
20
+ DEFAULTS_CMD = '/usr/bin/defaults'
21
+
22
+ # convert a ruby object to a `REXML::Element` for a plist
23
+ def self.to_xml_element obj
24
+ case obj
25
+ when String
26
+ REXML::Element.new("string").add_text obj
27
+ when Fixnum
28
+ REXML::Element.new('integer').add_text obj.to_s
29
+ when Float
30
+ REXML::Element.new('real').add_text obj.to_s
31
+ when Hash
32
+ dict = REXML::Element.new('dict')
33
+ obj.each {|dict_key, dict_obj|
34
+ dict.add_element REXML::Element.new('key').add_text(dict_key)
35
+ dict.add_element to_xml_element(dict_obj)
36
+ }
37
+ dict
38
+ when Array
39
+ array = REXML::Element.new('array')
40
+ obj.each {|array_entry|
41
+ array.add_element to_xml_element(array_entry)
42
+ }
43
+ array
44
+ when TrueClass, FalseClass
45
+ REXML::Element.new obj.to_s
46
+ when Time
47
+ REXML::Element.new('date').add_text obj.utc.iso8601
48
+ else
49
+ raise "can't handle type: #{ obj.inspect }"
50
+ end
51
+ end # ::to_xml_element
52
+
53
+ def self.prefs_path user
54
+ if user == 'root'
55
+ '/Library/Preferences'
56
+ else
57
+ "/Users/#{ user }/Library/Preferences"
58
+ end
59
+ end # ::prefs_path
60
+
61
+ def self.domain_to_filepath domain, user = ENV['USER'], current_host = false
62
+ # there are a few cases:
63
+ #
64
+ # 1.) absolute file path
65
+ if domain.start_with? '/'
66
+ domain
67
+ #
68
+ # 2.) home-based path
69
+ elsif domain.start_with? '~/'
70
+ if user == 'root'
71
+ "/var/root/#{ domain[2..-1] }"
72
+ else
73
+ "/Users/#{ user }/#{ domain[2..-1] }"
74
+ end
75
+ #
76
+ # global domain
77
+ elsif domain == "NSGlobalDomain"
78
+ if current_host
79
+ "#{ prefs_path user }/.GlobalPreferences.#{ hardware_uuid }.plist"
80
+ else
81
+ "#{ prefs_path user }/.GlobalPreferences.plist"
82
+ end
83
+ #
84
+ # 3.) domain with corresponding plist
85
+ else
86
+ if current_host
87
+ "#{ prefs_path user }/ByHost/#{ domain }.#{ hardware_uuid }.plist"
88
+ else
89
+ "#{ prefs_path user }/#{ domain }.plist"
90
+ end
91
+ end
92
+ end # ::domain_to_filepath
93
+
94
+ def self.parse_key key
95
+ domain, *key_segs = case key
96
+ when Array
97
+ key
98
+ when String
99
+ key.split KEY_SEP
100
+ else
101
+ raise "must be string or array, not #{ key.inspect }"
102
+ end # case
103
+ [domain, key_segs]
104
+ end # ::parse_key
105
+
106
+ # Converts a CFType hiercharchy to native Ruby types
107
+ #
108
+ # customized to use the Base64 encoding of binary blobs since
109
+ # JSON pukes on the raw ones
110
+ def self.native_types(object,keys_as_symbols=false)
111
+ return if object.nil?
112
+
113
+ if (object.is_a?(CFPropertyList::CFDate) ||
114
+ object.is_a?(CFPropertyList::CFString) ||
115
+ object.is_a?(CFPropertyList::CFInteger) ||
116
+ object.is_a?(CFPropertyList::CFReal) ||
117
+ object.is_a?(CFPropertyList::CFBoolean)) ||
118
+ object.is_a?(CFPropertyList::CFUid) then
119
+ return object.value
120
+ elsif(object.is_a?(CFPropertyList::CFData)) then
121
+ return CFPropertyList::Blob.new(object.encoded_value)
122
+ elsif(object.is_a?(CFPropertyList::CFArray)) then
123
+ ary = []
124
+ object.value.each do
125
+ |v|
126
+ ary.push native_types(v)
127
+ end
128
+
129
+ return ary
130
+ elsif(object.is_a?(CFPropertyList::CFDictionary)) then
131
+ hsh = {}
132
+ object.value.each_pair do
133
+ |k,v|
134
+ k = k.to_sym if keys_as_symbols
135
+ hsh[k] = native_types(v)
136
+ end
137
+
138
+ return hsh
139
+ end
140
+ end
141
+
142
+ def self.read_defaults domain, current_host = false
143
+ file = Tempfile.new('read_defaults')
144
+ begin
145
+ cmd_parts = ['%{cmd}']
146
+ cmd_parts << '-currentHost' if current_host
147
+ cmd_parts << 'export'
148
+ cmd_parts << '%{domain}'
149
+ cmd_parts << '%{filepath}'
150
+ cmd = NRSER::Exec.sub cmd_parts.join(' '), cmd: DEFAULTS_CMD,
151
+ domain: domain,
152
+ filepath: file.path
153
+ NRSER::Exec.run cmd
154
+
155
+ plist = CFPropertyList::List.new file: file.path
156
+ data = native_types plist.value
157
+ ensure
158
+ file.close
159
+ file.unlink # deletes the temp file
160
+ end
161
+ end
162
+
163
+ def self.read_type domain, key, current_host
164
+ cmd_parts = ['%{cmd}']
165
+ cmd_parts << '-currentHost' if current_host
166
+ cmd_parts << 'read-type'
167
+ cmd_parts << '%{domain}'
168
+ cmd_parts << '%{key}'
169
+
170
+ cmd = NRSER::Exec.sub cmd_parts.join(' '), cmd: DEFAULTS_CMD,
171
+ domain: domain,
172
+ key: key
173
+
174
+ out = NRSER::Exec.run(cmd).chomp
175
+
176
+ case out
177
+ when "Type is string"
178
+ :string
179
+ when "Type is data"
180
+ :data
181
+ when "Type is integer"
182
+ :int
183
+ when "Type is float"
184
+ :float
185
+ when "Type is boolean"
186
+ :bool
187
+ when "Type is date"
188
+ :date
189
+ when "Type is array"
190
+ :array
191
+ when "Type is dictionary"
192
+ :dict
193
+ else
194
+ raise "unknown output: #{ out.inspect }"
195
+ end
196
+ end # ::read_type
197
+
198
+ def self.read key, options = {}
199
+ options = {
200
+ 'current_host' => false,
201
+ }.merge options
202
+
203
+ domain, key_segs = parse_key key
204
+
205
+ value = read_defaults domain, options['current_host']
206
+
207
+ key_segs.each do |seg|
208
+ value = if (value.is_a?(Hash) && value.key?(seg))
209
+ value[seg]
210
+ else
211
+ nil
212
+ end
213
+ end
214
+
215
+ # when 0 or 1 are returned they might actually be true or false
216
+ # case value
217
+ # when 0, 1
218
+ value
219
+ end # ::read
220
+
221
+ # def self.read_type
222
+
223
+ def self.write key, value, options = {}
224
+ options = {
225
+ 'current_host' => false,
226
+ }.merge options
227
+
228
+ domain, key_segs = parse_key key
229
+
230
+ if key_segs.length > 1
231
+ deep_write domain,
232
+ key_segs[0],
233
+ key_segs.drop(1),
234
+ value,
235
+ options['current_host']
236
+ else
237
+ basic_write domain,
238
+ key_segs[0],
239
+ value,
240
+ options['current_host']
241
+ end
242
+ end # ::write
243
+
244
+ def self.basic_delete domain, key, current_host
245
+ cmd_parts = ['%{cmd}']
246
+ cmd_parts << '-currentHost' if current_host
247
+ cmd_parts << 'delete'
248
+ cmd_parts << '%{domain}'
249
+ cmd_parts << '%{key}' unless key.empty?
250
+
251
+ result = NRSER::Exec.result cmd_parts.join(' '), cmd: DEFAULTS_CMD,
252
+ domain: domain,
253
+ key: key
254
+
255
+ result.check_error
256
+ result
257
+ end
258
+
259
+ def self.basic_write domain, key, value, current_host
260
+ return basic_delete(domain, key, current_host) if value.nil?
261
+
262
+ xml = to_xml_element(value).to_s
263
+
264
+ cmd_parts = ['%{cmd}']
265
+ cmd_parts << '-currentHost' if current_host
266
+ cmd_parts << 'write'
267
+ cmd_parts << '%{domain}'
268
+ cmd_parts << '%{key}' unless key.empty?
269
+ cmd_parts << '%{xml}'
270
+
271
+ cmd = NRSER::Exec.sub cmd_parts.join(' '), cmd: DEFAULTS_CMD,
272
+ domain: domain,
273
+ key: key,
274
+ xml: xml
275
+
276
+ NRSER::Exec.run cmd
277
+ end # ::basic_write
278
+
279
+ def self.hash_deep_write! hash, key, value
280
+ segment = key.first
281
+ rest = key[1..-1]
282
+
283
+ # terminating case: we are at the last segment
284
+ if rest.empty?
285
+ hash[segment] = value
286
+ else
287
+ case hash[segment]
288
+ when Hash
289
+ # go deeper
290
+ hash_deep_write! hash[segment], rest, value
291
+ else
292
+ hash[segment] = {}
293
+ hash_deep_write! hash[segment], rest, value
294
+ end
295
+ end
296
+ value
297
+ end # hash_deep_write!
298
+
299
+ def self.deep_write domain, key, deep_segs, value, current_host
300
+ root = read [domain, key], current_host: current_host
301
+ # handle the root not being there
302
+ root = {} if root.nil?
303
+ hash_deep_write! root, deep_segs, value
304
+ basic_write domain, key, root, current_host
305
+ end # ::deep_write
306
+
307
+ # get the "by host" / "current host" id, also called the "hardware uuid".
308
+ # adapted from
309
+ #
310
+ # <http://stackoverflow.com/questions/933460/unique-hardware-id-in-mac-os-x>
311
+ #
312
+ def self.hardware_uuid
313
+ plist_xml_str = NRSER::Exec.run "ioreg -r -d 1 -c IOPlatformExpertDevice -a"
314
+ plist = CFPropertyList::List.new data: plist_xml_str
315
+ dict = CFPropertyList.native_types(plist.value).first
316
+ dict['IOPlatformUUID']
317
+ end # ::hardware_uuid
318
+
319
+ # `defaults` will return `true` as `1` and `false` as `0` :/
320
+ def self.values_equal? current, desired
321
+ case desired
322
+ when true
323
+ current == true || current == 1
324
+ when false
325
+ current == false || current == 0
326
+ else
327
+ current == desired
328
+ end
329
+ end
330
+ end
@@ -0,0 +1,35 @@
1
+ require 'nrser'
2
+ require 'nrser/exec'
3
+
4
+ using NRSER
5
+
6
+ module StateMate; end
7
+ module StateMate::Adapters; end
8
+
9
+ module StateMate::Adapters::GitConfig
10
+ def self.read key, options = {}
11
+ result = NRSER::Exec.result "git config --global --get %{key}", key: key
12
+
13
+ if result.success?
14
+ result.output.chomp
15
+ elsif result.output == ''
16
+ nil
17
+ else
18
+ result.raise_error
19
+ end
20
+ end
21
+
22
+ def self.write key, value, options = {}
23
+ action = if read(key, options).nil?
24
+ '--add'
25
+ else
26
+ '--replace'
27
+ end
28
+
29
+ result = NRSER::Exec.result(
30
+ "git config --global #{ action } %{key} %{value}",
31
+ key: key,
32
+ value: value
33
+ )
34
+ end
35
+ end # GitConfig
@@ -0,0 +1,66 @@
1
+ require 'json'
2
+
3
+ require 'nrser'
4
+ require 'nrser/exec'
5
+
6
+ require 'state_mate'
7
+ require 'state_mate/adapters/defaults'
8
+
9
+ using NRSER
10
+
11
+ module StateMate::Adapters::JSON
12
+ def self.parse_key key
13
+ # use the same key seperation as Defaults
14
+ StateMate::Adapters::Defaults.parse_key key
15
+ end
16
+
17
+ def self.read key, options = {}
18
+ filepath, key_segs = parse_key key
19
+
20
+ contents = File.read(File.expand_path(filepath))
21
+
22
+ value = JSON.load contents
23
+
24
+ key_segs.each do |seg|
25
+ value = if (value.is_a?(Hash) && value.key?(seg))
26
+ value[seg]
27
+ else
28
+ nil
29
+ end
30
+ end
31
+
32
+ value
33
+ end
34
+
35
+ def self.write key, value, options = {}
36
+ options = {
37
+ 'pretty' => true,
38
+ }.merge options
39
+
40
+ filepath, key_segs = parse_key key
41
+
42
+ new_root = if key_segs.length > 1
43
+ root = read filepath
44
+
45
+ StateMate::Adapters::Defaults.hash_deep_write!(
46
+ root,
47
+ key_segs,
48
+ value
49
+ )
50
+
51
+ root
52
+ else
53
+ value
54
+ end
55
+
56
+ content = if options['pretty']
57
+ JSON.pretty_generate new_root
58
+ else
59
+ JSON.dump new_root
60
+ end
61
+
62
+ File.open(filepath, 'w') do |f|
63
+ f.write content
64
+ end
65
+ end
66
+ end # JSON
@@ -0,0 +1,117 @@
1
+ require 'pp'
2
+
3
+ require 'CFPropertyList'
4
+
5
+ require 'nrser'
6
+ require 'nrser/exec'
7
+
8
+ require 'state_mate'
9
+ require 'state_mate/adapters/defaults'
10
+
11
+ using NRSER
12
+
13
+ # very useful:
14
+ #
15
+ # <http://launchd.info/>
16
+ # 46
17
+
18
+ module StateMate::Adapters::LaunchD
19
+
20
+ EXE = '/bin/launchctl'
21
+
22
+ def self.truncate_values hash, length
23
+ hash.map {|k, v|
24
+ case v
25
+ when String
26
+ [k, v.truncate(length)]
27
+ when Hash
28
+ [k, truncate_values(v, length)]
29
+ else
30
+ [k ,v]
31
+ end
32
+ }.to_h
33
+ end
34
+
35
+ def self.user_overrides_db_path user = ENV['USER']
36
+ if user == 'root'
37
+ "/var/db/launchd.db/com.apple.launchd/overrides.plist"
38
+ else
39
+ user_id = NRSER::Exec.run("id -u %{user}", user: user).chomp.to_i
40
+ "/var/db/launchd.db/com.apple.launchd.peruser.#{ user_id }/overrides.plist"
41
+ end
42
+ end
43
+
44
+ def self.user_overrides_db user = ENV['USER']
45
+ plist = CFPropertyList::List.new file: user_overrides_db_path(user)
46
+ CFPropertyList.native_types plist.value
47
+ end
48
+
49
+ def self.disabled? label, user = ENV['USER']
50
+ db = user_overrides_db(user)
51
+ return false unless db.key?(label) && db[label].key?('Disabled')
52
+ db[label]['Disabled']
53
+ end
54
+
55
+ def self.loaded? label
56
+ begin
57
+ NRSER::Exec.run "%{exe} list -x %{label}", exe: EXE, label: label
58
+ rescue SystemCallError => e
59
+ false
60
+ else
61
+ true
62
+ end
63
+ end
64
+
65
+ def self.parse_key key
66
+ # use the same key seperation as Defaults
67
+ StateMate::Adapters::Defaults.parse_key key
68
+ end
69
+
70
+ def self.load file_path
71
+ NRSER::Exec.run "%{exe} load -w %{file_path}", exe: EXE,
72
+ file_path: file_path
73
+ end
74
+
75
+ def self.unload file_path
76
+ NRSER::Exec.run "%{exe} unload -w %{file_path}", exe: EXE,
77
+ file_path: file_path
78
+ end
79
+
80
+ def self.read key, options = {}
81
+ file_path, key_segs = parse_key key
82
+
83
+ # get the hash of the plist at the file path and use that to get the label
84
+ plist = CFPropertyList::List.new file: file_path
85
+ data = CFPropertyList.native_types plist.value
86
+ label = data["Label"]
87
+
88
+ case key_segs
89
+ # the only thing we can handle right now
90
+ when ['Disabled']
91
+ disabled? label
92
+ else
93
+ raise "unprocessable key: #{ key.inspect }"
94
+ end
95
+ end
96
+
97
+ def self.write key, value, options = {}
98
+ file_path, key_segs = parse_key key
99
+
100
+ case key_segs
101
+ when ['Disabled']
102
+ case value
103
+ when true
104
+ unload file_path
105
+
106
+ when false
107
+ load file_path
108
+
109
+ else
110
+ raise StateMate::Error::TypeError value, "expected true or false"
111
+
112
+ end
113
+ else
114
+ raise "unprocessable key: #{ key.inspect }"
115
+ end
116
+ end
117
+ end # LaunchD