furnish 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ coverage
19
+ html
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in furnish.gemspec
4
+ gemspec
5
+
6
+ gem 'guard-rake', :git => "https://github.com/erikh/guard-rake", :branch => "failure_ok"
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