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