furnish 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,39 @@
1
+ * 0.1.0 (04/09/2013)
2
+ * Furnish requires 1.9.3 or greater -- always has been the case, now rubygems enforces that for us.
3
+ * Runtime performance increased significantly. No hard numbers, but test
4
+ suite assertion count doubled and total test runtime actually dropped
5
+ compared to 0.0.4. Yay.
6
+ * Furnish::Provisioner::API is the new way to write provisioners. Please read its documentation.
7
+ * Related, any existing provisioners will need significant changes to meet new changes.
8
+ * Provisioners now have programmed property and object construction
9
+ semantics, and the properties can now be queried, allowing for abstract
10
+ provisioning logic.
11
+ * Recovery mode: provisioners can opt-in to being able to recover from
12
+ transient failures. See Furnish::ProvisionerGroup#recover for more
13
+ information.
14
+ * Related, Threaded mode schedulers no longer stop when when a provision
15
+ fails. It instead marks Furnish::Scheduler#needs_recovery?
16
+ * Failed provisions still keep any dependencies from starting, but
17
+ independent provisions can still run.
18
+ * Serial mode schedulers still have the same behavior when
19
+ Furnish::Scheduler#run is called, but you can attempt to recover the
20
+ scheduler from where the run threw an exception.
21
+ * Furnish::Protocol is a way to specify what input and output provisioners
22
+ operate on. Static checking will occur at scheduling time to ensure a
23
+ provision can succeed (this is not a guarantee it will, just a way to
24
+ determine if it definitely won't).
25
+ * state transitions are now represented as hashes and merged over and
26
+ passed on. this means for provisioner group consisting of A -> B -> C,
27
+ that A can provide information that C can use without B knowing any
28
+ better. This is enforced by the static checking Furnish::Protocol
29
+ provides.
30
+ * shutdown provisioner state transitions can now carry data between them.
31
+ * Upgrade to palsy 0.0.4, which brings many consistency/durability/state
32
+ management benefits for persistent storage use.
33
+ * API shorthand for Furnish::Scheduler:
34
+ * `<<` is now an alias for `schedule_provisioner_group`
35
+ * `s` and `sched` are now aliases for `schedule_provision`
36
+ * Probably some other shit I don't remember now.
1
37
  * 0.0.4 (03/25/2013)
2
38
  * Support for FURNISH_DEBUG environment variable for test suites.
3
39
  * Ruby 2.0.0-p0 Compatibility Fixes
data/Gemfile CHANGED
@@ -2,5 +2,3 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in furnish.gemspec
4
4
  gemspec
5
-
6
- gem 'guard-rake', :git => "https://github.com/erikh/guard-rake", :branch => "failure_ok"
data/Guardfile CHANGED
@@ -2,9 +2,9 @@
2
2
  guard 'minitest' do
3
3
  # with Minitest::Unit
4
4
  watch(%r!^test/(.*)\/?test_(.*)\.rb!)
5
- watch(%r!^test/(?:helper|mt_cases)\.rb!) { "test" }
5
+ watch(%r!^test/(?:helper|mt_cases|dummy_classes)\.rb!) { "test" }
6
6
  end
7
7
 
8
- guard 'rake', :failure_ok => true, :run_on_all => false, :task => 'rdoc_cov' do
8
+ guard 'rake', :run_on_all => false, :task => 'rdoc_cov' do
9
9
  watch(%r!^lib/(.*)([^/]+)\.rb!)
10
10
  end
@@ -16,13 +16,14 @@ Gem::Specification.new do |gem|
16
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
+ gem.required_ruby_version = '>= 1.9.3'
19
20
 
20
- gem.add_dependency 'palsy', '~> 0.0.2'
21
+ gem.add_dependency 'palsy', '~> 0.0.4'
21
22
 
22
23
  gem.add_development_dependency 'rake'
23
24
  gem.add_development_dependency 'minitest', '~> 4.5.0'
24
25
  gem.add_development_dependency 'guard-minitest'
25
- gem.add_development_dependency 'guard-rake'
26
+ gem.add_development_dependency 'guard-rake', '~> 0.0.8'
26
27
  gem.add_development_dependency 'rdoc', '~> 4'
27
28
  gem.add_development_dependency 'rb-fsevent'
28
29
  gem.add_development_dependency 'simplecov'
@@ -4,10 +4,8 @@ require 'furnish/logger'
4
4
  require 'furnish/scheduler'
5
5
 
6
6
  #
7
- # Furnish is a scheduling system that has a massive readme which explains what
8
- # it does here:
9
- #
10
- # https://github.com/erikh/furnish
7
+ # Furnish is a scheduling system. Check out the README for basic usage
8
+ # instructions.
11
9
  #
12
10
  # You may also wish to read the Furnish::Scheduler, Furnish::Logger, and
13
11
  # Furnish::ProvisionerGroup documentation to learn more about it.
@@ -0,0 +1,226 @@
1
+ module Furnish # :nodoc:
2
+ #
3
+ # Furnish::Protocol implements a validating protocol for state transitions.
4
+ # It is an optional feature and not necessary for using Furnish.
5
+ #
6
+ # A Furnish::ProvisionerGroup looks like this:
7
+ #
8
+ # thing_a -> thing_b -> thing_c
9
+ #
10
+ # Where these things are furnish provisioners. When the scheduler says it's
11
+ # ready to provision this group, it executes thing_a's startup routine,
12
+ # passes its results to thing_b's startup routine, and then thing_b's startup
13
+ # routine passes its things to thing_c's startup routine.
14
+ #
15
+ # Presuming this all succeeds (and returns truthy values), the group is
16
+ # marked as 'solved', the scheduler considers it finished and will ignore
17
+ # future requests to provision it again. It also will start working on
18
+ # anything that depends on its solved state.
19
+ #
20
+ # A problem is that you have to know ahead of time how a, b, and c interact
21
+ # for this to be successful. For example, you can't allocate an EC2 security
22
+ # group, then an instance, then a VPC, and expect the security group and
23
+ # instance to live in that VPC. It's not only out of order, but the security
24
+ # group doesn't know enough at the time it runs to leverage the VPC, because
25
+ # the VPC doesn't exist yet.
26
+ #
27
+ # Furnish::Protocol lets you describe what each provisioner requires, what it
28
+ # accepts, and what it yields, so that analysis can be performed at scheduler
29
+ # time (when it's configured) instead of provisioning time (when it actually
30
+ # runs). This surfaces issues quicker and has some additional advantages for
31
+ # interfaces where users may not have full visibility into what the
32
+ # provisioners do (such as closed source provisioners, or inadequately
33
+ # documented ones).
34
+ #
35
+ # Here's a description of how this logic works for two adjacent provisioners
36
+ # in the group, a and b:
37
+ #
38
+ # * if Provisioner A and Provisioner B implement Furnish::Protocol
39
+ # * if B requires anything, and A yields all of it with the proper types
40
+ # * if B accepts anything, and A yields any of it with the proper types
41
+ # * success
42
+ # * else failure
43
+ # * if B accepts anything, and A yields any of it with the proper types
44
+ # * success
45
+ # * if B has #accepts_from_any set to true
46
+ # * success
47
+ # * if B accepts nothing
48
+ # * success
49
+ # * else failure
50
+ # * else success
51
+ #
52
+ # Provisioners at the head and tail do not get subject to acceptance tests
53
+ # because there's nothing to yield, or nothing to accept what is yielded.
54
+ #
55
+ class Protocol
56
+
57
+ ##
58
+ # :method:
59
+ # :call-seq:
60
+ # requires(name)
61
+ # requires(name, description)
62
+ # requires(name, description, type)
63
+ #
64
+ # Specifies a requirement. The name is the key of the requirement, the
65
+ # description is a text explanantion of what the requirement is used for,
66
+ # for informational purposes. The name and type (which is a class) are
67
+ # compared to #yields in #requires_from and the logic behind that is
68
+ # explained in Furnish::Protocol.
69
+ #
70
+ # See Furnish::Provisioner::API.configure_startup for a usage example.
71
+ #
72
+
73
+ ##
74
+ # :method:
75
+ # :call-seq:
76
+ # accepts(name)
77
+ # accepts(name, description)
78
+ # accepts(name, description, type)
79
+ #
80
+ # Specifies acceptance critieria. While #requires is "all", this is "any",
81
+ # and acceptance is further predicated by the #accepts_from_any status if
82
+ # acceptance checks fail. The attributes set here will be used in
83
+ # #accepts_from to validate against another provisioner.
84
+ #
85
+ # See the logic explanation in Furnish::Protocol for more information.
86
+ #
87
+ # See Furnish::Provisioner::API.configure_startup for a usage example.
88
+ #
89
+
90
+ ##
91
+ # :method:
92
+ # :call-seq:
93
+ # yields(name)
94
+ # yields(name, description)
95
+ # yields(name, description, type)
96
+ #
97
+ # Specifies what the provisioner is expected to yield. This is the producer
98
+ # for the consumer counterparts #requires and #accepts. The configuration
99
+ # made here will be used in both #requires_from and #accepts_from for
100
+ # determining if two provisioners can talk to each other.
101
+ #
102
+ # See the logic explanation in Furnish::Protocol for more information.
103
+ #
104
+ # See Furnish::Provisioner::API.configure_startup for a usage example.
105
+ #
106
+
107
+ VALIDATOR_NAMES = [:requires, :accepts, :yields] # :nodoc:
108
+
109
+ #
110
+ # Construct a Furnish::Protocol object.
111
+ #
112
+ def initialize
113
+ @hash = Hash[VALIDATOR_NAMES.map { |n| [n, { }] }]
114
+ @configuring = false
115
+ end
116
+
117
+ #
118
+ # This runs the block given instance evaled against the current
119
+ # Furnish::Protocol object. It is used by Furnish::Provisioner::API's
120
+ # syntax sugar.
121
+ #
122
+ # Additionally it sets a simple lock to ensure the assertions
123
+ # Furnish::Protocol provides cannot be used during configuration time, like
124
+ # #accept_from and #requires_from.
125
+ #
126
+ def configure(&block)
127
+ @configuring = true
128
+ instance_eval(&block)
129
+ @configuring = false
130
+ end
131
+
132
+ #
133
+ # Allow #accepts_from to completely mismatch with yields from a compared
134
+ # provisioner and still succeed. Use with caution.
135
+ #
136
+ # See the logic discussion in Furnish::Protocol for a deeper explanation.
137
+ #
138
+ def accepts_from_any(val)
139
+ @hash[:accepts_from_any] = val
140
+ end
141
+
142
+ #
143
+ # look up a rule set -- generally should not be used by consumers.
144
+ #
145
+ def [](key)
146
+ @hash[key]
147
+ end
148
+
149
+ #
150
+ # For a passed Furnish::Protocol object, ensures that this protocol object
151
+ # satisfies its requirements based on what it yields.
152
+ #
153
+ # See the logic discussion in Furnish::Protocol for a deeper explanation.
154
+ #
155
+ def requires_from(protocol)
156
+ not_configurable(__method__)
157
+
158
+ return true unless protocol
159
+
160
+ yp = protocol[:yields]
161
+ rp = self[:requires]
162
+
163
+ rp.keys.empty? ||
164
+ (
165
+ (yp.keys & rp.keys).sort == rp.keys.sort &&
166
+ rp.keys.all? { |k| rp[k][:type].ancestors.include?(yp[k][:type]) }
167
+ )
168
+ end
169
+
170
+ #
171
+ # For a passed Furnish::Protocol object, ensures that at least one thing
172
+ # this protocol object accepts is satisfied by what that Furnish::Protocol
173
+ # object yields.
174
+ #
175
+ # See the logic discussion in Furnish::Protocol for a deeper explanation.
176
+ #
177
+ def accepts_from(protocol)
178
+ not_configurable(__method__)
179
+
180
+ return true unless protocol
181
+
182
+ yp = protocol[:yields]
183
+ ap = self[:accepts]
184
+
185
+ return true if ap.keys.empty?
186
+
187
+ if (yp.keys & ap.keys).empty?
188
+ return self[:accepts_from_any]
189
+ end
190
+
191
+ return (yp.keys & ap.keys).all? { |k| ap[k][:type].ancestors.include?(yp[k][:type]) }
192
+ end
193
+
194
+ VALIDATOR_NAMES.each do |vname|
195
+ class_eval <<-EOF
196
+ def #{vname}(name, description='', type=Object)
197
+ name = name.to_sym unless name.kind_of?(Symbol)
198
+ build(#{vname.inspect}, name, description, type)
199
+ end
200
+ EOF
201
+ end
202
+
203
+ private
204
+
205
+ #
206
+ # Just a little "pragma" to ensure certain methods cannot be used in
207
+ # #configure.
208
+ #
209
+ def not_configurable(meth_name)
210
+ if @configuring
211
+ raise RuntimeError, "cannot use method '#{meth_name}' during protocol configuration"
212
+ end
213
+ end
214
+
215
+ #
216
+ # Metaprogramming shim, delegated to by #requires, #accepts and #yields.
217
+ # Fills out the tables when classes use those methods.
218
+ #
219
+ def build(vtype, name, description, type)
220
+ @hash[vtype][name] = {
221
+ :description => description,
222
+ :type => type
223
+ }
224
+ end
225
+ end
226
+ end
@@ -3,44 +3,7 @@ module Furnish
3
3
  # Furnish provides no Provisioners as a part of its package. To use
4
4
  # pre-packaged provisioners, you must install additional packages.
5
5
  #
6
- # Provisioners are *objects* that have a simple API and as a result, there is
7
- # no "interface" for them in the classic term. You implement it, and if it
8
- # doesn't work, you'll know in a hurry.
9
- #
10
- # I'm going to say this again -- Furnish does not construct your object.
11
- # That's your job.
12
- #
13
- # Provisioners need 3 methods and one attribute, outside of that, you can do
14
- # anything you want.
15
- #
16
- # * name is an attribute (getter/setter) that holds a string. It is used in
17
- # numerous places, is set by the ProvisionerGroup, and must not be volatile.
18
- # * startup(*args) is a method to bring the provisioner "up" that takes an
19
- # arbitrary number of arguments and returns truthy or falsey, and in
20
- # exceptional cases may raise. A falsey return value means that provisioning
21
- # failed and the Scheduler will stop. A truthy value is passed to the next
22
- # startup method in the ProvisionerGroup.
23
- # * shutdown is a method to bring the provisioner "down" and takes no
24
- # arguments. Like startup, truthy means success and falsey means failed, and
25
- # exceptions are fine, but return values aren't chained.
26
- # * report returns an array of strings, and is used for diagnostic functions.
27
- # You can provide anything that fits that description, such as IP addresses
28
- # or other identifiers.
29
- #
30
- # Tracking external state is not Furnish's job, that's for your provisioner.
31
- # Palsy is a state management system that Furnish links deeply to, so any
32
- # state tracking you do in your provisioner, presuming you do it with Palsy,
33
- # will be tracked along with Furnish's state information in the same
34
- # database. That said, you can do whatever you want. Furnish doesn't try to
35
- # think about your provisioner deadlocking itself because it's sharing state
36
- # with another provisioner, so be mindful of that.
37
- #
38
- # Additionally, while recovery of furnish's state is something it will do for
39
- # you, managing recovery inside your provisioner (e.g., ensuring that EC2
40
- # instance really did come up after the program died in the middle of waiting
41
- # for it) is your job. Everything will be brought up as it was and
42
- # provisioning will be restarted. Account for that.
43
- #
6
+ # For information on writing a provisioner, see Furnish::Provisioner::API.
44
7
  module Provisioner
45
8
  end
46
9
  end
@@ -1,6 +1,6 @@
1
1
  require 'delegate'
2
2
  require 'furnish/logger'
3
- require 'furnish/provisioner'
3
+ require 'furnish/provisioners/api'
4
4
 
5
5
  module Furnish
6
6
  #
@@ -28,6 +28,8 @@ module Furnish
28
28
  attr_reader :name
29
29
  # The list of names the group depends on.
30
30
  attr_reader :dependencies
31
+ # group state object. should not be used outside of internals.
32
+ attr_reader :group_state
31
33
 
32
34
  #
33
35
  # Create a new Provisioner group.
@@ -35,24 +37,37 @@ module Furnish
35
37
  # * provisioners can be an array of provisioner objects or a single item
36
38
  # (which will be boxed). This is what the array consists of that this
37
39
  # object is.
38
- # * name is a string. always.
40
+ # * furnish_group_name is a string. always.
39
41
  # * dependencies can either be passed as an Array or Set, and will be
40
42
  # converted to a Set if they are not a Set.
41
43
  #
42
- def initialize(provisioners, name, dependencies=[])
44
+ # See #assert_provisioner_protocol, Furnish::Protocol, and
45
+ # Furnish::Provisioner::API for information on how a set of provisioner
46
+ # objects will be validated during the construction of the group.
47
+ #
48
+ def initialize(provisioners, furnish_group_name, dependencies=[])
49
+ @group_state = Palsy::Map.new('vm_group_state', furnish_group_name)
50
+
43
51
  #
44
52
  # FIXME maybe move the naming construct to here instead of populating it
45
53
  # out to the provisioners
46
54
  #
47
55
 
48
- provisioners = [provisioners] unless provisioners.kind_of?(Array)
49
- provisioners.each do |p|
50
- p.name = name
56
+ provisioners = [provisioners].compact unless provisioners.kind_of?(Array)
57
+
58
+ if provisioners.empty?
59
+ raise ArgumentError, "A non-empty list of provisioners must be provided"
51
60
  end
52
61
 
53
- @name = name
62
+ provisioners.each do |prov|
63
+ prov.furnish_group_name = furnish_group_name
64
+ end
65
+
66
+ @name = furnish_group_name
54
67
  @dependencies = dependencies.kind_of?(Set) ? dependencies : Set[*dependencies]
55
68
 
69
+ assert_provisioner_protocol(provisioners)
70
+
56
71
  super(provisioners)
57
72
  end
58
73
 
@@ -60,78 +75,279 @@ module Furnish
60
75
  # Provision this group.
61
76
  #
62
77
  # Initial arguments go to the first provisioner's startup method, and then
63
- # the return values, if truthy, get passed to the next provisioner's
64
- # startup method. Any falsey value causes a RuntimeError to be raised and
65
- # provisioning halts, effectively creating a chain of responsibility
66
- # pattern.
78
+ # the return values, if a Hash, get merged with what was passed, and then
79
+ # the result is passed to the next provisioner's startup method. Any falsey
80
+ # value causes a RuntimeError to be raised and provisioning halts,
81
+ # effectively creating a chain of responsibility pattern.
67
82
  #
68
83
  # If a block is provided, will yield self to it for each step through the
69
84
  # group.
70
85
  #
71
- def startup(*args)
72
- each do |this_prov|
73
- unless args = this_prov.startup(args)
86
+ def startup(args={ })
87
+ @group_state['action'] = :startup
88
+
89
+ each_with_index do |this_prov, i|
90
+ next unless check_recovery(this_prov, i)
91
+ set_recovery(this_prov, i, args)
92
+
93
+ startup_args = args
94
+
95
+ unless args = this_prov.startup(startup_args)
74
96
  if_debug do
75
- puts "Could not provision #{this_prov.name} with provisioner #{this_prov.class.name}"
97
+ puts "Could not provision #{this_prov}"
76
98
  end
77
99
 
78
- raise "Could not provision #{this_prov.name} with provisioner #{this_prov.class.name}"
100
+ set_recovery(this_prov, i, startup_args)
101
+ raise "Could not provision #{this_prov}"
102
+ end
103
+
104
+ unless args.kind_of?(Hash)
105
+ set_recovery(this_prov, i, startup_args)
106
+ raise ArgumentError,
107
+ "#{this_prov.class} does not return data that can be consumed by the next provisioner"
79
108
  end
80
109
 
110
+ args = startup_args.merge(args)
111
+
81
112
  yield self if block_given?
82
113
  end
83
114
 
115
+ clean_state
116
+
84
117
  return true
85
118
  end
86
119
 
87
120
  #
88
121
  # Deprovision this group.
89
122
  #
90
- # Provisioners are run in reverse order against the shutdown method. No
91
- # arguments are seeded as in Furnish::ProvisionerGroup#startup. Raise
92
- # semantics are the same as with Furnish::ProvisionerGroup#startup.
123
+ # Provisioners are run in reverse order against the shutdown method.
124
+ # Argument handling semantics are exactly the same as #startup.
93
125
  #
94
- # If a true argument is passed to this method, the raise semantics will be
95
- # ignored (but still logged), allowing all the provisioners to run their
96
- # shutdown routines. See Furnish::Scheduler#force_deprovision for
97
- # information on how to use this externally.
126
+ # If a true argument is passed to this method as the second argument, the
127
+ # raise semantics will be ignored (but still logged), allowing all the
128
+ # provisioners to run their shutdown routines. See
129
+ # Furnish::Scheduler#force_deprovision for information on how to use this
130
+ # externally.
98
131
  #
99
- def shutdown(force=false)
100
- reverse.each do |this_prov|
101
- success = false
132
+ def shutdown(args={ }, force=false)
133
+ @group_state['action'] = :shutdown
134
+
135
+ reverse.each_with_index do |this_prov, i|
136
+ next unless check_recovery(this_prov, i)
137
+ set_recovery(this_prov, i, args)
138
+
139
+ shutdown_args = args
102
140
 
103
141
  begin
104
- success = perform_deprovision(this_prov) || force
142
+ args = perform_deprovision(this_prov, shutdown_args)
105
143
  rescue Exception => e
106
- if force
107
- if_debug do
108
- puts "Deprovision #{this_prov.class.name}/#{this_prov.name} had errors:"
109
- puts "#{e.message}"
110
- end
111
- else
144
+ if_debug do
145
+ puts "Deprovision of #{this_prov} had errors:"
146
+ puts "#{e.message}"
147
+ end
148
+
149
+ unless force
150
+ set_recovery(this_prov, i, shutdown_args)
112
151
  raise e
113
152
  end
114
153
  end
115
154
 
116
- unless success or force
117
- raise "Could not deprovision #{this_prov.name}/#{this_prov.class.name}"
155
+ unless args or force
156
+ set_recovery(this_prov, i, shutdown_args)
157
+ raise "Could not deprovision #{this_prov}"
158
+ end
159
+
160
+ unless args.kind_of?(Hash) or force
161
+ set_recovery(this_prov, i, startup_args)
162
+ raise ArgumentError,
163
+ "#{this_prov.class} does not return data that can be consumed by the next provisioner"
164
+ end
165
+
166
+ args = shutdown_args.merge(args || { })
167
+ end
168
+
169
+ clean_state
170
+
171
+ return true
172
+ end
173
+
174
+ #
175
+ # Initiate recovery for this group. Reading
176
+ # Furnish::Provisioner::API#recover is essential for this documentation.
177
+ #
178
+ # This method should not be used directly -- see
179
+ # Furnish::Scheduler#recover.
180
+ #
181
+ # #startup and #shutdown track various bits of information about state as
182
+ # they run provisioners. #recover uses this information to find out where
183
+ # things stopped, and executes a Furnish::Provisioner::API#recover method
184
+ # with the action and last parameters supplied. If the result of the
185
+ # recovery is true, it then attempts to finish the provisioning process by
186
+ # starting with the action that failed the last time (the same provisioner
187
+ # the recover method was called on).
188
+ #
189
+ # #recover will return nil if it can't actually recover anything because it
190
+ # doesn't have enough information. It will also make no attempt to recover
191
+ # (and fail by returning false) if the provisioner does not allow recovery
192
+ # (see Furnish::Provisioner::API.allows_recovery?).
193
+ #
194
+ # If you pass a truthy argument, it will pass this on to #shutdown if the
195
+ # action is required -- this is required for forced deprovisioning and is
196
+ # dealt with by Furnish::Scheduler.
197
+ #
198
+ def recover(force_deprovision=false)
199
+ index = @group_state['index']
200
+ action = @group_state['action']
201
+ provisioner = @group_state['provisioner']
202
+ provisioner_args = @group_state['provisioner_args']
203
+
204
+ return nil unless action and provisioner and index
205
+
206
+ result = false
207
+
208
+ #
209
+ # The next few lines here work around mutable state needing to happen in
210
+ # the original provisioner, but since the one we looked up will actually
211
+ # not be the same object, we need to deal with that by dispatching
212
+ # recovery to the actual provisioner object in the group.
213
+ #
214
+ # The one stored is still useful for informational and validation
215
+ # purposes, but the index is the ultimate authority.
216
+ #
217
+ offset = case action
218
+ when :startup
219
+ index
220
+ when :shutdown
221
+ size - 1 - index
222
+ else
223
+ raise "Wtf?"
224
+ end
225
+
226
+ orig_prov = self[offset]
227
+
228
+ unless orig_prov.class == provisioner.class
229
+ raise "index and provisioner data don't seem to agree"
230
+ end
231
+
232
+ if orig_prov.class.respond_to?(:allows_recovery?) and orig_prov.class.allows_recovery?
233
+ if orig_prov.recover(action, provisioner_args)
234
+ @start_index = index
235
+ @start_provisioner = orig_prov
236
+
237
+ result = case action
238
+ when :startup
239
+ startup(provisioner_args)
240
+ when :shutdown
241
+ shutdown(provisioner_args, force_deprovision)
242
+ else
243
+ raise "Wtf?"
244
+ end
118
245
  end
119
246
  end
247
+
248
+ @start_index, @start_provisioner = nil, nil
249
+
250
+ return result # scheduler will take it from here
120
251
  end
121
252
 
122
253
  protected
123
254
 
255
+ #
256
+ # Similar to #clean_state, a helper for recovery tracking
257
+ #
258
+
259
+ def set_recovery(prov, index, args=nil)
260
+ @group_state['provisioner'] = prov
261
+ @group_state['index'] = index
262
+ @group_state['provisioner_args'] = args
263
+ end
264
+
265
+ #
266
+ # Returns false if this provision is to be skipped, controlled by #recover.
267
+ #
268
+ # Raises if something really goes wrong.
269
+ #
270
+ def check_recovery(prov, index)
271
+ if @start_index and @start_provisioner
272
+ if @start_index == index
273
+ unless @start_provisioner.class == prov.class
274
+ raise "Provisioner state during recovery is incorrect - something is very wrong"
275
+ end
276
+ end
277
+
278
+ return index >= @start_index
279
+ end
280
+
281
+ return true
282
+ end
283
+
284
+ #
285
+ # cleanup the group state after a group operation.
286
+ #
287
+ def clean_state
288
+ @group_state.delete('index')
289
+ @group_state.delete('action')
290
+ @group_state.delete('provisioner')
291
+ @group_state.delete('provisioner_args')
292
+ end
293
+
124
294
  #
125
295
  # Just a way to simplify the deprovisioning logic with some generic logging.
126
296
  #
127
- def perform_deprovision(this_prov)
128
- result = this_prov.shutdown
297
+ def perform_deprovision(this_prov, args)
298
+ result = this_prov.shutdown(args)
129
299
  unless result
130
300
  if_debug do
131
- puts "Could not deprovision group #{this_prov.name}."
301
+ puts "Could not deprovision group #{this_prov}."
132
302
  end
133
303
  end
134
304
  return result
135
305
  end
306
+
307
+ #
308
+ # Asserts that all the provisioners can communicate with each other.
309
+ #
310
+ # This leverages the Furnish::Protocol#requires_from and
311
+ # Furnish::Protocol#accepts_from assertions and raises if they return
312
+ # false. Any previous provisioner in the chain may yield something that the
313
+ # current accepting provisioner can require or accept. See the merge
314
+ # semantics in #startup and #shutdown for more information.
315
+ #
316
+ def assert_provisioner_protocol(provisioners)
317
+ assert_ordered_protocol(provisioners.dup, :startup_protocol)
318
+ assert_ordered_protocol(provisioners.reverse, :shutdown_protocol)
319
+ end
320
+
321
+ #
322
+ # This carries out the logic in #assert_provisioner_protocol, catering
323
+ # towards which protocol we're validating.
324
+ #
325
+ def assert_ordered_protocol(iterator, protocol_method)
326
+ yielders = [iterator.shift.class]
327
+
328
+ while accepting = iterator.shift
329
+ accepting = accepting.class # meh
330
+
331
+ unless yielders.all? { |y| y.respond_to?(protocol_method) }
332
+ raise ArgumentError, "yielding classes do not implement protocol #{protocol_method} -- cannot continue"
333
+ end
334
+
335
+ unless accepting.respond_to?(protocol_method)
336
+ raise ArgumentError, "accepting class #{accepting} does not implement protocol #{protocol_method} -- cannot continue"
337
+ end
338
+
339
+ a_proto = accepting.send(protocol_method)
340
+
341
+ unless yielders.any? { |y| a_proto.requires_from(y.send(protocol_method)) }
342
+ raise ArgumentError, "#{accepting} requires information specified by #{protocol_method} that yielding classes do not yield"
343
+ end
344
+
345
+ unless yielders.any? { |y| a_proto.accepts_from(y.send(protocol_method)) }
346
+ raise ArgumentError, "#{accepting} expects information specified by #{protocol_method} that yielding classes will not deliver"
347
+ end
348
+
349
+ yielders.push(accepting)
350
+ end
351
+ end
136
352
  end
137
353
  end