furnish 0.0.1
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/.gitignore +19 -0
- data/Gemfile +6 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +110 -0
- data/Rakefile +31 -0
- data/furnish.gemspec +29 -0
- data/lib/furnish/logger.rb +110 -0
- data/lib/furnish/provisioner.rb +46 -0
- data/lib/furnish/provisioner_group.rb +132 -0
- data/lib/furnish/provisioners/dummy.rb +87 -0
- data/lib/furnish/scheduler.rb +410 -0
- data/lib/furnish/version.rb +4 -0
- data/lib/furnish/vm.rb +39 -0
- data/lib/furnish.rb +51 -0
- data/test/helper.rb +15 -0
- data/test/mt_cases.rb +267 -0
- data/test/test_dummy.rb +95 -0
- data/test/test_logger.rb +68 -0
- data/test/test_provisioner_group.rb +51 -0
- data/test/test_scheduler_basic.rb +33 -0
- data/test/test_scheduler_serial.rb +16 -0
- data/test/test_scheduler_threaded.rb +44 -0
- data/test/test_vm.rb +18 -0
- metadata +212 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# vim: ft=ruby
|
2
|
+
guard 'minitest' do
|
3
|
+
# with Minitest::Unit
|
4
|
+
watch(%r!^test/(.*)\/?test_(.*)\.rb!)
|
5
|
+
watch(%r!^test/(?:helper|mt_cases)\.rb!) { "test" }
|
6
|
+
end
|
7
|
+
|
8
|
+
guard 'rake', :failure_ok => true, :run_on_all => false, :task => 'rdoc_cov' do
|
9
|
+
watch(%r!^lib/(.*)([^/]+)\.rb!)
|
10
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Erik Hollensbe
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# Furnish
|
2
|
+
|
3
|
+
Furnish is a scheduler that thinks about dependencies and provisioning. It's
|
4
|
+
the core of the provisioning logic in
|
5
|
+
[chef-workflow](https://github.com/chef-workflow/chef-workflow).
|
6
|
+
|
7
|
+
Provisioners are just a pipeline of actions which raise and lower the existence
|
8
|
+
of... something. They encapsulate state, and the actions of dealing with that
|
9
|
+
state. While chef-workflow uses this for virtual machine and "cloud" things,
|
10
|
+
anything that has on and off state can be managed with Furnish.
|
11
|
+
|
12
|
+
Furnish is novel because it lets you walk away from the problem of dealing with
|
13
|
+
command pipelines and persistence, in a way that lets you deal with it
|
14
|
+
concurrently or serially without caring too much, making testing things that
|
15
|
+
use Furnish a lot easier. It has a number of guarantees it makes about this
|
16
|
+
stuff. See `Contracts` below.
|
17
|
+
|
18
|
+
Outside of that, it's just solving Dining Philosophers with Waiters and
|
19
|
+
cheating a little by knowing how MRI's thread scheduler works.
|
20
|
+
|
21
|
+
Furnish requires MRI Ruby 1.9.3 at minimum. It probably will explode violently
|
22
|
+
on a Ruby implemention that doesn't have a GVL or the I/O based coroutine
|
23
|
+
scheduler MRI has. If you don't like that, patches welcome.
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
|
27
|
+
Add this line to your application's Gemfile:
|
28
|
+
|
29
|
+
gem 'furnish'
|
30
|
+
|
31
|
+
And then execute:
|
32
|
+
|
33
|
+
$ bundle
|
34
|
+
|
35
|
+
Or install it yourself as:
|
36
|
+
|
37
|
+
$ gem install furnish
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
Furnish.init("state.db")
|
43
|
+
# set a logger if you want -- See Furnish::Logger for more info
|
44
|
+
Furnish.logger = File.open('log', 'w')
|
45
|
+
# start a scheduler and start spinning
|
46
|
+
scheduler = Furnish::Scheduler.new
|
47
|
+
scheduler.run # returns immediately
|
48
|
+
|
49
|
+
# or, start it serially
|
50
|
+
scheduler.serial = true
|
51
|
+
scheduler.run # blocks until provisions finish
|
52
|
+
|
53
|
+
# Provision something with a Provisioner -- See Furnish::ProvisionerGroup for
|
54
|
+
# how to write them.
|
55
|
+
scheduler.schedule_provision('some_name', [MyProvisioner.new])
|
56
|
+
|
57
|
+
# depend on other provisions
|
58
|
+
scheduler.schedule_provision('some_other_name', [MyProvisioner.new], %w[some_name])
|
59
|
+
|
60
|
+
# if you want to block the current thread, you can with the wait_for call
|
61
|
+
scheduler.wait_for('some_other_name') # waits until some_other_name provisions successfully.
|
62
|
+
|
63
|
+
# in threaded mode (the default), these would have already started. If you're
|
64
|
+
# in serial mode, you need to kick the scheduler:
|
65
|
+
scheduler.run # blocks until everything finishes
|
66
|
+
|
67
|
+
# tell the scheduler to stop -- still finishes what's there, just doesn't do
|
68
|
+
# anything new.
|
69
|
+
scheduler.stop
|
70
|
+
|
71
|
+
# shutdown furnish -- closes state database
|
72
|
+
Furnish.shutdown
|
73
|
+
```
|
74
|
+
|
75
|
+
## Contracts
|
76
|
+
|
77
|
+
Furnish has high level contracts that it guarantees. These are expressed
|
78
|
+
liberally in the test suite, and any reported issue that can prove these are
|
79
|
+
violated is a blocker.
|
80
|
+
|
81
|
+
See Furnish::Scheduler and Furnish::ProvisionerGroup for what "provisioner"
|
82
|
+
means in this context.
|
83
|
+
|
84
|
+
* Furnish is a singleton and operates on a single database. Only one Furnish
|
85
|
+
instance will exist for any given process.
|
86
|
+
* Furnish will never lose track of your state unless it is never given the
|
87
|
+
opportunity to record it (e.g., `kill -9` or a hard power-off).
|
88
|
+
* Furnish will never deadlock dealing with state.
|
89
|
+
* Furnish, in threaded mode, will never block the provisioning process, and
|
90
|
+
provisioners from one group cannot block another group via Furnish.
|
91
|
+
* If a provision crashes or fails:
|
92
|
+
* Furnish will never crash as a result.
|
93
|
+
* Furnish will stop processing new items and raise an exception when
|
94
|
+
Furnish::Scheduler#running? is called.
|
95
|
+
* Currently running items will continue in threaded mode, and their state
|
96
|
+
will be dealt with accordingly.
|
97
|
+
* Furnish will never get into an irrecoverable state -- you can clean up and
|
98
|
+
start the scheduler again if that's what you need to do.
|
99
|
+
* Furnish will never try to "fix" a failed provision. You are responsible for
|
100
|
+
dealing with recovery.
|
101
|
+
* Furnish will always come with a serial mode to deal with bad actors (quite
|
102
|
+
literally) in a toolkit-independent way, when possible.
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
1. Fork it
|
107
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
108
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
109
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
110
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rdoc/task'
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.test_files = FileList["test/test_*.rb"]
|
8
|
+
t.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
RDoc::Task.new do |rdoc|
|
12
|
+
rdoc.title = "Furnish: A novel way to do virtual machine provisioning"
|
13
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
14
|
+
rdoc.rdoc_files -= ["lib/furnish/version.rb"]
|
15
|
+
if ENV["RDOC_COVER"]
|
16
|
+
rdoc.options << "-C"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "run tests with coverage report"
|
21
|
+
task "test:coverage" do
|
22
|
+
ENV["COVERAGE"] = "1"
|
23
|
+
Rake::Task["test"].invoke
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "run rdoc with coverage report"
|
27
|
+
task :rdoc_cov do
|
28
|
+
# ugh
|
29
|
+
ENV["RDOC_COVER"] = "1"
|
30
|
+
ruby "-S rake rerdoc"
|
31
|
+
end
|
data/furnish.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'furnish/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "furnish"
|
8
|
+
gem.version = Furnish::VERSION
|
9
|
+
gem.authors = ["Erik Hollensbe"]
|
10
|
+
gem.email = ["erik+github@hollensbe.org"]
|
11
|
+
gem.description = %q{A novel way to do virtual machine provisioning}
|
12
|
+
gem.summary = %q{A novel way to do virtual machine provisioning}
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'palsy', '~> 0.0.2'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rake'
|
23
|
+
gem.add_development_dependency 'minitest', '~> 4.5.0'
|
24
|
+
gem.add_development_dependency 'guard-minitest'
|
25
|
+
gem.add_development_dependency 'guard-rake'
|
26
|
+
gem.add_development_dependency 'rdoc'
|
27
|
+
gem.add_development_dependency 'rb-fsevent'
|
28
|
+
gem.add_development_dependency 'simplecov'
|
29
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Furnish
|
5
|
+
#
|
6
|
+
# Furnish::Logger is a thread safe, auto-flushing, IO-delegating logger with
|
7
|
+
# numeric level control.
|
8
|
+
#
|
9
|
+
# See Furnish::Logger::Mixins for functionality you can add to your
|
10
|
+
# provisioners to deal with loggers easily.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# # debug level is 0
|
15
|
+
# logger = Furnish::Logger.new($stderr, 0)
|
16
|
+
# # IO methods are sent straight to the IO object, synchronized by a
|
17
|
+
# # mutex:
|
18
|
+
# logger.puts "foo"
|
19
|
+
# logger.print "foo"
|
20
|
+
#
|
21
|
+
# # if_debug is a way to scope log writes:
|
22
|
+
#
|
23
|
+
# # this will never run because debug level is 0
|
24
|
+
# logger.if_debug(1) do
|
25
|
+
# # self is the IO object here
|
26
|
+
# puts "foo"
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# logger.if_debug(0) do # this will run
|
30
|
+
# puts "foo"
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# logger.debug_level = 2
|
34
|
+
#
|
35
|
+
# # if_debug's parameter merely must equal or be less than the debug
|
36
|
+
# # level to process.
|
37
|
+
# logger.if_debug(1) do # will run
|
38
|
+
# puts "bar"
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
class Logger
|
42
|
+
|
43
|
+
#
|
44
|
+
# Intended to be mixed in by other classes, provides an API for dealing
|
45
|
+
# with the standard logger object set as Furnish.logger.
|
46
|
+
#
|
47
|
+
module Mixins
|
48
|
+
#
|
49
|
+
# Delegates to Furnish::Logger#if_debug.
|
50
|
+
#
|
51
|
+
def if_debug(*args, &block)
|
52
|
+
Furnish.logger.if_debug(*args, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Set the debug level - adjustable after creation.
|
58
|
+
#
|
59
|
+
attr_accessor :debug_level
|
60
|
+
|
61
|
+
#
|
62
|
+
# The IO object. Probably best to not mess with this attribute directly,
|
63
|
+
# most methods will be proxied to it.
|
64
|
+
#
|
65
|
+
attr_reader :io
|
66
|
+
|
67
|
+
#
|
68
|
+
# Create a new Furnish::Logger. Takes an IO object and an Integer debug
|
69
|
+
# level. See Furnish::Logger class documentation for more information.
|
70
|
+
#
|
71
|
+
def initialize(logger_io=$stderr, debug_level=0)
|
72
|
+
@write_mutex = Mutex.new
|
73
|
+
@io = logger_io
|
74
|
+
@io.sync = true
|
75
|
+
@debug_level = debug_level
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Runs the block if the level is equal to or lesser than the
|
80
|
+
# Furnish::Logger#debug_level. The default debug level is 1.
|
81
|
+
#
|
82
|
+
# The block runs in the context of the Furnish::Logger#io object, that is,
|
83
|
+
# `self` is the IO object.
|
84
|
+
#
|
85
|
+
# If an additional proc is applied, will run that if the debug block would
|
86
|
+
# *not* fire, effectively creating an else. Generally an anti-pattern, but
|
87
|
+
# is useful in a few situations.
|
88
|
+
#
|
89
|
+
# if_debug is synchronized over the logger's mutex.
|
90
|
+
#
|
91
|
+
def if_debug(level=1, else_block=nil, &block)
|
92
|
+
@write_mutex.synchronize do
|
93
|
+
if debug_level >= level and block
|
94
|
+
io.instance_eval(&block)
|
95
|
+
elsif else_block
|
96
|
+
io.instance_eval(&else_block)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Delegates to the Furnish::Logger#io if possible. If not possible, raises
|
103
|
+
# a NoMethodError. All calls are synchronized over the logger's mutex.
|
104
|
+
#
|
105
|
+
def method_missing(sym, *args)
|
106
|
+
raise NoMethodError, "#{io.inspect} has no method #{sym}" unless io.respond_to?(sym)
|
107
|
+
@write_mutex.synchronize { io.__send__(sym, *args) }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Furnish
|
2
|
+
#
|
3
|
+
# Furnish provides no Provisioners as a part of its package. To use
|
4
|
+
# pre-packaged provisioners, you must install additional packages.
|
5
|
+
#
|
6
|
+
# Provisioners are *objects* that have a simple API and as a result, there is
|
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
|
+
#
|
44
|
+
module Provisioner
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'furnish/logger'
|
3
|
+
require 'furnish/provisioner'
|
4
|
+
|
5
|
+
module Furnish
|
6
|
+
#
|
7
|
+
# A provisioner group is an array of provisioners. See Furnish::Provisioner
|
8
|
+
# for what the Provisioner API looks like.
|
9
|
+
#
|
10
|
+
# A group has a set of provisioner objects, a name for the group, and a list
|
11
|
+
# of names that count as dependencies. It has methods to operate on the group
|
12
|
+
# as a unit, starting them up as a unit and shutting them down. It is
|
13
|
+
# primarily operated on by Furnish::Scheduler.
|
14
|
+
#
|
15
|
+
# In general, you interact with this class via
|
16
|
+
# Furnish::Scheduler#schedule_provision, but you can also construct groups
|
17
|
+
# yourself and deal with them via
|
18
|
+
# Furnish::Scheduler#schedule_provisioner_group.
|
19
|
+
#
|
20
|
+
# It delegates to Array and can be treated like one via the semantics of
|
21
|
+
# Ruby's DelegateClass.
|
22
|
+
#
|
23
|
+
class ProvisionerGroup < DelegateClass(Array)
|
24
|
+
|
25
|
+
include Furnish::Logger::Mixins
|
26
|
+
|
27
|
+
# The name of the group.
|
28
|
+
attr_reader :name
|
29
|
+
# The list of names the group depends on.
|
30
|
+
attr_reader :dependencies
|
31
|
+
|
32
|
+
#
|
33
|
+
# Create a new Provisioner group.
|
34
|
+
#
|
35
|
+
# * provisioners can be an array of provisioner objects or a single item
|
36
|
+
# (which will be boxed). This is what the array consists of that this
|
37
|
+
# object is.
|
38
|
+
# * name is a string. always.
|
39
|
+
# * dependencies can either be passed as an Array or Set, and will be
|
40
|
+
# converted to a Set if they are not a Set.
|
41
|
+
#
|
42
|
+
def initialize(provisioners, name, dependencies=[])
|
43
|
+
#
|
44
|
+
# FIXME maybe move the naming construct to here instead of populating it
|
45
|
+
# out to the provisioners
|
46
|
+
#
|
47
|
+
|
48
|
+
provisioners = [provisioners] unless provisioners.kind_of?(Array)
|
49
|
+
provisioners.each do |p|
|
50
|
+
p.name = name
|
51
|
+
end
|
52
|
+
|
53
|
+
@name = name
|
54
|
+
@dependencies = dependencies.kind_of?(Set) ? dependencies : Set[*dependencies]
|
55
|
+
|
56
|
+
super(provisioners)
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Provision this group.
|
61
|
+
#
|
62
|
+
# Initial arguments go to the first provisioner's startup method, and then
|
63
|
+
# the return values, if truthy, get passed to the next provisioner's
|
64
|
+
# startup method. Any falsey value causes a RuntimeError to be raised and
|
65
|
+
# provisioning halts, effectively creating a chain of responsibility
|
66
|
+
# pattern.
|
67
|
+
#
|
68
|
+
def startup(*args)
|
69
|
+
each do |this_prov|
|
70
|
+
unless args = this_prov.startup(args)
|
71
|
+
if_debug do
|
72
|
+
puts "Could not provision #{this_prov.name} with provisioner #{this_prov.class.name}"
|
73
|
+
end
|
74
|
+
|
75
|
+
raise "Could not provision #{this_prov.name} with provisioner #{this_prov.class.name}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
return true
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Deprovision this group.
|
84
|
+
#
|
85
|
+
# Provisioners are run in reverse order against the shutdown method. No
|
86
|
+
# arguments are seeded as in Furnish::ProvisionerGroup#startup. Raise
|
87
|
+
# semantics are the same as with Furnish::ProvisionerGroup#startup.
|
88
|
+
#
|
89
|
+
# If a true argument is passed to this method, the raise semantics will be
|
90
|
+
# ignored (but still logged), allowing all the provisioners to run their
|
91
|
+
# shutdown routines. See Furnish::Scheduler#force_deprovision for
|
92
|
+
# information on how to use this externally.
|
93
|
+
#
|
94
|
+
def shutdown(force=false)
|
95
|
+
reverse.each do |this_prov|
|
96
|
+
success = false
|
97
|
+
|
98
|
+
begin
|
99
|
+
success = perform_deprovision(this_prov) || force
|
100
|
+
rescue Exception => e
|
101
|
+
if force
|
102
|
+
if_debug do
|
103
|
+
puts "Deprovision #{this_prov.class.name}/#{this_prov.name} had errors:"
|
104
|
+
puts "#{e.message}"
|
105
|
+
end
|
106
|
+
else
|
107
|
+
raise e
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
unless success or force
|
112
|
+
raise "Could not deprovision #{this_prov.name}/#{this_prov.class.name}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
protected
|
118
|
+
|
119
|
+
#
|
120
|
+
# Just a way to simplify the deprovisioning logic with some generic logging.
|
121
|
+
#
|
122
|
+
def perform_deprovision(this_prov)
|
123
|
+
result = this_prov.shutdown
|
124
|
+
unless result
|
125
|
+
if_debug do
|
126
|
+
puts "Could not deprovision group #{this_prov.name}."
|
127
|
+
end
|
128
|
+
end
|
129
|
+
return result
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Furnish
|
2
|
+
module Provisioner
|
3
|
+
#
|
4
|
+
# Primarily for testing, this is a provisioner that has a basic storage
|
5
|
+
# model.
|
6
|
+
#
|
7
|
+
# In short, unless you're writing tests you should probably never use this
|
8
|
+
# code.
|
9
|
+
#
|
10
|
+
class Dummy
|
11
|
+
|
12
|
+
#--
|
13
|
+
# Some dancing around the marshal issues with this provisioner. Note that
|
14
|
+
# after restoration, any delegates you set will no longer exist, so
|
15
|
+
# relying on scheduler persistence is a really bad idea.
|
16
|
+
#++
|
17
|
+
|
18
|
+
# basic Palsy::Object store for stuffing random stuff
|
19
|
+
attr_reader :store
|
20
|
+
# order tracking via Palsy::List, delegation makes a breadcrumb here
|
21
|
+
# that's ordered between all provisioners.
|
22
|
+
attr_reader :order
|
23
|
+
# name of the provisioner according to the API
|
24
|
+
attr_accessor :name
|
25
|
+
# arbitrary identifier for Dummy#call_order
|
26
|
+
attr_accessor :id
|
27
|
+
|
28
|
+
#
|
29
|
+
# Construct a Dummy.
|
30
|
+
#
|
31
|
+
def initialize
|
32
|
+
@store = Palsy::Object.new('dummy')
|
33
|
+
@order = Palsy::List.new('dummy_order', 'shared')
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# call order is ordering on a per-provisioner group basis, and is used to
|
38
|
+
# validate that groups do indeed execute in the proper order.
|
39
|
+
#
|
40
|
+
def call_order
|
41
|
+
@call_order ||= Palsy::List.new('dummy_order', name)
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# report shim
|
46
|
+
#
|
47
|
+
def report
|
48
|
+
do_delegate(__method__) do
|
49
|
+
[name]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# startup shim
|
55
|
+
#
|
56
|
+
def startup(*args)
|
57
|
+
do_delegate(__method__) do
|
58
|
+
true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# shutdown shim
|
64
|
+
#
|
65
|
+
def shutdown
|
66
|
+
do_delegate(__method__) do
|
67
|
+
true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Helper to trace calls to this provisioner. Pretty much everything we
|
73
|
+
# care about goes through here.
|
74
|
+
#
|
75
|
+
def do_delegate(meth_name)
|
76
|
+
meth_name = meth_name.to_s
|
77
|
+
|
78
|
+
# indicate we actually did something
|
79
|
+
@store[ [name, meth_name].join("-") ] = Time.now.to_i
|
80
|
+
@order.push(name)
|
81
|
+
call_order.push(id || "unknown")
|
82
|
+
|
83
|
+
yield
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|