lib/jamie.rb in jamie-0.1.0.alpha16 vs lib/jamie.rb in jamie-0.1.0.alpha17
- old
+ new
@@ -1,10 +1,11 @@
# -*- encoding: utf-8 -*-
require 'base64'
require 'delegate'
require 'digest'
+require 'erb'
require 'fileutils'
require 'json'
require 'mixlib/shellout'
require 'net/https'
require 'net/scp'
@@ -148,22 +149,26 @@
def new_driver(plugin, config)
Driver.for_plugin(plugin, config)
end
def yaml
- @yaml ||= YAML.load_file(File.expand_path(yaml_file)).rmerge(local_yaml)
+ @yaml ||= YAML.load(yaml_contents).rmerge(local_yaml)
end
+ def yaml_contents
+ ERB.new(IO.read(File.expand_path(yaml_file))).result
+ end
+
def local_yaml_file
std = File.expand_path(yaml_file)
std.sub(/(#{File.extname(std)})$/, '.local\1')
end
def local_yaml
@local_yaml ||= begin
if File.exists?(local_yaml_file)
- YAML.load_file(local_yaml_file)
+ YAML.load(ERB.new(IO.read(local_yaml_file)).result)
else
Hash.new
end
end
end
@@ -362,100 +367,123 @@
# @return [self] this instance, used to chain actions
#
# @todo rescue Driver::ActionFailed and return some kind of null object
# to gracfully stop action chaining
def create
- puts "-----> Creating instance #{name}"
- action(:create) { |state| platform.driver.create(self, state) }
- puts " Creation of instance #{name} complete."
- self
+ transition_to(:create)
end
# Converges this running instance.
#
# @see Driver::Base#converge
# @return [self] this instance, used to chain actions
#
# @todo rescue Driver::ActionFailed and return some kind of null object
# to gracfully stop action chaining
def converge
- puts "-----> Converging instance #{name}"
- action(:converge) { |state| platform.driver.converge(self, state) }
- puts " Convergence of instance #{name} complete."
- self
+ transition_to(:converge)
end
# Sets up this converged instance for suite tests.
#
# @see Driver::Base#setup
# @return [self] this instance, used to chain actions
#
# @todo rescue Driver::ActionFailed and return some kind of null object
# to gracfully stop action chaining
def setup
- puts "-----> Setting up instance #{name}"
- action(:setup) { |state| platform.driver.setup(self, state) }
- puts " Setup of instance #{name} complete."
- self
+ transition_to(:setup)
end
# Verifies this set up instance by executing suite tests.
#
# @see Driver::Base#verify
# @return [self] this instance, used to chain actions
#
# @todo rescue Driver::ActionFailed and return some kind of null object
# to gracfully stop action chaining
def verify
- puts "-----> Verifying instance #{name}"
- action(:verify) { |state| platform.driver.verify(self, state) }
- puts " Verification of instance #{name} complete."
- self
+ transition_to(:verify)
end
# Destroys this instance.
#
# @see Driver::Base#destroy
# @return [self] this instance, used to chain actions
#
# @todo rescue Driver::ActionFailed and return some kind of null object
# to gracfully stop action chaining
def destroy
- puts "-----> Destroying instance #{name}"
- action(:destroy) { |state| platform.driver.destroy(self, state) }
- destroy_state
- puts " Destruction of instance #{name} complete."
- self
+ transition_to(:destroy)
end
# Tests this instance by creating, converging and verifying. If this
# instance is running, it will be pre-emptively destroyed to ensure a
# clean slate. The instance will be left post-verify in a running state.
#
- # @see #destroy
- # @see #create
- # @see #converge
- # @see #setup
- # @see #verify
+ # @param destroy_mode [Symbol] strategy used to cleanup after instance
+ # has finished verifying (default: `:passing`)
# @return [self] this instance, used to chain actions
#
# @todo rescue Driver::ActionFailed and return some kind of null object
# to gracfully stop action chaining
- def test
+ def test(destroy_mode = :passing)
puts "-----> Cleaning up any prior instances of #{name}"
destroy
puts "-----> Testing instance #{name}"
- create
- converge
- setup
verify
+ destroy if destroy_mode == :passing
puts " Testing of instance #{name} complete."
self
+ ensure
+ destroy if destroy_mode == :always
end
private
+ def transition_to(desired)
+ FSM.actions(last_action, desired).each do |transition|
+ send("#{transition}_action")
+ end
+ end
+
+ def create_action
+ puts "-----> Creating instance #{name}"
+ action(:create) { |state| platform.driver.create(self, state) }
+ puts " Creation of instance #{name} complete."
+ self
+ end
+
+ def converge_action
+ puts "-----> Converging instance #{name}"
+ action(:converge) { |state| platform.driver.converge(self, state) }
+ puts " Convergence of instance #{name} complete."
+ self
+ end
+
+ def setup_action
+ puts "-----> Setting up instance #{name}"
+ action(:setup) { |state| platform.driver.setup(self, state) }
+ puts " Setup of instance #{name} complete."
+ self
+ end
+
+ def verify_action
+ puts "-----> Verifying instance #{name}"
+ action(:verify) { |state| platform.driver.verify(self, state) }
+ puts " Verification of instance #{name} complete."
+ self
+ end
+
+ def destroy_action
+ puts "-----> Destroying instance #{name}"
+ action(:destroy) { |state| platform.driver.destroy(self, state) }
+ destroy_state
+ puts " Destruction of instance #{name} complete."
+ self
+ end
+
def action(what)
state = load_state
yield state if block_given?
state['last_action'] = what.to_s
ensure
@@ -480,10 +508,50 @@
def statefile
File.expand_path(File.join(
platform.driver['jamie_root'], ".jamie", "#{name}.yml"
))
end
+
+ def last_action
+ load_state['last_action']
+ end
+
+ # The simplest finite state machine pseudo-implementation needed to manage
+ # an Instance.
+ class FSM
+
+ # Returns an Array of all transitions to bring an Instance from its last
+ # reported transistioned state into the desired transitioned state.
+ #
+ # @param last [String,Symbol,nil] the last known transitioned state of
+ # the Instance, defaulting to `nil` (for unknown or no history)
+ # @param desired [String,Symbol] the desired transitioned state for the
+ # Instance
+ # @return [Array<Symbol>] an Array of transition actions to perform
+ def self.actions(last = nil, desired)
+ last_index = index(last)
+ desired_index = index(desired)
+
+ if last_index == desired_index || last_index > desired_index
+ Array(TRANSITIONS[desired_index])
+ else
+ TRANSITIONS.slice(last_index + 1, desired_index - last_index)
+ end
+ end
+
+ private
+
+ TRANSITIONS = [ :destroy, :create, :converge, :setup, :verify ]
+
+ def self.index(transition)
+ if transition.nil?
+ 0
+ else
+ TRANSITIONS.find_index { |t| t == transition.to_sym }
+ end
+ end
+ end
end
# Command string generator to interface with Jamie Runner (jr). The
# commands that are generated are safe to pass to an SSH command or as an
# unix command argument (escaped in single quotes).
@@ -779,28 +847,28 @@
def create(instance, state)
raise NotImplementedError, "#create must be implemented by subclass."
end
def converge(instance, state)
- ssh_args = generate_ssh_args(state)
+ ssh_args = build_ssh_args(state)
install_omnibus(ssh_args) if config['require_chef_omnibus']
prepare_chef_home(ssh_args)
upload_chef_data(ssh_args, instance)
run_chef_solo(ssh_args)
end
def setup(instance, state)
- ssh_args = generate_ssh_args(state)
+ ssh_args = build_ssh_args(state)
if instance.jr.setup_cmd
ssh(ssh_args, instance.jr.setup_cmd)
end
end
def verify(instance, state)
- ssh_args = generate_ssh_args(state)
+ ssh_args = build_ssh_args(state)
if instance.jr.run_cmd
ssh(ssh_args, instance.jr.sync_cmd)
ssh(ssh_args, instance.jr.run_cmd)
end
@@ -810,14 +878,15 @@
raise NotImplementedError, "#destroy must be implemented by subclass."
end
protected
- def generate_ssh_args(state)
- [ state['hostname'],
- config['username'],
- { :password => config['password'] }
- ]
+ def build_ssh_args(state)
+ opts = Hash.new
+ opts[:password] = config['password'] if config['password']
+ opts[:keys] = Array(config['ssh_key']) if config['ssh_key']
+
+ [ state['hostname'], config['username'], opts ]
end
def chef_home
"/tmp/jamie-chef-solo".freeze
end