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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f14c453e245d291cf47b02c15b3adb98d2692031
4
+ data.tar.gz: b902035035ae436940b6bc570c913c48a6985521
5
+ SHA512:
6
+ metadata.gz: 2c8c217a5ad90c6f7eb8bacdcd758477342bc9e3087d7f97c06f03df7e54f16c22d6bbc69fc1654e97ff7d3025bfe116cd897c5544ce7d194914d22fe0509835
7
+ data.tar.gz: e498d0a52b5f4f13941205ab984cd6a0b91fab22b4cac7e4da152211d67856c8c2a8dd63795989af2814ede653eb97a40267728184d3af4f20639d221c80f2f2
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # use local
4
+ # gem 'nrser', '~> 0.0', :path => '../nrser-ruby'
5
+
6
+ # Specify your gem's dependencies in state_mate.gemspec
7
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 nrser
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # StateMate
2
+
3
+ i heard it's meant to help you with your state, mate!
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'state_mate'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install state_mate
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/nrser/state_mate/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ # WANT JSON
3
+
4
+ require 'json'
5
+ require 'shellwords'
6
+ require 'pp'
7
+
8
+ require 'bundler/setup'
9
+
10
+ require 'nrser'
11
+ require 'state_mate'
12
+
13
+ using NRSER
14
+
15
+ MODULE_COMPLEX_ARGS = "<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
16
+
17
+ def parse input
18
+ # require 'shellwords'
19
+ parsed = {}
20
+ Shellwords.split(input).each do |word|
21
+ (key, value) = word.split('=', 2)
22
+ parsed[key] = value
23
+ end
24
+ unless MODULE_COMPLEX_ARGS.empty?
25
+ parsed.update JSON.load(MODULE_COMPLEX_ARGS)
26
+ end
27
+ parsed
28
+ end
29
+
30
+ def main
31
+ input = nil
32
+ args = nil
33
+ changed = false
34
+
35
+ begin
36
+ input = File.read ARGV[0]
37
+ args = parse input
38
+
39
+ spec = args
40
+
41
+ changes = StateMate.execute spec
42
+
43
+ print JSON.dump({
44
+ 'changed' => !changes.empty?,
45
+ 'changes' => changes,
46
+ })
47
+ rescue Exception => e
48
+ print JSON.dump({
49
+ 'failed' => true,
50
+ 'msg' => e.format,
51
+ # 'input' => input,
52
+ 'args' => args,
53
+ # 'ARGV' => ARGV,
54
+ # 'ruby' => RUBY_VERSION,
55
+ })
56
+ end
57
+ end
58
+
59
+ main if __FILE__ == $0
@@ -0,0 +1,366 @@
1
+ require 'set'
2
+ require 'nrser'
3
+
4
+ require "state_mate/version"
5
+
6
+ using NRSER
7
+
8
+ module StateMate
9
+
10
+ DIRECTIVES = Set.new [
11
+ 'set',
12
+ 'unset',
13
+ 'array_contains',
14
+ 'array_missing',
15
+ ]
16
+
17
+ module Error
18
+ class ExecutionError < StandardError; end
19
+
20
+ class WriteError < ExecutionError; end
21
+
22
+ class ValueChangeError < ExecutionError; end
23
+
24
+ class TypeError < ::TypeError
25
+ attr_accessor :value
26
+
27
+ def initialize value, msg
28
+ @value = value
29
+ super "#{ msg }, found #{ value.inspect }"
30
+ end
31
+ end
32
+ end # Error
33
+
34
+ class StateSet
35
+ attr_accessor :spec
36
+ attr_reader :states,
37
+ :read_values,
38
+ :states_to_change,
39
+ :new_values,
40
+ :written_states,
41
+ :write_error,
42
+ :rollback_errors,
43
+ :changes
44
+
45
+
46
+ State = Struct.new :adapter,
47
+ :key,
48
+ :directive,
49
+ :value,
50
+ :options
51
+
52
+ def self.from_spec spec
53
+ state_set = self.new
54
+ state_set.spec = spec
55
+
56
+ unless spec.is_a? Hash
57
+ raise Error::TypeError.new spec,
58
+ "spec must be a Hash of adapter names to states"
59
+ end
60
+
61
+ spec.each do |adapter_name, states|
62
+ adapter = StateMate.get_adapter adapter_name
63
+
64
+ states = case states
65
+ when Hash
66
+ [states]
67
+ when Array
68
+ states
69
+ else
70
+ raise Error::TypeError.new states, <<-BLOCK.unblock
71
+ each value of the spec needs to be a single state hash or an
72
+ array or state
73
+ BLOCK
74
+ end
75
+
76
+ states.each do |state|
77
+ unless spec.is_a? Hash
78
+ raise Error::TypeError.new state, "each state needs to be a Hash"
79
+ end
80
+
81
+ key = nil
82
+ directives = []
83
+ options = {}
84
+
85
+ state.each do |k, v|
86
+ if k == 'key'
87
+ key = v
88
+ elsif k == 'options'
89
+ options = v
90
+ elsif DIRECTIVES.include? k
91
+ directives << [k, v]
92
+ else
93
+ raise "bad key #{ k.inspect } in state #{ state.inspect }"
94
+ end
95
+ end
96
+
97
+ directive, value = case directives.length
98
+ when 0
99
+ raise "no directive found in #{ state.inspect }"
100
+ when 1
101
+ directives.first
102
+ else
103
+ raise "multiple directives found in #{ state.inspect }"
104
+ end
105
+
106
+ state_set.add adapter, key, directive, value, options
107
+ end # state.each
108
+ end # states.each
109
+
110
+ state_set
111
+ end # from_spec
112
+
113
+ def initialize
114
+ @spec = nil
115
+ @states = []
116
+ @read_values = {}
117
+ @states_to_change = []
118
+ @new_values = []
119
+ @written_states = []
120
+ @write_error = nil
121
+ # map of states to errors raised when trying to rollback
122
+ @rollback_errors = {}
123
+ # report of changes made
124
+ @changes = {}
125
+ end
126
+
127
+ def add adapter, key, directive, value, options = {}
128
+ @states << State.new(adapter, key, directive, value, options)
129
+ end
130
+
131
+ def execute
132
+ # find out what needs to be changed
133
+ @states.each do |state|
134
+ # read the current value
135
+ read_value = state.adapter.read state.key, state.options
136
+
137
+ # store it for use in the actual change
138
+ @read_values[state] = read_value
139
+
140
+ # the test method is the directive with a '?' appended,
141
+ # like `set?` or `array_contains?`
142
+ test_method = StateMate.method "#{ state.directive }?"
143
+
144
+ # find out if the state is in sync
145
+ in_sync = test_method.call state.key, read_value, state.value, state.adapter
146
+
147
+ # add to the list of changes to be made for states that are
148
+ # out of sync
149
+ @states_to_change << state unless in_sync
150
+ end
151
+
152
+ # if everything is in sync, no changes need to be attempted
153
+ # reutrn the empty hash of changes
154
+ return @changes if @states_to_change.empty?
155
+
156
+ # do the change to each in-memory value
157
+ # this will raise an excption if the operation can't be done for
158
+ # some reason
159
+ states_to_change.each do |state|
160
+ sync_method = StateMate.method state.directive
161
+ # we want to catch any error and report it
162
+ begin
163
+ new_value = sync_method.call state.key,
164
+ @read_values[state],
165
+ state.value,
166
+ state.options
167
+ rescue Exception => e
168
+ @new_value_error = e
169
+ raise Error::ValueChangeError.new tpl binding, <<-BLOCK
170
+ an error occured when changing a values:
171
+
172
+ <%= @new_value_error.format %>
173
+
174
+ no changes were attempted to the system, so there is no rollback
175
+ neessicary.
176
+ BLOCK
177
+ end
178
+ # change successful, store the new value along-side the state
179
+ # for use in the next block
180
+ @new_values << [state, new_value]
181
+ end
182
+
183
+ new_values.each do |state, new_value|
184
+ begin
185
+ state.adapter.write state.key, new_value, state.options
186
+ rescue Exception => e
187
+ @write_error = e
188
+ rollback
189
+ raise Error::WriteError.new tpl binding, <<-BLOCK
190
+ an error occured when writing new state values:
191
+
192
+ <%= @write_error.format %>
193
+
194
+ <% if @written_states.empty? %>
195
+ the error occured on the first write, so no values were rolled
196
+ back.
197
+
198
+ <% else %>
199
+ <% if @rollback_errors.empty? %>
200
+ all values were sucessfully rolled back:
201
+
202
+ <% else %>
203
+ some values failed to rollback:
204
+
205
+ <% end %>
206
+
207
+ <% @written_states.each do |state| %>
208
+ <% if @rollback_errors[state] %>
209
+ <% state.key %>: <% @rollback_errors[state].format.indent(8) %>
210
+ <% else %>
211
+ <%= state.key %>: rolled back.
212
+ <% end %>
213
+ <% end %>
214
+ <% end %>
215
+ BLOCK
216
+ else
217
+ @written_states << state
218
+ end # begin / rescue / else
219
+ end # new_values.each
220
+
221
+ # ok, we made it. report the changes
222
+ new_values_hash = Hash[@new_values]
223
+ @written_states.each do |state|
224
+ @changes[[state.adapter.name, state.key]] = [@read_values[state], new_values_hash[state]]
225
+ end
226
+
227
+ @changes
228
+ end # execute
229
+
230
+ private
231
+
232
+ def rollback
233
+ # go through the writes that were sucessfully made and try to
234
+ # reverse them
235
+ @written_states.reverse.each do |state|
236
+ # wrap in rescue so that we can record that the rollback failed
237
+ # for a value and continue
238
+ begin
239
+ state.adapter.write state.key, state.value, state.options
240
+ rescue Exception => e
241
+ # record when and why a rollback fails to include it in the
242
+ # exiting exception
243
+ @rollback_errors[state] = e
244
+ end
245
+ end
246
+ end # rollback
247
+ end # StateSet
248
+
249
+ def self.get_adapter adapter_name
250
+ begin
251
+ require "state_mate/adapters/#{ adapter_name }"
252
+ StateMate::Adapters.constants.find {|sym|
253
+ sym.to_s.downcase == adapter_name.gsub('_', '')
254
+ }.pipe {|sym| StateMate::Adapters.const_get sym}
255
+ rescue
256
+ raise "can't find adapter #{ adapter_name.inspect }"
257
+ end
258
+ end
259
+
260
+ def self.execute spec
261
+ StateSet.from_spec(spec).execute
262
+ end
263
+
264
+ def self.values_equal? current, desired, adapter
265
+ if adapter.respond_to? :values_equal?
266
+ adapter.values_equal? current, desired
267
+ else
268
+ current == desired
269
+ end
270
+ end
271
+
272
+ def self.set? key, current, value, adapter
273
+ values_equal? current, value, adapter
274
+ end
275
+
276
+ def self.set key, current, value, options
277
+ # TODO: handle options
278
+ value
279
+ end
280
+
281
+ def self.unset? key, current, value, adapter
282
+ current.nil?
283
+ end
284
+
285
+ def self.unset key, current, value, options
286
+ # TODO: handle options
287
+ raise "value most be nil to unset" unless value.nil?
288
+ nil
289
+ end
290
+
291
+ def self.array_contains? key, current, value, adapter
292
+ current.is_a?(Array) && current.any? {|v|
293
+ values_equal? v, value, adapter
294
+ }
295
+ end
296
+
297
+ def self.array_contains key, current, value, options
298
+ case current
299
+ when Array
300
+ current + [value]
301
+
302
+ when nil
303
+ # it needs to be created
304
+ if options[:create]
305
+ [value]
306
+ else
307
+ raise <<-BLOCK.unblock
308
+ can not ensure #{ key.inspect } contains #{ value.inspect } because
309
+ the key does not exist and options[:create] is not true.
310
+ BLOCK
311
+ end
312
+
313
+ else
314
+ # there is something there, but it's not an array. out only option
315
+ # to achieve the declared state is to replace it with a new array
316
+ # where value is the only element, but we don't want to do that unless
317
+ # we've been told to clobber
318
+ if options[:clobber]
319
+ [value]
320
+ else
321
+ raise <<-BLOCK.unblock
322
+ can not ensure #{ key.inspect } contains #{ value.inspect } because
323
+ the value is #{ current.inspect } and options[:clobber] is not true.
324
+ BLOCK
325
+ end
326
+ end # case current
327
+ end # array_contians
328
+
329
+ def self.array_missing? key, current, value, adapter
330
+ current.is_a?(Array) && !current.any? {|v|
331
+ values_equal? v, value, adapter
332
+ }
333
+ end
334
+
335
+ def self.array_missing key, current, value, options
336
+ case current
337
+ when Array
338
+ current - [value]
339
+
340
+ when nil
341
+ # there is no value, only option is to create a new empty array there
342
+ if options[:create]
343
+ []
344
+ else
345
+ raise <<-BLOCK.unblock
346
+ can not ensure #{ key.inspect } missing #{ value.inspect } because
347
+ the key does not exist and options[:create] is not true.
348
+ BLOCK
349
+ end
350
+
351
+ else
352
+ # there is something there, but it's not an array. out only option
353
+ # to achieve the declared state is to replace it with a new empty array
354
+ # but we don't want to do that unless we've been told to clobber
355
+ if options[:clobber]
356
+ []
357
+ else
358
+ raise <<-BLOCK.unblock
359
+ can not ensure #{ key.inspect } missing #{ value.inspect } because
360
+ the value is #{ current.inspect } and options[:clobber] is not true.
361
+ BLOCK
362
+ end
363
+ end # case current
364
+ end # array_missing
365
+
366
+ end # StateMate