Class: Furnish::ProvisionerGroup

Inherits:
Array
  • Object
show all
Includes:
Logger::Mixins
Defined in:
lib/furnish/provisioner_group.rb

Overview

A provisioner group is an array of provisioners. See Furnish::Provisioner for what the Provisioner API looks like.

A group has a set of provisioner objects, a name for the group, and a list of names that count as dependencies. It has methods to operate on the group as a unit, starting them up as a unit and shutting them down. It is primarily operated on by Furnish::Scheduler.

In general, you interact with this class via Furnish::Scheduler#schedule_provision, but you can also construct groups yourself and deal with them via Furnish::Scheduler#schedule_provisioner_group.

It delegates to Array and can be treated like one via the semantics of Ruby’s DelegateClass.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Logger::Mixins

#if_debug

Constructor Details

#initialize(provisioners, furnish_group_name, dependencies = []) ⇒ ProvisionerGroup

Create a new Provisioner group.

  • provisioners can be an array of provisioner objects or a single item (which will be boxed). This is what the array consists of that this object is.

  • furnish_group_name is a string. always.

  • dependencies can either be passed as an Array or Set, and will be converted to a Set if they are not a Set.

See #assert_provisioner_protocol, Furnish::Protocol, and Furnish::Provisioner::API for information on how a set of provisioner objects will be validated during the construction of the group.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/furnish/provisioner_group.rb', line 48

def initialize(provisioners, furnish_group_name, dependencies=[])
  @group_state = Palsy::Map.new('vm_group_state', furnish_group_name)

  #
  # FIXME maybe move the naming construct to here instead of populating it
  #       out to the provisioners
  #

  provisioners = [provisioners].compact unless provisioners.kind_of?(Array)

  if provisioners.empty?
    raise ArgumentError, "A non-empty list of provisioners must be provided"
  end

  provisioners.each do |prov|
    prov.furnish_group_name = furnish_group_name
  end

  @name         = furnish_group_name
  @dependencies = dependencies.kind_of?(Set) ? dependencies : Set[*dependencies]

  assert_provisioner_protocol(provisioners)

  super(provisioners)
end

Instance Attribute Details

#dependenciesObject (readonly)

The list of names the group depends on.



30
31
32
# File 'lib/furnish/provisioner_group.rb', line 30

def dependencies
  @dependencies
end

#group_stateObject (readonly)

group state object. should not be used outside of internals.



32
33
34
# File 'lib/furnish/provisioner_group.rb', line 32

def group_state
  @group_state
end

#nameObject (readonly)

The name of the group.



28
29
30
# File 'lib/furnish/provisioner_group.rb', line 28

def name
  @name
end

Instance Method Details

#recover(force_deprovision = false) ⇒ Object

Initiate recovery for this group. Reading Furnish::Provisioner::API#recover is essential for this documentation.

This method should not be used directly – see Furnish::Scheduler#recover.

#startup and #shutdown track various bits of information about state as they run provisioners. #recover uses this information to find out where things stopped, and executes a Furnish::Provisioner::API#recover method with the action and last parameters supplied. If the result of the recovery is true, it then attempts to finish the provisioning process by starting with the action that failed the last time (the same provisioner the recover method was called on).

#recover will return nil if it can’t actually recover anything because it doesn’t have enough information. It will also make no attempt to recover (and fail by returning false) if the provisioner does not allow recovery (see Furnish::Provisioner::API.allows_recovery?).

If you pass a truthy argument, it will pass this on to #shutdown if the action is required – this is required for forced deprovisioning and is dealt with by Furnish::Scheduler.



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/furnish/provisioner_group.rb', line 198

def recover(force_deprovision=false)
  index             = @group_state['index']
  action            = @group_state['action']
  provisioner       = @group_state['provisioner']
  provisioner_args  = @group_state['provisioner_args']

  return nil unless action and provisioner and index

  result = false

  #
  # The next few lines here work around mutable state needing to happen in
  # the original provisioner, but since the one we looked up will actually
  # not be the same object, we need to deal with that by dispatching
  # recovery to the actual provisioner object in the group.
  #
  # The one stored is still useful for informational and validation
  # purposes, but the index is the ultimate authority.
  #
  offset = case action
           when :startup
             index
           when :shutdown
             size - 1 - index
           else
             raise "Wtf?"
           end

  orig_prov = self[offset]

  unless orig_prov.class == provisioner.class
    raise "index and provisioner data don't seem to agree"
  end

  if orig_prov.class.respond_to?(:allows_recovery?) and orig_prov.class.allows_recovery?
    if orig_prov.recover(action, provisioner_args)
      @start_index        = index
      @start_provisioner  = orig_prov

      result = case action
               when :startup
                 startup(provisioner_args)
               when :shutdown
                 shutdown(provisioner_args, force_deprovision)
               else
                 raise "Wtf?"
               end
    end
  end

  @start_index, @start_provisioner = nil, nil

  return result # scheduler will take it from here
end

#shutdown(args = { }, force = false) ⇒ Object

Deprovision this group.

Provisioners are run in reverse order against the shutdown method. Argument handling semantics are exactly the same as #startup.

If a true argument is passed to this method as the second argument, the raise semantics will be ignored (but still logged), allowing all the provisioners to run their shutdown routines. See Furnish::Scheduler#force_deprovision for information on how to use this externally.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/furnish/provisioner_group.rb', line 132

def shutdown(args={ }, force=false)
  @group_state['action'] = :shutdown

  reverse.each_with_index do |this_prov, i|
    next unless check_recovery(this_prov, i)
    set_recovery(this_prov, i, args)

    shutdown_args = args

    begin
      args = perform_deprovision(this_prov, shutdown_args)
    rescue Exception => e
      if_debug do
        puts "Deprovision of #{this_prov} had errors:"
        puts "#{e.message}"
      end

      unless force
        set_recovery(this_prov, i, shutdown_args)
        raise e
      end
    end

    unless args or force
      set_recovery(this_prov, i, shutdown_args)
      raise "Could not deprovision #{this_prov}"
    end

    unless args.kind_of?(Hash) or force
      set_recovery(this_prov, i, startup_args)
      raise ArgumentError,
        "#{this_prov.class} does not return data that can be consumed by the next provisioner"
    end

    args = shutdown_args.merge(args || { })
  end

  clean_state

  return true
end

#startup(args = { }) ⇒ Object

Provision this group.

Initial arguments go to the first provisioner’s startup method, and then the return values, if a Hash, get merged with what was passed, and then the result is passed to the next provisioner’s startup method. Any falsey value causes a RuntimeError to be raised and provisioning halts, effectively creating a chain of responsibility pattern.

If a block is provided, will yield self to it for each step through the group.



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/furnish/provisioner_group.rb', line 86

def startup(args={ })
  @group_state['action'] = :startup

  each_with_index do |this_prov, i|
    next unless check_recovery(this_prov, i)
    set_recovery(this_prov, i, args)

    startup_args = args

    unless args = this_prov.startup(startup_args)
      if_debug do
        puts "Could not provision #{this_prov}"
      end

      set_recovery(this_prov, i, startup_args)
      raise "Could not provision #{this_prov}"
    end

    unless args.kind_of?(Hash)
      set_recovery(this_prov, i, startup_args)
      raise ArgumentError,
        "#{this_prov.class} does not return data that can be consumed by the next provisioner"
    end

    args = startup_args.merge(args)

    yield self if block_given?
  end

  clean_state

  return true
end