class Pocketknife::Node

Node

A node represents a remote computer that will be managed with Pocketknife and chef-solo. It can connect to a node, execute commands on it, install the stack, and upload and apply configurations to it.

Constants

CHEF_SOLO_APPLY

@private

CHEF_SOLO_APPLY_ALIAS

Remote path to csa @private

CHEF_SOLO_APPLY_CONTENT

Content of the chef-solo-apply file @private

ETC_CHEF

Remote path to Chef’s settings @private

NODE_JSON

Remote path to node.json @private

SOLO_RB

Remote path to solo.rb @private

SOLO_RB_CONTENT

Content of the solo.rb file @private

TMP_CHEF_SOLO_APPLY

Local path to chef-solo-apply.rb that will be included in the tarball @private

TMP_SOLO_RB

@private

TMP_TARBALL

Local path to the tarball to upload to the remote node containing shared files @private

VAR_POCKETKNIFE

Remote path to pocketknife’s deployed configuration @private

VAR_POCKETKNIFE_CACHE

Remote path to pocketknife’s cache @private

VAR_POCKETKNIFE_COOKBOOKS

Remote path to pocketknife’s cookbooks @private

VAR_POCKETKNIFE_DATA_BAGS

Remote path to pocketknife’s databags @private

VAR_POCKETKNIFE_ROLES

Remote path to pocketknife’s roles @private

VAR_POCKETKNIFE_SITE_COOKBOOKS

Remote path to pocketknife’s site-cookbooks @private

VAR_POCKETKNIFE_TARBALL

Remote path to temporary tarball containing uploaded files. @private

Attributes

connection_cache[RW]

@return [Rye::Box] The Rye::Box connection, cached by {#connection}.

name[RW]

@return [String] Name of the node.

platform_cache[RW]

@return [Hash{Symbol => String, Numeric}] Information about platform, cached by {#platform}.

pocketknife[RW]

@return [Pocketknife] The Pocketknife this node is associated with.

Public Class Methods

cleanup_upload() click to toggle source

Cleans up cache of shared files uploaded to all nodes. This cache is created by the {prepare_upload} method.

@return [void]

# File lib/pocketknife/node.rb, line 237
def self.cleanup_upload
  # TODO make this an instance method so it can avoid creating a tarball if using :rsync
  [
    TMP_TARBALL,
    TMP_SOLO_RB,
    TMP_CHEF_SOLO_APPLY
  ].each do |path|
    path.unlink if path.exist?
  end
end
new(name, pocketknife) click to toggle source

Initialize a new node.

@param [String] name A node name. @param [Pocketknife] pocketknife

# File lib/pocketknife/node.rb, line 22
def initialize(name, pocketknife)
  self.name = name
  self.pocketknife = pocketknife
  self.connection_cache = nil
end
prepare_upload() { |self| ... } click to toggle source

Prepares an upload, by creating a cache of shared files used by all nodes.

IMPORTANT: This will create files and leave them behind. You should use the block syntax or manually call {cleanup_upload} when done.

If an optional block is supplied, calls {cleanup_upload} automatically when done. This is typically used like:

Node.prepare_upload do
  mynode.upload
end

@yield to execute the block, will prepare upload before block is invoked, and cleanup the temporary files afterwards. @return [void]

# File lib/pocketknife/node.rb, line 199
def self.prepare_upload(&block)
  # TODO make this an instance method so it can avoid creating a tarball if using :rsync
  begin
    # TODO either do this in memory or scope this to the PID to allow concurrency
    TMP_SOLO_RB.open("w") {|h| h.write(SOLO_RB_CONTENT)}
    TMP_CHEF_SOLO_APPLY.open("w") {|h| h.write(CHEF_SOLO_APPLY_CONTENT)}
    TMP_TARBALL.open("w") do |handle|
      items = [
        VAR_POCKETKNIFE_COOKBOOKS.basename,
        VAR_POCKETKNIFE_SITE_COOKBOOKS.basename,
        VAR_POCKETKNIFE_ROLES.basename,
        VAR_POCKETKNIFE_DATA_BAGS.basename,
        TMP_SOLO_RB,
        TMP_CHEF_SOLO_APPLY
      ].reject { |o| not File.exist?(o) }.map {|o| o.to_s}

      Archive::Tar::Minitar.pack(
        items,
        handle
      )
    end
  rescue Exception => e
    cleanup_upload
    raise e
  end

  if block
    begin
      yield(self)
    ensure
      cleanup_upload
    end
  end
end

Public Instance Methods

apply() click to toggle source

Applies the configuration to the node. Installs Chef, Ruby and Rubygems if needed.

@return [void]

# File lib/pocketknife/node.rb, line 348
def apply
  self.install

  self.say("Applying configuration...", true)
  command = "chef-solo -j #{NODE_JSON.shellescape}"
  command << " -o #{self.pocketknife.runlist}" if self.pocketknife.runlist
  command << " -l debug" if self.pocketknife.verbosity == true
  self.execute(command, true)
  self.say("Finished applying!")
end
connection() click to toggle source

Returns a connection.

Caches result to {#connection_cache}.

@return [Rye::Box]

# File lib/pocketknife/node.rb, line 33
def connection
  return self.connection_cache ||= begin
      rye = Rye::Box.new(self.name, :user => "root")
      rye.disable_safe_mode
      rye
    end
end
deploy() click to toggle source

Deploys the configuration to the node, which calls {#upload} and {#apply}.

@return [void]

# File lib/pocketknife/node.rb, line 362
def deploy
  self.upload
  self.apply
end
execute(commands, immediate=false) click to toggle source

Executes commands on the external node.

@param [String] commands Shell commands to execute. @param [Boolean] immediate Display execution information immediately to STDOUT, rather than returning it as an object when done. @return [Rye::Rap] A result object describing the completed execution. @raise [ExecutionError] Something went wrong with the execution, the cause is described in the exception.

# File lib/pocketknife/node.rb, line 373
def execute(commands, immediate=false)
  self.say("Executing:\n#{commands}", false)
  if immediate
    self.connection.stdout_hook {|line| puts line}
  end
  return self.connection.execute("(#{commands}) 2>&1")
rescue Rye::Err => e
  raise Pocketknife::ExecutionError.new(self.name, commands, e, immediate)
ensure
  self.connection.stdout_hook = nil
end
has_executable?(executable) click to toggle source

Does this node have the given executable?

@param [String] executable A name of an executable, e.g. chef-solo. @return [Boolean] Has executable?

# File lib/pocketknife/node.rb, line 61
def has_executable?(executable)
  begin
    self.connection.execute(%{which #{executable.shellescape} && test -x `which #{executable.shellescape}`})
    return true
  rescue Rye::Err
    return false
  end
end
install() click to toggle source

Installs Chef and its dependencies on a node if needed.

@return [void] @raise [NotInstalling] Can’t install because user Chef isn’t already present and user forbade automatic installation. @raise [UnsupportedInstallationPlatform] Don’t know how to install on this platform.

# File lib/pocketknife/node.rb, line 109
def install
  unless self.has_executable?("chef-solo")
    case self.pocketknife.can_install
    when nil
      # Prompt for installation
      print "? #{self.name}: Chef not found. Install it and its dependencies? (Y/n) "
      STDOUT.flush
      answer = STDIN.gets.chomp
      case answer
      when /^y/i, ''
        # Continue with install
      else
        raise NotInstalling.new("Chef isn't installed on node '#{self.name}', but user doesn't want to install it.", self.name)
      end
    when true
      # User wanted us to install
    else
      # Don't install
      raise NotInstalling.new("Chef isn't installed on node '#{self.name}', but user doesn't want to install it.", self.name)
    end

    unless self.has_executable?("ruby")
      self.install_ruby
    end

    unless self.has_executable?("gem")
      self.install_rubygems
    end

    self.install_chef
  end
end
install_chef() click to toggle source

Installs Chef on the remote node.

@return [void]

# File lib/pocketknife/node.rb, line 145
def install_chef
  self.say("Installing chef...")
  self.execute("gem install --no-rdoc --no-ri chef", true)
  self.say("Installed chef", false)
end
install_ruby() click to toggle source

Installs Ruby on the remote node.

@return [void]

# File lib/pocketknife/node.rb, line 171
def install_ruby
  command = \
    case self.platform[:distributor].downcase
    when /ubuntu/, /debian/, /gnu\/linux/
      "DEBIAN_FRONTEND=noninteractive apt-get --yes install ruby ruby-dev libopenssl-ruby irb build-essential wget ssl-cert"
    when /centos/, /red hat/, /scientific linux/
      "yum -y install ruby ruby-shadow gcc gcc-c++ ruby-devel wget"
    else
      raise UnsupportedInstallationPlatform.new("Can't install on node '#{self.name}' with unknown distrubtor: `#{self.platform[:distrubtor]}`", self.name)
    end

  self.say("Installing ruby...")
  self.execute(command, true)
  self.say("Installed ruby", false)
end
install_rubygems() click to toggle source

Installs Rubygems on the remote node.

@return [void]

# File lib/pocketknife/node.rb, line 154
    def install_rubygems
      self.say("Installing rubygems...")
      self.execute(<<-HERE, true)
cd /root &&
  rm -rf rubygems-1.3.7 rubygems-1.3.7.tgz &&
  wget http://production.cf.rubygems.org/rubygems/rubygems-1.3.7.tgz &&
  tar zxf rubygems-1.3.7.tgz &&
  cd rubygems-1.3.7 &&
  ruby setup.rb --no-format-executable &&
  rm -rf rubygems-1.3.7 rubygems-1.3.7.tgz
      HERE
      self.say("Installed rubygems", false)
    end
local_node_json_pathname() click to toggle source

Returns path to this node’s nodes/NAME.json file, used as node.json by chef-solo.

@return [Pathname]

# File lib/pocketknife/node.rb, line 53
def local_node_json_pathname
  return Pathname.new("nodes") + "#{self.name}.json"
end
platform() click to toggle source

Returns information describing the node.

The information is formatted similar to this:

{
  :distributor => "Ubuntu", # String with distributor name
  :codename => "maverick", # String with release codename
  :release => "10.10", # String with release number
  :version => 10.1 # Float with release number
}

@return [Hash{Symbol => String, Numeric}] Return a hash describing the node, see above. @raise [UnsupportedInstallationPlatform] Don’t know how to install on this platform.

# File lib/pocketknife/node.rb, line 82
def platform
  return self.platform_cache ||= begin
    lsb_release = "/etc/lsb-release"
    begin
      output = self.connection.cat(lsb_release).to_s
      result = {}
      result[:distributor] = output[/DISTRIB_ID\s*=\s*(.+?)$/, 1]
      result[:release] = output[/DISTRIB_RELEASE\s*=\s*(.+?)$/, 1]
      result[:codename] = output[/DISTRIB_CODENAME\s*=\s*(.+?)$/, 1]
      result[:version] = result[:release].to_f

      if result[:distributor] && result[:release] && result[:codename] && result[:version]
        return result
      else
        raise UnsupportedInstallationPlatform.new("Can't install on node '#{self.name}' with invalid '#{lsb_release}' file", self.name)
      end
    rescue Rye::Err
      raise UnsupportedInstallationPlatform.new("Can't install on node '#{self.name}' without '#{lsb_release}'", self.name)
    end
  end
end
rsync(*args) click to toggle source

Rsync files to a node.

@param [Array] args Arguments to sent to rsync, e.g. options, filenames, and target. @return [void] @raise [RsyncError] Something went wrong with the rsync.

# File lib/pocketknife/node.rb, line 253
def rsync(*args)
  command = ['rsync', *args.map{|o| o.shellescape}]
  unless system *command
    raise Pocketknife::RsyncError.new(command.join(' '), self.name)
  end
end
rsync_directory(*args) click to toggle source

Rsync directory to a node with options: --recursive --update --copy-links --delete

@param [Array] args Arguments to sent to rsync, e.g. options, directory name, and target. @return [void] @raise [RsyncError] Something went wrong with the rsync.

# File lib/pocketknife/node.rb, line 274
def rsync_directory(*args)
  self.rsync("-ruL", "--delete", *args)
end
rsync_file(*args) click to toggle source

Rsync a file to a node with options: --update --copy-links

@param [Array] args Arguments to sent to rsync, e.g. options, filename, and target. @return [void] @raise [RsyncError] Something went wrong with the rsync.

# File lib/pocketknife/node.rb, line 265
def rsync_file(*args)
  self.rsync("-uL", *args)
end
say(message, importance=nil) click to toggle source

Displays status message.

@param [String] message The message to display. @param [Boolean] importance How important is this? true means important, nil means normal, false means unimportant. @return [void]

# File lib/pocketknife/node.rb, line 46
def say(message, importance=nil)
  self.pocketknife.say("* #{self.name}: #{message}", importance)
end
upload() click to toggle source

Uploads configuration information to node.

IMPORTANT: You must first call {prepare_upload} to create the shared files that will be uploaded.

@return [void]

# File lib/pocketknife/node.rb, line 283
    def upload
      self.say("Uploading configuration...")

      self.say("Removing old files...", false)
      self.execute <<-HERE
umask 0377 &&
  rm -rf #{ETC_CHEF.shellescape} #{VAR_POCKETKNIFE.shellescape} #{VAR_POCKETKNIFE_CACHE.shellescape} #{CHEF_SOLO_APPLY.shellescape} #{CHEF_SOLO_APPLY_ALIAS.shellescape} &&
  mkdir -p #{ETC_CHEF.shellescape} #{VAR_POCKETKNIFE.shellescape} #{VAR_POCKETKNIFE_CACHE.shellescape} #{CHEF_SOLO_APPLY.dirname.shellescape}
      HERE

      case self.pocketknife.transfer_mechanism
      when :tar
        self.say("Uploading new files...", false)
        self.connection.file_upload(self.local_node_json_pathname.to_s, NODE_JSON.to_s)
        self.connection.file_upload(TMP_TARBALL.to_s, VAR_POCKETKNIFE_TARBALL.to_s)

        self.say("Installing new files...", false)
        self.execute <<-HERE, true
cd #{VAR_POCKETKNIFE_CACHE.shellescape} &&
  tar xf #{VAR_POCKETKNIFE_TARBALL.shellescape} &&
  chmod -R u+rwX,go= . &&
  chown -R root:root . &&
  mv #{TMP_SOLO_RB.shellescape} #{SOLO_RB.shellescape} &&
  mv #{TMP_CHEF_SOLO_APPLY.shellescape} #{CHEF_SOLO_APPLY.shellescape} &&
  chmod u+x #{CHEF_SOLO_APPLY.shellescape} &&
  ln -s #{CHEF_SOLO_APPLY.basename.shellescape} #{CHEF_SOLO_APPLY_ALIAS.shellescape} &&
  rm #{VAR_POCKETKNIFE_TARBALL.shellescape} &&
  mv * #{VAR_POCKETKNIFE.shellescape}
        HERE

      when :rsync
        self.say("Uploading new files...", false)

        self.rsync_file("#{self.local_node_json_pathname}", "root@#{self.name}:#{NODE_JSON}")

        %w[SOLO_RB CHEF_SOLO_APPLY].each do |fragment|
          source = self.class.const_get("TMP_#{fragment}")
          target = self.class.const_get(fragment)
          self.rsync_file("#{source}", "root@#{self.name}:#{target}")
        end

        %w[COOKBOOKS SITE_COOKBOOKS ROLES DATA_BAGS].each do |fragment|
          target = self.class.const_get("VAR_POCKETKNIFE_#{fragment}")
          source = target.basename
          next unless source.exist?
          self.rsync_directory("#{source}/", "root@#{self.name}:#{target}")
        end

        self.say("Modifying new files...", false)
        self.execute <<-HERE, true
cd #{VAR_POCKETKNIFE_CACHE.shellescape} &&
  chmod u+x #{CHEF_SOLO_APPLY.shellescape} &&
  ln -s #{CHEF_SOLO_APPLY.basename.shellescape} #{CHEF_SOLO_APPLY_ALIAS.shellescape}
        HERE

      else
        raise InvalidTransferMechanism.new(self.pocketknife.transfer_mechanism)
      end

      self.say("Finished uploading!", false)
    end