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
@return [Rye::Box] The Rye::Box connection, cached by {#connection}.
@return [String] Name of the node.
@return [Hash{Symbol => String
, Numeric}] Information about platform, cached by {#platform}.
@return [Pocketknife] The Pocketknife
this node is associated with.
Public Class Methods
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
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
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
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
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
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
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
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
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
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
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
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
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
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 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 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 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
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
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