furnish 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +36 -0
- data/Gemfile +0 -2
- data/Guardfile +2 -2
- data/furnish.gemspec +3 -2
- data/lib/furnish.rb +2 -4
- data/lib/furnish/protocol.rb +226 -0
- data/lib/furnish/provisioner.rb +1 -38
- data/lib/furnish/provisioner_group.rb +254 -38
- data/lib/furnish/provisioners/api.rb +430 -0
- data/lib/furnish/provisioners/dummy.rb +19 -12
- data/lib/furnish/scheduler.rb +148 -25
- data/lib/furnish/version.rb +1 -1
- data/lib/furnish/vm.rb +3 -0
- data/test/dummy_classes.rb +275 -0
- data/test/mt_cases.rb +103 -45
- data/test/test_api.rb +68 -0
- data/test/test_dummy.rb +6 -6
- data/test/test_protocol.rb +211 -0
- data/test/test_provisioner_group.rb +163 -11
- data/test/test_scheduler_basic.rb +57 -4
- data/test/test_scheduler_threaded.rb +5 -17
- metadata +18 -13
data/CHANGELOG.md
CHANGED
@@ -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
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', :
|
8
|
+
guard 'rake', :run_on_all => false, :task => 'rdoc_cov' do
|
9
9
|
watch(%r!^lib/(.*)([^/]+)\.rb!)
|
10
10
|
end
|
data/furnish.gemspec
CHANGED
@@ -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.
|
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'
|
data/lib/furnish.rb
CHANGED
@@ -4,10 +4,8 @@ require 'furnish/logger'
|
|
4
4
|
require 'furnish/scheduler'
|
5
5
|
|
6
6
|
#
|
7
|
-
# Furnish is a scheduling system
|
8
|
-
#
|
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
|
data/lib/furnish/provisioner.rb
CHANGED
@@ -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
|
-
#
|
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/
|
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
|
-
# *
|
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
|
-
|
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
|
-
|
50
|
-
|
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
|
-
|
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
|
64
|
-
#
|
65
|
-
#
|
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(
|
72
|
-
|
73
|
-
|
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
|
97
|
+
puts "Could not provision #{this_prov}"
|
76
98
|
end
|
77
99
|
|
78
|
-
|
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.
|
91
|
-
#
|
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
|
95
|
-
# ignored (but still logged), allowing all the
|
96
|
-
# shutdown routines. See
|
97
|
-
# information on how to use this
|
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
|
-
|
101
|
-
|
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
|
-
|
142
|
+
args = perform_deprovision(this_prov, shutdown_args)
|
105
143
|
rescue Exception => e
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
117
|
-
|
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
|
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
|