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