furnish 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,430 @@
1
+ require 'furnish/provisioner'
2
+ require 'furnish/protocol'
3
+
4
+ module Furnish # :nodoc:
5
+ module Provisioner # :nodoc:
6
+ #
7
+ # API base class for furnish provisioners. Ideally, you will want to inherit
8
+ # from this and supply your overrides. Nothing in furnish expects this
9
+ # class to be the base class of your provisioners, this just lets you save
10
+ # some trouble. That said, all methods in this class are expected to be
11
+ # implemented by your provisioner if you choose not to use it.
12
+ #
13
+ # The method documentation here also declares expectations on how
14
+ # provisioners are expected to operate, so even if you don't use this
15
+ # class, reading the documentation and knowing what's expected are
16
+ # essential to writing a working provisioner.
17
+ #
18
+ # Note that all Provisioners *must* be capable of being marshalled with
19
+ # Ruby's Marshal library. If they are unable to do this, many parts of
20
+ # Furnish will not work. There are a few things that break Marshal, such as
21
+ # proc/lambda in instance variables, usage of the Singleton module, and
22
+ # singleton class manipulation. Marshal has tooling to work around this if
23
+ # you absolutely need it; see Marshal's documentation for more information.
24
+ # Additionally, There are less important (but worth knowing) performance
25
+ # complications around extend and refinements that will not break anything,
26
+ # but should be noted as well.
27
+ #
28
+ # This class provides some basic boilerplate for:
29
+ #
30
+ # * initializer/constructor usage (see API.new)
31
+ # * property management / querying (see API.furnish_property)
32
+ # * #furnish_group_name (see Furnish::ProvisionerGroup) usage
33
+ # * Implementations of Furnish::Protocol for #startup and #shutdown (see
34
+ # API.configure_startup and API.configure_shutdown)
35
+ # * standard #report output
36
+ # * #to_s and #inspect for various ruby functions
37
+ #
38
+ # Additionally, "abstract" methods have been defined for provisioner
39
+ # control methods:
40
+ #
41
+ # * #startup
42
+ # * #shutdown
43
+ #
44
+ # Which will raise unless implemented by your subclass.
45
+ #
46
+ # Both parameters and return values are expected to be normal for these
47
+ # methods. Return base types are in parens next to the method name. Please
48
+ # see the methods themselves for parameter information.
49
+ #
50
+ # * #startup (Hash or false)
51
+ # * #shutdown (Hash or false)
52
+ # * #report
53
+ #
54
+ # And it would be wise to read the documentation on how those should be
55
+ # written.
56
+ #
57
+ class API
58
+
59
+ #
60
+ # Ensures all properties are inherited by subclasses that inherit from API.
61
+ #
62
+ def self.inherited(inheriting)
63
+ [
64
+ :@furnish_properties,
65
+ :@startup_protocol,
66
+ :@shutdown_protocol
67
+ ].each do |prop|
68
+ inheriting.instance_variable_set(prop, (instance_variable_get(prop).dup rescue nil))
69
+ end
70
+
71
+ inheriting.instance_variable_set(:@allows_recovery, instance_variable_get(:@allows_recovery))
72
+ end
73
+
74
+ ##
75
+ # The set of furnish properties for this class. Returns a hash which is
76
+ # keyed with symbols representing the property name, and the value itself
77
+ # is a hash which contains two additional key/value combinations, `:type`
78
+ # and `:description`, e.g.:
79
+ #
80
+ # {
81
+ # :my_property => {
82
+ # :description => "some description",
83
+ # :type => Object
84
+ # }
85
+ # }
86
+ #
87
+ # See API.furnish_property for more information.
88
+ #
89
+ def self.furnish_properties
90
+ @furnish_properties ||= { }
91
+ end
92
+
93
+ #
94
+ # Configure a furnish property. Used by the standard initializer on
95
+ # API.new for parameter validation, and provides a queryable interface
96
+ # for external consumers via API.furnish_properties, which will be
97
+ # exposed as a class method for your provisioner class.
98
+ #
99
+ # The name is a symbol or will be converted to one, and will generate an
100
+ # accessor for instances of this class. No attempt is made to type check
101
+ # accessor writes outside of the constructor.
102
+ #
103
+ # The description is a string which describes what the property controls.
104
+ # It is unused by Furnish but exists to allow external consumers the
105
+ # ability to expose this to third parties. The default is an empty
106
+ # string.
107
+ #
108
+ # The type is a class name (default Object) used for parameter checking
109
+ # in API.new's initializer. If the value provided during construction is
110
+ # not a kind of this class, an ArgumentError will be raised. No attempt
111
+ # is made to deal with inner collection types ala Generics.
112
+ #
113
+ # Example:
114
+ #
115
+ # class MyProv < API
116
+ # furnish_property :foo, "does a foo", Integer
117
+ # end
118
+ #
119
+ # obj = MyProv.new(:bar => 1) # raises, no property
120
+ # obj = MyProv.new(:foo => "string") # raises, invalid type
121
+ # obj = MyProv.new(:foo => 1) # succeeds
122
+ #
123
+ # obj.foo == 1
124
+ #
125
+ # MyProv.furnish_properties[:foo] ==
126
+ # { :description => "does a foo", :type => Integer }
127
+ #
128
+ def self.furnish_property(name, description="", type=Object)
129
+ name = name.to_sym unless name.kind_of?(Symbol)
130
+
131
+ attr_accessor name
132
+
133
+ furnish_properties[name] = {
134
+ :description => description,
135
+ :type => type
136
+ }
137
+ end
138
+
139
+ #
140
+ # Indicate whether or not this Provisioner allows recovery functions.
141
+ # Will be used in recovery mode to determine whether or not to
142
+ # automatically deprovision the group, or attempt to recover the group
143
+ # provision.
144
+ #
145
+ # Recovery (the feature) is defaulted to false, but calling this method
146
+ # with no arguments will turn it on (i.e., set it to true). You may also
147
+ # provide a boolean argument if you wish to turn it off.
148
+ #
149
+ # Note that if you turn this on, you must also define a #recover state
150
+ # method which implements your recovery routines. If you turn it on and
151
+ # do not define the #recover routine, NotImplementedError will be raised
152
+ # during recovery.
153
+ #
154
+ # Usage:
155
+ #
156
+ # # turn it on
157
+ # class MyProv < API
158
+ # allows_recovery
159
+ # end
160
+ #
161
+ # # turn it off explicitly
162
+ # class MyProv < API
163
+ # allows_recovery false
164
+ # end
165
+ #
166
+ # # not specifying means it's off.
167
+ # class MyProv < API
168
+ # ...
169
+ # end
170
+ #
171
+ def self.allows_recovery(val=true)
172
+ @allows_recovery = val
173
+ end
174
+
175
+ #
176
+ # Predicate to determine if this provisioner supports recovery or not.
177
+ #
178
+ # Please see API.allows_recovery and #recover for more information.
179
+ #
180
+ def self.allows_recovery?
181
+ @allows_recovery ||= false
182
+ @allows_recovery
183
+ end
184
+
185
+ #
186
+ # This contains the Furnish::Protocol configuration for startup (aka
187
+ # provisioning) state execution. See API.configure_startup for more
188
+ # information.
189
+ #
190
+ def self.startup_protocol
191
+ @startup_protocol ||= Furnish::Protocol.new
192
+ end
193
+
194
+ #
195
+ # This contains the Furnish::Protocol configuration for shutdown (aka
196
+ # provisioning) state execution. See API.configure_shutdown for more
197
+ # information.
198
+ #
199
+ def self.shutdown_protocol
200
+ @shutdown_protocol ||= Furnish::Protocol.new
201
+ end
202
+
203
+ #
204
+ # configure the Furnish::Protocol for startup state execution. This
205
+ # allows you to define constraints for your provisioner that are used at
206
+ # scheduling time to determine whether or not the ProvisionerGroup will
207
+ # be able to finish its provision.
208
+ #
209
+ # It's a bit like run-time type inference for a full provision; it just
210
+ # tries to figure out if it'll break before it runs.
211
+ #
212
+ # The block provided will be instance_eval'd over a Furnish::Protocol
213
+ # object. You can use methods like Furnish::Protocol#accepts_from_any,
214
+ # Furnish::Protocol#requires, Furnish::Protocol#accepts,
215
+ # Furnish::Protocol#yields to describe what it'll pass on to the next
216
+ # provisioner or expects from the one coming before it.
217
+ #
218
+ # Example:
219
+ #
220
+ # class MyProv < API
221
+ # configure_startup do
222
+ # requires :ip_address, "the IP address returned by the last provision", String
223
+ # accepts :network, "A CIDR network used by the last provision", String
224
+ # yields :port, "A TCP port for the service allocated by this provisioner", Integer
225
+ # end
226
+ # end
227
+ #
228
+ # This means:
229
+ #
230
+ # * An IP address has to come from the previous provisioner named "ip_address".
231
+ # * If a network CIDR was supplied, it will be used.
232
+ # * This provision will provide a TCP port number for whatever it makes,
233
+ # which the next provisioner can work with.
234
+ #
235
+ # See Furnish::Protocol for more information.
236
+ #
237
+ def self.configure_startup(&block)
238
+ startup_protocol.configure(&block)
239
+ end
240
+
241
+ #
242
+ # This is exactly like API.configure_startup, but for the shutdown
243
+ # process. Do note that shutdown runs the provisioners backwards, meaning
244
+ # the validation process also follows suit.
245
+ #
246
+ def self.configure_shutdown(&block)
247
+ shutdown_protocol.configure(&block)
248
+ end
249
+
250
+ ##
251
+ # The furnish_group_name is set by Furnish::ProvisionerGroup when
252
+ # scheduling is requested via Furnish::Scheduler. It is a hint to the
253
+ # provisioner as to what the name of the group it's in is, which can be
254
+ # used to persist data, name things in a unique way, etc.
255
+ #
256
+ # Note that furnish_group_name will be nil until the
257
+ # Furnish::ProvisionerGroup is constructed. This means that it will not
258
+ # be set when #initialize runs, or any time before it joins the group. It
259
+ # is wise to only rely on this value being set when #startup or #shutdown
260
+ # execute.
261
+ attr_accessor :furnish_group_name
262
+
263
+ #
264
+ # Default constructor. If given arguments, must be of type Hash, keys are
265
+ # the name of furnish properties. Raises ArgumentError if no furnish
266
+ # property exists, or the type of the value provided is not a kind of
267
+ # type specified in the property. See API.furnish_property for more
268
+ # information.
269
+ #
270
+ # Does nothing more, not required anywhere in furnish itself -- you may
271
+ # redefine this constructor and work against completely differently input
272
+ # and behavior, or call this as a superclass initializer and then do your
273
+ # work.
274
+ #
275
+ def initialize(args={})
276
+ unless args.kind_of?(Hash)
277
+ raise ArgumentError, "Arguments must be a kind of hash"
278
+ end
279
+
280
+ args.each do |k, v|
281
+ props = self.class.furnish_properties
282
+
283
+ if props.has_key?(k)
284
+ if v.kind_of?(props[k][:type])
285
+ send("#{k}=", v)
286
+ else
287
+ raise ArgumentError, "Value for furnish property #{k} on #{self.class.name} does not match type #{props[k][:type]}"
288
+ end
289
+ else
290
+ raise ArgumentError, "Invalid argument #{k}, not a furnish property for #{self.class.name}"
291
+ end
292
+ end
293
+ end
294
+
295
+ #
296
+ # Called by Furnish::ProvisionerGroup#startup which is itself called by
297
+ # Furnish::Scheduler#startup. Indicates the resource this provisioner
298
+ # manages is to be created.
299
+ #
300
+ # Arguments will come from the return values of the previous
301
+ # provisioner's startup or an empty Hash if this is the first
302
+ # provisioner. Return value is expected to be false if the provision
303
+ # failed in a non-exceptional way, or a hash of values for the next
304
+ # provisioner if successful. See API.startup_protocol for how you can
305
+ # validate what you accept and yield for parameters will always work.
306
+ #
307
+ # The routine in this base class will raise NotImplementedError,
308
+ # expecting you to override it in your provisioner.
309
+ #
310
+ def startup(args={})
311
+ raise NotImplementedError, "startup method not implemented for #{self.class.name}"
312
+ end
313
+
314
+ #
315
+ # called by Furnish::ProvisionerGroup#shutdown which is itself called by
316
+ # Furnish::Scheduler#shutdown. Indicates the resource this provisioner
317
+ # manages is to be destroyed.
318
+ #
319
+ # Arguments are exactly the same as #startup. Note that provisioners run
320
+ # in reverse order when executing shutdown methods. See
321
+ # API.shutdown_protocol for information on how to validate incoming
322
+ # parameters and how to declare what you yield.
323
+ #
324
+ # The routine in this base class will raise NotImplementedError,
325
+ # expecting you to override it in your provisioner.
326
+ #
327
+ def shutdown(args={})
328
+ raise NotImplementedError, "shutdown method not implemented for #{self.class.name}"
329
+ end
330
+
331
+ #
332
+ # Initiate recovery. This is an optional state transition, and if
333
+ # unavailable will assume recovery is not possible. Inheriting from this
334
+ # class provides this and therefore will always be available. See
335
+ # API.allows_recovery for information on how to control this feature in
336
+ # your provisioner. If recovery is possible in your provisioner and you
337
+ # have not defined a working recover method of your own,
338
+ # NotImplementedError will be raised.
339
+ #
340
+ # recover takes two arguments, the desired state and the arguments passed
341
+ # during the initial attempt at state transition: for example, `:startup`
342
+ # and a hash that conforms to Furnish::Protocol definitions.
343
+ #
344
+ # recover is expected to return true if recovery was successful, and
345
+ # false if it was not. If successful, the original state will be invoked
346
+ # with its original arguments, just like it was receiving the transition
347
+ # for the first time. Therefore, for recover to be successful, it should
348
+ # clean up any work the state has already done.
349
+ #
350
+ # See Furnish::Scheduler#recover for information on how to deal with
351
+ # recovery that will not succeed.
352
+ #
353
+ # Example: a provisioner for a security group crashes during running
354
+ # startup. The scheduler marks that group as needing recovery, and marks
355
+ # the scheduler in general as needing recovery.
356
+ # Furnish::Scheduler#recover is called, which locates our group and calls
357
+ # its recover routine, which delegates to your provisioner. Your
358
+ # provisioner then cleans up the security group attempted to be created
359
+ # (if it does not exist, that's fine too). Then, it returns true. Then
360
+ # the scheduler will retry the startup routine for your security group
361
+ # provisioner, which will attempt the same thing as if it has never tried
362
+ # to begin with.
363
+ #
364
+ def recover(state, args)
365
+ if self.class.allows_recovery?
366
+ raise NotImplementedError, "#{self.class} allows recovery but no #recover method was defined."
367
+ else
368
+ return false
369
+ end
370
+ end
371
+
372
+ #
373
+ # returns an array of strings with some high-level information about the
374
+ # provision. Intended for UI tools that need to query a provisioner group
375
+ # about the resources it manages.
376
+ #
377
+ # Default is to return the provisioner group name or "unknown" if it is
378
+ # not set yet.
379
+ #
380
+ def report
381
+ [furnish_group_name || "unknown"]
382
+ end
383
+
384
+ #
385
+ # Used by various logging pieces throughout furnish and Ruby itself.
386
+ #
387
+ # Default is to return "group name[provisioner class name]" where group
388
+ # name will be "unknown" if not set.
389
+ #
390
+ # For example, in a group called 'test1', and the provisioner class is
391
+ # Furnish::Provisioner::EC2:
392
+ #
393
+ # "test1[Furnish::Provisioner::EC2]"
394
+ #
395
+ def to_s
396
+ name = furnish_group_name || "unknown"
397
+ "#{name}[#{self.class.name}]"
398
+ end
399
+
400
+ alias inspect to_s # FIXME make this less stupid later
401
+
402
+ #
403
+ # Assists with equality checks. See #hash for how this is done.
404
+ #
405
+ def ==(other)
406
+ self.hash == other.hash
407
+ end
408
+
409
+ #
410
+ # Computes something suitable for storing in hash tables, also used for
411
+ # equality (see #==).
412
+ #
413
+ # Accomplishes this by collecting all instance variables in an array,
414
+ # converting strings to a common encoding if necessary. Then, appends the
415
+ # class and Marshals the contents of the array.
416
+ #
417
+ def hash
418
+ Marshal.dump(
419
+ instance_variables.sort.map do |x|
420
+ y = instance_variable_get(x)
421
+ y.kind_of?(String) ?
422
+ y.encode("UTF-8", :invalid => :replace, :replace => "?".chr) :
423
+ y
424
+ end +
425
+ [self.class]
426
+ )
427
+ end
428
+ end
429
+ end
430
+ end
@@ -7,8 +7,7 @@ module Furnish
7
7
  # In short, unless you're writing tests you should probably never use this
8
8
  # code.
9
9
  #
10
- class Dummy
11
-
10
+ class Dummy < API
12
11
  #--
13
12
  # Some dancing around the marshal issues with this provisioner. Note that
14
13
  # after restoration, any delegates you set will no longer exist, so
@@ -17,11 +16,11 @@ module Furnish
17
16
 
18
17
  # basic Palsy::Object store for stuffing random stuff
19
18
  attr_reader :store
19
+
20
20
  # order tracking via Palsy::List, delegation makes a breadcrumb here
21
21
  # that's ordered between all provisioners.
22
22
  attr_reader :order
23
- # name of the provisioner according to the API
24
- attr_accessor :name
23
+
25
24
  # arbitrary identifier for Dummy#call_order
26
25
  attr_accessor :id
27
26
 
@@ -33,12 +32,20 @@ module Furnish
33
32
  @order = Palsy::List.new('dummy_order', 'shared')
34
33
  end
35
34
 
35
+ #
36
+ # used in state transitions to capture run information
37
+ #
38
+ def run_state
39
+ @run_state ||= Palsy::Map.new('dummy_run_state', [self.class.name, respond_to?(:furnish_group_name) ? furnish_group_name : name].join("-"))
40
+ end
41
+
36
42
  #
37
43
  # call order is ordering on a per-provisioner group basis, and is used to
38
44
  # validate that groups do indeed execute in the proper order.
39
45
  #
40
46
  def call_order
41
- @call_order ||= Palsy::List.new('dummy_order', name)
47
+ # respond_to? here is to assist with deprecation tests
48
+ @call_order ||= Palsy::List.new('dummy_order', respond_to?(:furnish_group_name) ? furnish_group_name : name)
42
49
  end
43
50
 
44
51
  #
@@ -46,26 +53,26 @@ module Furnish
46
53
  #
47
54
  def report
48
55
  do_delegate(__method__) do
49
- [name, @persist]
56
+ [furnish_group_name, @persist]
50
57
  end
51
58
  end
52
59
 
53
60
  #
54
61
  # startup shim
55
62
  #
56
- def startup(*args)
63
+ def startup(args={ })
57
64
  @persist = "floop"
58
65
  do_delegate(__method__) do
59
- true
66
+ run_state[__method__] = args
60
67
  end
61
68
  end
62
69
 
63
70
  #
64
71
  # shutdown shim
65
72
  #
66
- def shutdown
73
+ def shutdown(args={ })
67
74
  do_delegate(__method__) do
68
- true
75
+ run_state[__method__] = args
69
76
  end
70
77
  end
71
78
 
@@ -77,8 +84,8 @@ module Furnish
77
84
  meth_name = meth_name.to_s
78
85
 
79
86
  # indicate we actually did something
80
- @store[ [name, meth_name].join("-") ] = Time.now.to_i
81
- @order.push(name)
87
+ @store[ [furnish_group_name, meth_name].join("-") ] = Time.now.to_i
88
+ @order.push(furnish_group_name)
82
89
  call_order.push(id || "unknown")
83
90
 
84
91
  yield