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,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