furnish 0.0.4 → 0.1.0

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.
@@ -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