state_mate 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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