furnish 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|