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