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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/ansible/library/state +59 -0
- data/lib/state_mate.rb +366 -0
- data/lib/state_mate/adapters/defaults.rb +330 -0
- data/lib/state_mate/adapters/git_config.rb +35 -0
- data/lib/state_mate/adapters/json.rb +66 -0
- data/lib/state_mate/adapters/launchd.rb +117 -0
- data/lib/state_mate/adapters/nvram.rb +44 -0
- data/lib/state_mate/adapters/time_machine.rb +60 -0
- data/lib/state_mate/version.rb +3 -0
- data/notes/state-set-steps.md +26 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/state_mate/adapters/defaults/hardware_uuid_spec.rb +15 -0
- data/spec/state_mate/adapters/defaults/hash_deep_write_spec.rb +33 -0
- data/spec/state_mate/adapters/defaults/read_defaults_spec.rb +57 -0
- data/spec/state_mate/adapters/defaults/read_spec.rb +32 -0
- data/spec/state_mate/adapters/defaults/read_type_spec.rb +27 -0
- data/spec/state_mate/adapters/defaults/write_spec.rb +29 -0
- data/spec/state_mate/adapters/git_config/read_spec.rb +36 -0
- data/spec/state_mate/adapters/git_config/write_spec.rb +17 -0
- data/spec/state_mate/adapters/json/read_spec.rb +56 -0
- data/spec/state_mate/adapters/json/write_spec.rb +54 -0
- data/spec/state_mate_spec.rb +7 -0
- data/state_mate.gemspec +25 -0
- data/test/ansible/ansible.cfg +5 -0
- data/test/ansible/clear +2 -0
- data/test/ansible/hosts +1 -0
- data/test/ansible/play +2 -0
- data/test/ansible/playbook.yml +38 -0
- data/test/ansible/read +2 -0
- metadata +167 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -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
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
data/lib/state_mate.rb
ADDED
@@ -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
|