class Rouster

TODO better document keys :constrain and :version

Vagrant specific (and related) methods

Constants

VERSION

sporadically updated version number

Attributes

cache[R]
cache_timeout[R]
deltas[R]
exitcode[R]
facts[RW]
last_puppet_run[RW]
logger[R]
name[R]
output[R]
passthrough[R]
retries[R]
sshkey[R]
unittest[R]
vagrantbinary[R]
vagrantfile[R]

Public Class Methods

new(opts = nil) click to toggle source

initialize - object instantiation

parameters

  • <name> - the name of the VM as specified in the Vagrantfile

  • cache_timeout
    • integer specifying how long Rouster should cache status() and is_available_via_ssh?() results, default is false

  • logfile
    • allows logging to an external file, if passed true, generates a dynamic filename, otherwise uses what is passed, default is false

  • passthrough
    • boolean of whether this is a VM or passthrough, default is false – passthrough is not completely implemented

  • retries
    • integer specifying number of retries Rouster should attempt when running external (currently only vagrant()) commands

  • sshkey
    • the full or relative path to a SSH key used to auth to VM – defaults to location Vagrant installs to (ENV[VAGRANT_HOME} or ]~/.vagrant.d/)

  • sshtunnel
    • boolean of whether or not to instantiate the SSH tunnel upon upping the VM, default is true

  • sudo
    • boolean of whether or not to prefix commands run in VM with 'sudo', default is true

  • vagrantfile
    • the full or relative path to the Vagrantfile to use, if not specified, will look for one in 5 directories above current location

  • vagrant_concurrency
    • boolean controlling whether Rouster will attempt to run `vagrant *` if another vagrant process is already running, default is false

  • vagrant_reboot
    • particularly sticky systems restart better if Vagrant does it for us, default is false

  • verbosity
    • an integer representing console level logging, or an array of integers representing console,file level logging - DEBUG (0) < INFO (1) < WARN (2) < ERROR (3) < FATAL (4)

# File lib/rouster.rb, line 46
def initialize(opts = nil)
  @cache_timeout       = opts[:cache_timeout].nil? ? false : opts[:cache_timeout]
  @logfile             = opts[:logfile].nil? ? false : opts[:logfile]
  @name                = opts[:name]
  @passthrough         = opts[:passthrough].nil? ? false : opts[:passthrough]
  @retries             = opts[:retries].nil? ? 0 : opts[:retries]
  @sshkey              = opts[:sshkey]
  @sshtunnel           = opts[:sshtunnel].nil? ? true : opts[:sshtunnel]
  @unittest            = opts[:unittest].nil? ? false : opts[:unittest]
  @vagrantfile         = opts[:vagrantfile].nil? ? traverse_up(Dir.pwd, 'Vagrantfile', 5) : opts[:vagrantfile]
  @vagrant_concurrency = opts[:vagrant_concurrency].nil? ? false : opts[:vagrant_concurrency]
  @vagrant_reboot      = opts[:vagrant_reboot].nil? ? false : opts[:vagrant_reboot]

  # TODO kind of want to invert this, 0 = trace, 1 = debug, 2 = info, 3 = warning, 4 = error
  # could do `fixed_ordering = [4, 3, 2, 1, 0]` and use user input as index instead, so an input of 4 (which should be more verbose), yields 0
  if opts[:verbosity]
    # TODO decide how to handle this case -- currently #2 is implemented
    # - option 1, if passed a single integer, use that level for both loggers
    # - option 2, if passed a single integer, use that level for stdout, and a hardcoded level (probably INFO) to logfile

    # kind of want to do if opts[:verbosity].respond_to?(:[]), but for 1.87 compatability, going this way..
    if ! opts[:verbosity].is_a?(Array) or opts[:verbosity].is_a?(Integer)
      @verbosity_console = opts[:verbosity].to_i
      @verbosity_logfile = 2
    elsif opts[:verbosity].is_a?(Array)
      # TODO more error checking here when we are sure this is the right way to go
      @verbosity_console = opts[:verbosity][0].to_i
      @verbosity_logfile = opts[:verbosity][1].to_i
      @logfile = true if @logfile.eql?(false) # overriding the default setting
    end
  else
    @verbosity_console = 3
    @verbosity_logfile = 2 # this is kind of arbitrary, but won't actually be created unless opts[:logfile] is also passed
  end

  @ostype    = nil
  @osversion = nil

  @output = Array.new
  @cache  = Hash.new
  @deltas = Hash.new

  @exitcode = nil
  @ssh      = nil # hash containing the SSH connection object
  @ssh_info = nil # hash containing connection information

  # set up logging
  require 'log4r/config'
  Log4r.define_levels(*Log4r::Log4rConfig::LogLevels)

  @logger            = Log4r::Logger.new(sprintf('rouster:%s', @name))
  @logger.outputters << Log4r::Outputter.stderr
  #@log.outputters << Log4r::Outputter.stdout

  if @logfile
    @logfile = @logfile.eql?(true) ? sprintf('/tmp/rouster-%s.%s.%s.log', @name, Time.now.to_i, $$) : @logfile
    @logger.outputters << Log4r::FileOutputter.new(sprintf('rouster:%s', @name), :filename => @logfile, :level => @verbosity_logfile)
  end

  @logger.outputters[0].level = @verbosity_console # can't set this when instantiating a .std* logger, and want the FileOutputter at a different level

  if opts.has_key?(:sudo)
    @sudo = opts[:sudo]
  elsif @passthrough.class.eql?(Hash)
    @logger.debug(sprintf('passthrough without sudo specification, defaulting to false'))
    @sudo = false
  else
    @sudo = true
  end

  if @passthrough
    @vagrantbinary = 'vagrant' # hacky fix to is_vagrant_running?() grepping, doesn't need to actually be in $PATH
    @sshtunnel     = opts[:sshtunnel].nil? ? false : @sshtunnel # unless user has specified it, non-local passthroughs default to not open tunnel

    defaults = {
      :paranoid          => false, # valid overrides are: false, true, :very, or :secure
      :ssh_sleep_ceiling => 9,
      :ssh_sleep_time    => 10,
    }

    @passthrough = defaults.merge(@passthrough)

    if @passthrough.class != Hash
      raise ArgumentError.new('passthrough specification should be hash')
    elsif @passthrough[:type].nil?
      raise ArgumentError.new('passthrough :type must be specified, :local, :remote or :aws allowed')
    elsif @passthrough[:type].eql?(:local)
      @logger.debug('instantiating a local passthrough worker')
      @sshtunnel = opts[:sshtunnel].nil? ? true : opts[:sshtunnel] # override default, if local, open immediately

    elsif @passthrough[:type].eql?(:remote)
      @logger.debug('instantiating a remote passthrough worker')

      [:host, :user, :key].each do |r|
        raise ArgumentError.new(sprintf('remote passthrough requires[%s] specification', r)) if @passthrough[r].nil?
      end

      raise ArgumentError.new('remote passthrough requires valid :key specification, should be path to private half') unless File.file?(@passthrough[:key])
      @sshkey = @passthrough[:key] # TODO refactor so that you don't have to do this..

    elsif @passthrough[:type].eql?(:aws) or @passthrough[:type].eql?(:raiden)
      @logger.debug(sprintf('instantiating an %s passthrough worker', @passthrough[:type]))

      aws_defaults = {
        :ami                   => 'ami-7bdaa84b', # RHEL 6.5 x64 in us-west-2
        :dns_propagation_sleep => 30, # how much time to wait after ELB creation before attempting to connect
        :elb_cleanup           => false,
        :key_id                => ENV['AWS_ACCESS_KEY_ID'],
        :min_count             => 1,
        :max_count             => 1,
        :region                => 'us-west-2',
        :secret_key            => ENV['AWS_SECRET_ACCESS_KEY'],
        :size                  => 't1.micro',
        :ssh_port              => 22,
        :user                  => 'ec2-user',
      }

      if @passthrough.has_key?(:ami)
        @logger.debug(':ami specified, will start new EC2 instance')

        @passthrough[:security_groups] = @passthrough[:security_groups].is_a?(Array) ? @passthrough[:security_groups] : [ @passthrough[:security_groups] ]

        @passthrough = aws_defaults.merge(@passthrough)

        [:ami, :size, :user, :region, :key, :keypair, :key_id, :secret_key, :security_groups].each do |r|
          raise ArgumentError.new(sprintf('AWS passthrough requires %s specification', r)) if @passthrough[r].nil?
        end

      elsif @passthrough.has_key?(:instance)
        @logger.debug(':instance specified, will connect to existing EC2 instance')

        @passthrough = aws_defaults.merge(@passthrough)

        if @passthrough[:type].eql?(:aws)
          @passthrough[:host] = self.aws_describe_instance(@passthrough[:instance])['dnsName']
        else
          @passthrough[:host] = self.find_ssh_elb(true)
        end

        [:instance, :key, :user, :host].each do |r|
          raise ArgumentError.new(sprintf('AWS passthrough requires [%s] specification', r)) if @passthrough[r].nil?
        end

      else
        raise ArgumentError.new('AWS passthrough requires either :ami or :instance specification')
      end

      raise ArgumentError.new('AWS passthrough requires valid :sshkey specification, should be path to private half') unless File.file?(@passthrough[:key])
      @sshkey = @passthrough[:key]
    elsif @passthrough[:type].eql?(:openstack)
      @logger.debug(sprintf('instantiating an %s passthrough worker', @passthrough[:type]))
      @sshkey = @passthrough[:key]

      ostack_defaults = {
        :ssh_port => 22,
      }
      @passthrough = ostack_defaults.merge(@passthrough)

      [:openstack_auth_url, :openstack_username, :openstack_tenant, :openstack_api_key,
        :key ].each do |r|
          raise ArgumentError.new(sprintf('OpenStack passthrough requires %s specification', r)) if @passthrough[r].nil?
      end

      if @passthrough.has_key?(:image_ref)
        @logger.debug(':image_ref specified, will start new Nova instance')
      elsif @passthrough.has_key?(:instance)
        @logger.debug(':instance specified, will connect to existing OpenStack instance')
        inst_details = self.ostack_describe_instance(@passthrough[:instance])
        raise ArgumentError.new(sprintf('No such instance found in OpenStack - %s', @passthrough[:instance])) if inst_details.nil?
        inst_details.addresses.each_key do |address_key|
          if defined?(inst_details.addresses[address_key].first['addr'])
            @passthrough[:host] = inst_details.addresses[address_key].first['addr']
            break
          end
        end
      end
    else
      raise ArgumentError.new(sprintf('passthrough :type [%s] unknown, allowed: :aws, :openstack, :local, :remote', @passthrough[:type]))
    end

  else

    @logger.debug('Vagrantfile and VM name validation..')
    unless File.file?(@vagrantfile)
      raise ArgumentError.new(sprintf('specified Vagrantfile [%s] does not exist', @vagrantfile))
    end

    raise ArgumentError.new('name of Vagrant VM not specified') if @name.nil?

    return if opts[:unittest].eql?(true) # quick return if we're a unit test

    begin
      @vagrantbinary = self._run('which vagrant').chomp!
    rescue
      raise ExternalError.new('vagrant not found in path')
    end

    @logger.debug('SSH key discovery and viability tests..')
    if @sshkey.nil?
      # ref the key from the vagrant home dir if it's been overridden
      @sshkey = sprintf('%s/insecure_private_key', ENV['VAGRANT_HOME']) if ENV['VAGRANT_HOME']
      @sshkey = sprintf('%s/.vagrant.d/insecure_private_key', ENV['HOME']) unless ENV['VAGRANT_HOME']
    end
  end

  begin
    raise InternalError.new('ssh key not specified') if @sshkey.nil?
    raise InternalError.new('ssh key does not exist') unless File.file?(@sshkey)
    self.check_key_permissions(@sshkey)
  rescue => e

    unless self.is_passthrough? and @passthrough[:type].eql?(:local)
      raise InternalError.new("specified key [#{@sshkey}] has bad permissions. Vagrant exception: [#{e.message}]")
    end

  end

  if @sshtunnel
    self.up()
  end

  @logger.info('Rouster object successfully instantiated')
end
os_files() click to toggle source
# File lib/rouster.rb, line 829
def self.os_files
  {
    :ubuntu  => '/etc/os-release', # debian too
    :solaris => '/etc/release',
    :rhel    => ['/etc/os-release', '/etc/redhat-release'], # and centos
    :osx     => '/System/Library/CoreServices/SystemVersion.plist',
  }
end

Public Instance Methods

_run(command) click to toggle source

_run

(should be) private method that executes commands on the local host (not guest VM)

returns STDOUT|STDERR, raises Rouster::LocalExecutionError on non 0 exit code sets @exitcode

parameters

  • <command> - command to be run

# File lib/rouster.rb, line 711
def _run(command)
  tmp_file = sprintf('/tmp/rouster-cmd_output.%s.%s', Time.now.to_i, $$)
  cmd      = sprintf('%s > %s 2> %s', command, tmp_file, tmp_file) # this is a holdover from Salesforce::Vagrant, can we use '2&>1' here?
  res      = `#{cmd}` # what does this actually hold?

  @logger.info(sprintf('host running: [%s]', cmd))

  output = File.read(tmp_file)
  File.delete(tmp_file) or raise InternalError.new(sprintf('unable to delete [%s]: %s', tmp_file, $!))

  self.output.push(output)
  @logger.debug(sprintf('output: [%s]', output))

  unless $?.success?
    raise LocalExecutionError.new(sprintf('command [%s] exited with code [%s], output [%s]', cmd, $?.to_i(), output))
  end

  @exitcode = $?.to_i()
  output
end
check_key_permissions(key, fix=false) click to toggle source

check_key_permissions

checks (and optionally fixes) permissions on the SSH key used to auth to the Vagrant VM

parameters

* <key> - full path to SSH key
* [fix] - boolean, if true and required, will attempt to set permissions on key to 0400 - default is false
# File lib/rouster.rb, line 797
def check_key_permissions(key, fix=false)
  allowed_modes = ['0400', '0600']

  if key.match(/\.pub$/)
    # if this is the public half of the key, be more permissive
    allowed_modes << '0644'
  end

  raw   = self._run(sprintf('ls -l %s', key))
  perms = self.parse_ls_string(raw)

  unless allowed_modes.member?(perms[:mode])
    if fix.eql?(true)
      self._run(sprintf('chmod 0400 %s', key))
      return check_key_permissions(key, fix)
    else
      raise InternalError.new(sprintf('perms for [%s] are [%s], expecting [%s]', key, perms[:mode], allowed_modes))
    end
  end

  unless perms[:owner].eql?(ENV['USER'])
    if fix.eql?(true)
      self._run(sprintf('chown %s %s', ENV['USER'], key))
      return check_key_permissions(key, fix)
    else
      raise InternalError.new(sprintf('owner for [%s] is [%s], expecting [%s]', key, perms[:owner], ENV['USER']))
    end
  end

  nil
end
connect_ssh_tunnel() click to toggle source

connect_ssh_tunnel

instantiates a Net::SSH persistent connection to the Vagrant VM

raises its own InternalError if the machine isn't running, otherwise returns Net::SSH connection object

# File lib/rouster.rb, line 439
def connect_ssh_tunnel

  if self.is_passthrough?
    if @passthrough[:type].eql?(:local)
      @logger.debug("local passthroughs don't need ssh tunnel, shell execs are used")
      return false
    elsif @passthrough[:host].nil?
      @logger.info(sprintf('not attempting to connect, no known hostname for[%s]', self.passthrough))
      return false
    else
      ceiling    = @passthrough[:ssh_sleep_ceiling]
      sleep_time = @passthrough[:ssh_sleep_time]

      0.upto(ceiling) do |try|
        @logger.debug(sprintf('opening remote SSH tunnel[%s]..', @passthrough[:host]))
        begin
          @ssh = Net::SSH.start(
            @passthrough[:host],
            @passthrough[:user],
            :port                       => @passthrough[:ssh_port],
            :keys                       => [ @passthrough[:key] ], # TODO this should be @sshkey
            :paranoid                   => false,
            :number_of_password_prompts => 0
          )
          break
        rescue => e
          raise e if try.eql?(ceiling) # eventually want to throw a SocketError
          @logger.debug(sprintf('failed to open tunnel[%s], trying again in %ss', e.message, sleep_time))
          sleep sleep_time
        end
      end
    end
    @logger.debug(sprintf('successfully opened SSH tunnel to[%s]', passthrough[:host]))

  else
    # not a passthrough, normal connection
    status = self.status()

    if status.eql?('running')
      self.get_ssh_info()
      @logger.debug('opening VM SSH tunnel..')
      @ssh = Net::SSH.start(
        @ssh_info[:hostname],
        @ssh_info[:user],
        :port => @ssh_info[:ssh_port],
        :keys => [ @sshkey, @ssh_info[:identity_file] ].uniq, # try to use what the user specified first, but fall back to what vagrant says
        :paranoid => false
      )
    else
      # TODO will we ever hit this? or will we be thrown first?
      raise InternalError.new(sprintf('VM is not running[%s], unable open SSH tunnel', status))
    end
  end

  @ssh
end
destroy() click to toggle source

destroy runs `vagrant destroy <name>` from the Vagrantfile path

# File lib/rouster/vagrant.rb, line 86
def destroy
  @logger.info('destroy()')

  # don't like putting this here, may be refactored
  if self.is_passthrough?
    if (self.passthrough[:type].equal?(:aws) or self.passthrough[:type].equal?(:raiden))
      self.aws_destroy()
    elsif self.is_passthrough? and self.passthrough[:type].equal?(:openstack)
      self.ostack_destroy()
    else
      raise InternalError.new(sprintf('failed to execute destroy(), unsupported passthrough type %s', self.passthrough[:type]))
    end
  else
    self.vagrant(sprintf('destroy -f %s', @name))
  end

  disconnect_ssh_tunnel
end
did_exec_fire?(resource_name, puppet_run = self.last_puppet_run) click to toggle source

did_exec_fire?

given the name of an Exec resource, parse the output from the most recent puppet run and return true/false based on whether the exec in question was fired

# File lib/rouster/puppet.rb, line 54
def did_exec_fire?(resource_name, puppet_run = self.last_puppet_run)
  # Notice: /Stage[main]//Exec[foo]/returns: executed successfully
  # Error: /Stage[main]//Exec[bar]/returns: change from notrun to 0 failed: Could not find command '/bin/bar'
  matchers = [
    'Notice: /Stage\[.*\]//Exec\[%s\]/returns: executed successfully',
    'Error: /Stage\[.*\]//Exec\[%s\]/returns: change from notrun to 0 failed'
  ]

  matchers.each do |m|
    matcher = sprintf(m, resource_name)
    return true if puppet_run.match(matcher)
  end

  false
end
dir(dir, cache=false) click to toggle source

dir

runs `ls -ld <dir>` and parses output, returns nil (if dir DNE or permission issue) or hash: {

:directory? => boolean,
:file? => boolean,
:executable? => boolean, # based on user 'vagrant' context
:writeable? => boolean, # based on user 'vagrant' context
:readable? => boolean, # based on user 'vagrant' context
:mode => mode, # 0-prefixed octal mode
:name => name, # short name
:owner => owner,
:group => group,
:size => size, # in bytes

}

parameters

  • <dir> - path of directory to act on, full path or relative to ~vagrant/

  • cache
    • boolean controlling whether to cache retrieved data, defaults to false

# File lib/rouster/tests.rb, line 27
def dir(dir, cache=false)

  if cache and self.deltas[:files].class.eql?(Hash) and ! self.deltas[:files][dir].nil?
    return self.deltas[:files][dir]
  end

  if self.unittest and cache
    # preventing a functional test fallthrough
    return nil
  end

  begin
    raw = self.run(sprintf('ls -ld %s', dir))
  rescue Rouster::RemoteExecutionError
    raw = self.get_output()
  end

  if raw.match(/No such file or directory/)
    res = nil
  elsif raw.match(/Permission denied/)
    @logger.info(sprintf('dir(%s) output[%s], try with sudo', dir, raw)) unless self.uses_sudo?
    res = nil
  else
    res = parse_ls_string(raw)
  end

  if cache
    self.deltas[:files] = Hash.new if self.deltas[:files].nil?
    self.deltas[:files][dir] = res
  end

  res
end
dirs(dir, wildcard='*', insensitive=true, recursive=false) click to toggle source

dirs

runs `find <dir> <recursive muckery> -type d -name '<wildcard>'`, and returns array of directories (fully qualified paths)

parameters

  • <dir> - path to directory to act on, full path or relative to ~vagrant/

  • wildcard
    • glob of directories to match, defaults to '*'

  • recursive
    • boolean controlling whether or not to look in directories recursively, defaults to false

# File lib/rouster/tests.rb, line 70
def dirs(dir, wildcard='*', insensitive=true, recursive=false)
  # TODO use a numerical, not boolean value for 'recursive' -- and rename to 'depth' ?
  raise InternalError.new(sprintf('invalid dir specified[%s]', dir)) unless self.is_dir?(dir)

  raw = self.run(sprintf("find %s %s -type d %s '%s'", dir, recursive ? '' : '-maxdepth 1', insensitive ? '-iname' : '-name', wildcard))
  res = Array.new

  raw.split("\n").each do |line|
    next if line.eql?(dir)
    res.push(line)
  end

  res
end
disconnect_ssh_tunnel() click to toggle source

disconnect_ssh_tunnel

shuts down the persistent Net::SSH tunnel

# File lib/rouster.rb, line 501
def disconnect_ssh_tunnel
  @logger.debug('closing SSH tunnel..')

  @ssh.shutdown! unless @ssh.nil?
  @ssh = nil
end
facter(cache=true, custom_facts=true) click to toggle source

facter

runs facter, returns parsed hash of { fact1 => value1, factN => valueN }

parameters

  • cache
    • whether to store/return cached facter data, if available

  • custom_facts
    • whether to include custom facts in return (uses -p argument)

# File lib/rouster/puppet.rb, line 18
def facter(cache=true, custom_facts=true)

  if cache.true? and ! self.facts.nil?

    if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:facter]) > self.cache_timeout
      @logger.debug(sprintf('invalidating [facter] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:facter]), self.cache_timeout))
      self.facts = nil
    else
      @logger.debug(sprintf('using cached [facter] from [%s]', self.cache[:facter]))
      return self.facts
    end

  end

  raw  = self.run(sprintf('facter %s', custom_facts.true? ? '-p' : ''))
  res  = Hash.new()

  raw.split("\n").each do |line|
    next unless line.match(/(\S*?)\s\=\>\s(.*)/)
    res[$1] = $2
  end

  if cache.true?
    @logger.debug(sprintf('caching [facter] at [%s]', Time.now.asctime))
    self.facts = res
    self.cache[:facter] = Time.now.to_i
  end

  res
end
file(file, cache=false) click to toggle source

file

runs `ls -l <file>` and parses output, returns nil (if file DNE or permission issue) or hash: {

:directory? => boolean,
:file? => boolean,
:executable? => boolean, # based on user 'vagrant' context
:writeable? => boolean, # based on user 'vagrant' context
:readable? => boolean, # based on user 'vagrant' context
:mode => mode, # 0-prefixed octal mode
:name => name, # short name
:owner => owner,
:group => group,
:size => size, # in bytes

}

parameters

  • <file> - path of file to act on, full path or relative to ~vagrant/

  • cache
    • boolean controlling whether to cache retrieved data, defaults to false

# File lib/rouster/tests.rb, line 105
def file(file, cache=false)

  if cache and self.deltas[:files].class.eql?(Hash) and ! self.deltas[:files][file].nil?
    return self.deltas[:files][file]
  end

  if self.unittest and cache
    # preventing a functional test fallthrough
    return nil
  end

  begin
    raw = self.run(sprintf('ls -l %s', file))
  rescue Rouster::RemoteExecutionError
    raw = self.get_output()
  end

  if raw.match(/No such file or directory/)
    @logger.info(sprintf('is_file?(%s) output[%s], try with sudo', file, raw)) unless self.uses_sudo?
    res = nil
  elsif raw.match(/Permission denied/)
    res = nil
  else
    res = parse_ls_string(raw)
  end

  if cache
    self.deltas[:files] = Hash.new if self.deltas[:files].nil?
    self.deltas[:files][file] = res
  end

  res
end
files(dir, wildcard='*', insensitive=true, recursive=false) click to toggle source

files

runs `find <dir> <recursive muckery> -type f -name '<wildcard>'`, and reutns array of files (fullly qualified paths) parameters

  • <dir> - directory to look in, full path or relative to ~vagrant/

  • wildcard
    • glob of files to match, defaults to '*'

  • recursive
    • boolean controlling whether or not to look in directories recursively, defaults to false

# File lib/rouster/tests.rb, line 147
def files(dir, wildcard='*', insensitive=true, recursive=false)
  # TODO use a numerical, not boolean value for 'recursive'
  raise InternalError.new(sprintf('invalid dir specified[%s]', dir)) unless self.is_dir?(dir)

  raw = self.run(sprintf("find %s %s -type f %s '%s'", dir, recursive ? '' : '-maxdepth 1', insensitive ? '-iname' : '-name', wildcard))
  res = Array.new

  raw.split("\n").each do |line|
    res.push(line)
  end

  res
end
generate_unique_mac() click to toggle source

generate_unique_mac

returns a ~unique, valid MAC ht www.commandlinefu.com/commands/view/7242/generate-random-valid-mac-addresses

uses prefix 'b88d12' (actually Apple's prefix) uniqueness is not guaranteed, is really more just 'random'

# File lib/rouster.rb, line 753
def generate_unique_mac
  sprintf('b88d12%s', (1..3).map{"%0.2X" % rand(256)}.join('').downcase)
end
get(remote_file, local_file=nil) click to toggle source

get

downloads a file from VM to host

parameters

  • <remote_file> - full or relative path (based on ~vagrant) of file to download

  • local_file
    • full or relative path (based on $PWD) of file to download to

if no local_file is specified, will be downloaded to $PWD with the same shortname as it had in the VM

returns true on successful download, false if the file DNE and raises a FileTransferError.. well, you know

# File lib/rouster.rb, line 585
def get(remote_file, local_file=nil)
  # TODO what happens when we pass a wildcard as remote_file?

  local_file = local_file.nil? ? File.basename(remote_file) : local_file
  @logger.debug(sprintf('scp from VM[%s] to host[%s]', remote_file, local_file))

  begin
    @ssh.scp.download!(remote_file, local_file)
  rescue => e
    raise FileTransferError.new(sprintf('unable to get[%s], exception[%s]', remote_file, e.message()))
  end

  return true
end
get_catalog(hostname=nil, puppetmaster=nil, facts=nil, puppetmaster_port=8140) click to toggle source

get_catalog

not completely implemented method to get a compiled catalog about a node (based on its facts) from a puppetmaster

original implementation used the catalog face, which does not actually work. switched to an API call, but still need to convert facts into PSON

parameters

  • hostname
    • hostname of node to return catalog for, if not specified, will use `hostname –fqdn`

  • puppetmaster
    • hostname of puppetmaster to use in API call, defaults to 'puppet'

  • facts
    • hash of facts to pass to puppetmaster

  • puppetmaster_port
    • port to talk to the puppetmaster on, defaults to 8140

# File lib/rouster/puppet.rb, line 82
def get_catalog(hostname=nil, puppetmaster=nil, facts=nil, puppetmaster_port=8140)
  # post https://<puppetmaster>/catalog/<node>?facts_format=pson&facts=<pson URL encoded> == ht to patrick@puppetlabs
  certname     = hostname.nil? ? self.run('hostname --fqdn').chomp : hostname
  puppetmaster = puppetmaster.nil? ? 'puppet' : puppetmaster
  facts        = facts.nil? ? self.facter() : facts

  %w(fqdn hostname operatingsystem operatingsystemrelease osfamily rubyversion).each do |required|
    raise ArgumentError.new(sprintf('missing required fact[%s]', required)) unless facts.has_key?(required)
  end

  raise InternalError.new('need to finish conversion of facts to PSON')
  facts.to_pson # this does not work, but needs to

  json = nil
  url  = sprintf('https://%s:%s/catalog/%s?facts_format=pson&facts=%s', puppetmaster, puppetmaster_port, certname, facts)
  uri  = URI.parse(url)

  begin
    res  = Net::HTTP.get(uri)
    json = res.to_json
  rescue => e
    raise ExternalError.new("calling[#{url}] led to exception[#{e}")
  end

  json
end
get_crontab(user='root', cache=true) click to toggle source

get_crontab

runs `crontab -l <user>` and parses output, returns hash: {

user => {
  command => {
    :minute => minute,
    :hour   => hour,
    :dom    => dom, # day of month
    :mon    => mon, # month
    :dow    => dow, # day of week
  }
}

}

the hash will contain integers (not strings) for numerical values – all but '*'

parameters

  • <user> - name of user who owns crontab for examination – or '*' to determine list of users and iterate over them to find all cron jobs

  • cache
    • boolean controlling whether or not retrieved/parsed data is cached, defaults to true

# File lib/rouster/deltas.rb, line 30
def get_crontab(user='root', cache=true)

  if cache and self.deltas[:crontab].class.eql?(Hash)

    if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:crontab]) > self.cache_timeout
      @logger.debug(sprintf('invalidating [crontab] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:crontab]), self.cache_timeout))
      self.deltas.delete(:crontab)
    end

    if self.deltas.has_key?(:crontab) and self.deltas[:crontab].has_key?(user)
      @logger.debug(sprintf('using cached [crontab] from [%s]', self.cache[:crontab]))
      return self.deltas[:crontab][user]
    elsif self.deltas.has_key?(:crontab) and user.eql?('*')
      @logger.debug(sprintf('using cached [crontab] from [%s]', self.cache[:crontab]))
      return self.deltas[:crontab]
    else
      # noop fallthrough to gather data to cache
    end

  end

  res = Hash.new
  users = nil

  if user.eql?('*')
    users = self.get_users().keys
  else
    users = [user]
  end

  users.each do |u|
    begin
      raw = self.run(sprintf('crontab -u %s -l', u))
    rescue RemoteExecutionError => e
      # crontab throws a non-0 exit code if there is no crontab for the specified user
      res[u] ||= Hash.new
      next
    end

    raw.split("\n").each do |line|
      next if line.match(/^#|^\s+$/)
      elements = line.split("\s")

      if elements.size < 5
        # this is usually (only?) caused by ENV_VARIABLE=VALUE directives
        @logger.debug(sprintf('line [%s] did not match format expectations for a crontab entry, skipping', line))
        next
      end

      command = elements[5..elements.size].join(' ')

      res[u] ||= Hash.new

      if res[u][command].class.eql?(Hash)
        unique = elements.join('')
        command = sprintf('%s-duplicate.%s', command, unique)
        @logger.info(sprintf('duplicate crontab command found, adding hash key[%s]', command))
      end

      res[u][command]           = Hash.new
      res[u][command][:minute]  = elements[0]
      res[u][command][:hour]    = elements[1]
      res[u][command][:dom]     = elements[2]
      res[u][command][:mon]     = elements[3]
      res[u][command][:dow]     = elements[4]
    end
  end

  if cache
    @logger.debug(sprintf('caching [crontab] at [%s]', Time.now.asctime))

    if ! user.eql?('*')
      self.deltas[:crontab] ||= Hash.new
      self.deltas[:crontab][user] ||= Hash.new
      self.deltas[:crontab][user] = res[user]
    else
      self.deltas[:crontab] ||= Hash.new
      self.deltas[:crontab] = res
    end

    self.cache[:crontab] = Time.now.to_i

  end

  return user.eql?('*') ? res : res[user]
end
get_groups(cache=true, deep=true) click to toggle source

get_groups

cats /etc/group and parses output, returns hash: {

groupN => {
  :gid => gid,
  :users => [user1, userN]
}

}

parameters

  • cache
    • boolean controlling whether data retrieved/parsed is cached, defaults to true

  • deep
    • boolean controlling whether get_users() is called in order to correctly populate res[:users]

# File lib/rouster/deltas.rb, line 131
def get_groups(cache=true, deep=true)

  if cache and ! self.deltas[:groups].nil?

    if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:groups]) > self.cache_timeout
      @logger.debug(sprintf('invalidating [groups] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:groups]), self.cache_timeout))
      self.deltas.delete(:groups)
    else
      @logger.debug(sprintf('using cached [groups] from [%s]', self.cache[:groups]))
      return self.deltas[:groups]
    end

  end

  res = Hash.new()

  {
    :file    => self.run('cat /etc/group'),
    :dynamic => self.run('getent group', [0,127]),
  }.each_pair do |source, raw|

    raw.split("\n").each do |line|
      next unless line.match(/\w+:\w*:\w+/)

      data = line.split(':')

      group = data[0]
      gid   = data[2]

      # this works in some cases, deep functionality picks up the others
      users = data[3].nil? ? ['NONE'] : data[3].split(',')

      if res.has_key?(group)
        @logger.debug(sprintf('for[%s] old GID[%s] new GID[%s]', group, gid, res[group][:users])) unless gid.eql?(res[group][:gid])
        @logger.debug(sprintf('for[%s] old users[%s] new users[%s]', group, users)) unless users.eql?(res[group][:users])
      end

      res[group] = Hash.new() # i miss autovivification
      res[group][:gid]    = gid
      res[group][:users]  = users
      res[group][:source] = source
    end

  end

  groups = res

  if deep
    users = self.get_users(cache)

    known_valid_gids = groups.keys.map { |g| groups[g][:gid] } # no need to calculate this in every loop

    # TODO better, much better -- since the number of users/groups is finite and usually small, this is a low priority
    users.each_key do |user|
      # iterate over each user to get their gid
      gid = users[user][:gid]

      unless known_valid_gids.member?(gid)
        @logger.warn(sprintf('found user[%s] with unknown GID[%s], known GIDs[%s]', user, gid, known_valid_gids))
        next
      end

      ## do this more efficiently
      groups.each_key do |group|
        # iterate over each group to find the matching gid
        if gid.eql?(groups[group][:gid])
          if groups[group][:users].eql?(['NONE'])
            groups[group][:users] = []
          end
          groups[group][:users] << user unless groups[group][:users].member?(user)
        end

      end

    end
  end

  if cache
    @logger.debug(sprintf('caching [groups] at [%s]', Time.now.asctime))
    self.deltas[:groups] = groups
    self.cache[:groups]  = Time.now.to_i
  end

  groups
end
get_output(index = 1) click to toggle source

get_output

returns output from commands passed through _run() and run()

if no parameter passed, returns output from the last command run

parameters

  • index
    • positive or negative indexing of LIFO datastructure

# File lib/rouster.rb, line 741
def get_output(index = 1)
  index.is_a?(Fixnum) and index > 0 ? self.output[-index] : self.output[index]
end
get_packages(cache=true, deep=true) click to toggle source

get_packages

runs an OS appropriate command to gather list of packages, returns hash: {

packageN => {
  package => version|? # if 'deep', attempts to parse version numbers
}

}

parameters

  • cache
    • boolean controlling whether data retrieved/parsed is cached, defaults to true

  • deep
    • boolean controlling whether to attempt to parse extended information (see supported OS), defaults to true

supported OS

  • OSX - runs `pkgutil –pkgs` and `pkgutil –pkg-info=<package>` (if deep)

  • RedHat - runs `rpm -qa –qf “%{n}@%{v}@%{arch}n”` (does not support deep)

  • Solaris - runs `pkginfo` and `pkginfo -l <package>` (if deep)

  • Ubuntu - runs `dpkg-query -W -f='${Package}@${Version}@${Architecture}n'` (does not support deep)

raises InternalError if unsupported operating system

# File lib/rouster/deltas.rb, line 238
def get_packages(cache=true, deep=true)
  if cache and ! self.deltas[:packages].nil?

    if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:packages]) > self.cache_timeout
      @logger.debug(sprintf('invalidating [packages] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:packages]), self.cache_timeout))
      self.deltas.delete(:packages)
    else
      @logger.debug(sprintf('using cached [packages] from [%s]', self.cache[:packages]))
      return self.deltas[:packages]
    end

  end

  res = Hash.new()

  os = self.os_type

  if os.eql?(:osx)

    raw = self.run('pkgutil --pkgs')
    raw.split("\n").each do |line|
      name    = line
      arch    = '?'
      version = '?'

      if deep
        # can get install time, volume and location as well
        local_res = self.run(sprintf('pkgutil --pkg-info=%s', name))
        version   = $1 if local_res.match(/version\:\s+(.*?)$/)
      end

      if res.has_key?(name)
        # different architecture of an already known package
        @logger.debug(sprintf('found package with already known name[%s], value[%s], new line[%s], turning into array', name, res[name], line))
        new_element = { :version => version, :arch => arch }
        res[name]   = [ res[name], new_element ]
      else
        res[name] = { :version => version, :arch => arch }
      end

    end

  elsif os.eql?(:solaris)
    raw = self.run('pkginfo')
    raw.split("\n").each do |line|
      next if line.match(/(.*?)\s+(.*?)\s(.*)$/).nil?
      name    = $2
      arch    = '?'
      version = '?'

      if deep
        begin
          # who throws non-0 exit codes when querying for legit package information? solaris does.
          local_res = self.run(sprintf('pkginfo -l %s', name))
          arch      = $1 if local_res.match(/ARCH\:\s+(.*?)$/)
          version   = $1 if local_res.match(/VERSION\:\s+(.*?)$/)
        rescue
          arch    = '?' if arch.nil?
          version = '?' if arch.nil?
        end
      end

      if res.has_key?(name)
        # different architecture of an already known package
        @logger.debug(sprintf('found package with already known name[%s], value[%s], new line[%s], turning into array', name, res[name], line))
        new_element = { :version => version, :arch => arch }
        res[name]   = [ res[name], new_element ]
      else
        res[name] = { :version => version, :arch => arch }
      end
    end

  elsif os.eql?(:ubuntu) or os.eql?(:debian)
    raw = self.run("dpkg-query -W -f='${Package}@${Version}@${Architecture}\n'")
    raw.split("\n").each do |line|
      next if line.match(/(.*?)\@(.*?)\@(.*)/).nil?
      name    = $1
      version = $2
      arch    = $3

      if res.has_key?(name)
        # different architecture of an already known package
        @logger.debug(sprintf('found package with already known name[%s], value[%s], new line[%s], turning into array', name, res[name], line))
        new_element = { :version => version, :arch => arch }
        res[name]   = [ res[name], new_element ]
      else
        res[name] = { :version => version, :arch => arch }
      end

    end

  elsif os.eql?(:rhel)
    raw = self.run('rpm -qa --qf "%{n}@%{v}@%{arch}\n"')
    raw.split("\n").each do |line|
      next if line.match(/(.*?)\@(.*?)\@(.*)/).nil?
      name    = $1
      version = $2
      arch    = $3

      if res.has_key?(name)
        # different architecture of an already known package
        @logger.debug(sprintf('found package with already known name[%s], value[%s], new line[%s], turning into array', name, res[name], line))
        new_element = { :version => version, :arch => arch }
        res[name]   = [ res[name], new_element ]
      else
        res[name] = { :version => version, :arch => arch }
      end

    end

  else
    raise InternalError.new(sprintf('VM operating system[%s] not currently supported', os))
  end

  if cache
    @logger.debug(sprintf('caching [packages] at [%s]', Time.now.asctime))
    self.deltas[:packages] = res
    self.cache[:packages]  = Time.now.to_i
  end

  res
end
get_ports(cache=false) click to toggle source

get_ports

runs an OS appropriate command to gather port information, returns hash: {

protocolN => {
  portN => {
    :addressN => state
  }
}

}

parameters

  • cache
    • boolean controlling whether data retrieved/parsed is cached, defaults to true

supported OS

  • RedHat, Ubuntu - runs `netstat -ln`

raises InternalError if unsupported operating system

# File lib/rouster/deltas.rb, line 380
def get_ports(cache=false)
  # TODO add unix domain sockets
  # TODO improve ipv6 support

  if cache and ! self.deltas[:ports].nil?
    if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:ports]) > self.cache_timeout
      @logger.debug(sprintf('invalidating [ports] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:ports]), self.cache_timeout))
      self.deltas.delete(:ports)
    else
      @logger.debug(sprintf('using cached [ports] from [%s]', self.cache[:ports]))
      return self.deltas[:ports]
    end
  end

  res = Hash.new()
  os  = self.os_type()

  if os.eql?(:rhel) or os.eql?(:ubuntu) or os.eql?(:debian)

    raw = self.run('netstat -ln')

    raw.split("\n").each do |line|

      next unless line.match(/(\w+)\s+\d+\s+\d+\s+([\S\:]*)\:(\w*)\s.*?(\w+)\s/) or line.match(/(\w+)\s+\d+\s+\d+\s+([\S\:]*)\:(\w*)\s.*?(\w*)\s/)

      protocol = $1
      address  = $2
      port     = $3
      state    = protocol.eql?('udp') ? 'you_might_not_get_it' : $4

      res[protocol] = Hash.new if res[protocol].nil?
      res[protocol][port] = Hash.new if res[protocol][port].nil?
      res[protocol][port][:address] = Hash.new if res[protocol][port][:address].nil?
      res[protocol][port][:address][address] = state

    end
  else
    raise InternalError.new(sprintf('unable to get port information from VM operating system[%s]', os))
  end

  if cache
    @logger.debug(sprintf('caching [ports] at [%s]', Time.now.asctime))
    self.deltas[:ports] = res
    self.cache[:ports]  = Time.now.to_i
  end

  res
end
get_puppet_errors(input=nil) click to toggle source

get_puppet_errors

parses input for puppet errors, returns array of strings

parameters

  • input
    • string to look at, defaults to self.get_output()

# File lib/rouster/puppet.rb, line 116
def get_puppet_errors(input=nil)
  str       = input.nil? ? self.get_output() : input
  errors    = nil
  errors_27 = str.scan(/35merr:.*/)
  errors_30 = str.scan(/Error:.*/)

  # TODO this is a little less than efficient, don't scan for 3.0 if you found 2.7
  if errors_27.size > 0
    errors = errors_27
  else
    errors = errors_30
  end

  errors.empty? ? nil : errors
end
get_puppet_notices(input=nil) click to toggle source

get_puppet_notices

parses input for puppet notices, returns array of strings

parameters

  • input
    • string to look at, defaults to self.get_output()

# File lib/rouster/puppet.rb, line 139
def get_puppet_notices(input=nil)
  str        = input.nil? ? self.get_output() : input
  notices    = nil
  notices_27 = str.scan(/36mnotice:.*/) # not sure when this stopped working
  notices_30 = str.scan(/Notice:.*/)

  # TODO this is a little less than efficient, don't scan for 3.0 if you found 2.7
  if notices_27.size > 0
    notices = notices_27
  else
    notices = notices_30
  end

  notices.empty? ? nil : notices
end
get_puppet_version() click to toggle source

get_puppet_version

executes `puppet –version` and returns parsed version string or nil

# File lib/rouster/puppet.rb, line 159
def get_puppet_version
  version   = nil
  installed = self.is_in_path?('puppet')

  if installed
    raw = self.run('puppet --version')
    version = raw.match(/([\d\.]*)\s/) ? $1 : nil
  else
    version = nil
  end

  version
end
get_services(cache=true, humanize=true, type=:all, seed=nil) click to toggle source

get_services

runs an OS appropriate command to gather service information, returns hash: {

serviceN => mode # exists|installed|operational|running|stopped|unsure

}

parameters

  • cache
    • boolean controlling whether data retrieved/parsed is cached, defaults to true

  • humanize
    • boolean controlling whether data retrieved is massaged into simplified names or returned mostly as retrieved

  • type
    • symbol indicating which service controller to query, defaults to :all

  • seed
    • test hook to seed the output of service commands

supported OS and types

  • OSX - :launchd

  • RedHat - :systemv or :upstart

  • Solaris - :smf

  • Ubuntu - :systemv or :upstart

notes

  • raises InternalError if unsupported operating system

  • OSX, Solaris and Ubuntu/Debian will only return running|stopped|unsure, the exists|installed|operational modes are RHEL/CentOS only

# File lib/rouster/deltas.rb, line 453
def get_services(cache=true, humanize=true, type=:all, seed=nil)
  if cache and ! self.deltas[:services].nil?

    if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:services]) > self.cache_timeout
      @logger.debug(sprintf('invalidating [services] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:services]), self.cache_timeout))
      self.deltas.delete(:services)
    else
      @logger.debug(sprintf('using cached [services] from [%s]', self.cache[:services]))
      return self.deltas[:services]
    end

  end

  res = Hash.new()
  os  = self.os_type

  commands = {
    :osx => {
      :launchd => 'launchctl list',
    },
    :solaris => {
      :smf => 'svcs -a',
    },

    # TODO we really need to implement something like osfamily
    :ubuntu => {
      :systemv => 'service --status-all 2>&1',
      :upstart => 'initctl list',
    },
    :debian => {
      :systemv => 'service --status-all 2>&1',
      :upstart => 'initctl list',
    },
    :rhel => {
      :systemd => 'systemctl list-units --type=service --no-pager',
      :systemv => 'service --status-all',
      :upstart => 'initctl list',
    },

    :invalid => {
      :invalid => 'invalid',
    },
  }

  if type.eql?(:all)
    type = commands[os].keys
  end

  type = type.class.eql?(Array) ? type : [ type ]

  type.each do |provider|

    raise InternalError.new(sprintf('unable to get service information from VM operating system[%s]', os)) if provider.eql?(:invalid)
    raise ArgumentError.new(sprintf('unable to find command provider[%s] for [%s]', provider, os))  if commands[os][provider].nil?

    unless seed or self.is_in_path?(commands[os][provider].split(' ').first)
      @logger.info(sprintf('skipping provider[%s], not in $PATH[%s]', provider, commands[os][provider]))
      next
    end

    @logger.info(sprintf('get_services using provider [%s] on [%s]', provider, os))

    # TODO while this is true, what if self.user is 'root'.. -- the problem is we don't have self.user, and we store this data differently depending on self.passthrough?
    @logger.warn('gathering service information typically works better with sudo, which is currently not being used') unless self.uses_sudo?

    # TODO come up with a better test hook -- real problem is that we can't seed 'raw' with different values per iteration
    raw = seed.nil? ? self.run(commands[os][provider]) : seed

    if os.eql?(:osx)

      raw.split("\n").each do |line|
        next if line.match(/(?:\S*?)\s+(\S*?)\s+(\S+)$/).nil?
        tokens = line.split("\s")
        service = tokens[-1]
        mode    = tokens[0]

        if humanize # should we do this with a .freeze instead?
          if mode.match(/^\d/)
            mode = 'running'
          elsif mode.match(/-/)
            mode = 'stopped'
          else
            next # this should handle the banner "PID     Status  Label"
          end
        end

        res[service] = mode
      end

    elsif os.eql?(:solaris)

      raw.split("\n").each do |line|
        next if line.match(/(.*?)\s+(?:.*?)\s+(.*?)$/).nil?

        service = $2
        mode    = $1

        if humanize
          if mode.match(/^online/)
            mode = 'running'
          elsif mode.match(/^legacy_run/)
            mode = 'running'
          elsif mode.match(/^disabled/)
            mode = 'stopped'
          end

          if service.match(/^svc:\/.*\/(.*?):.*/)
            # turning 'svc:/network/cswpuppetd:default' into 'cswpuppetd'
            service = $1
          elsif service.match(/^lrc:\/.*?\/.*\/(.*)/)
            # turning 'lrc:/etc/rcS_d/S50sk98Sol' into 'S50sk98Sol'
            service = $1
          end
        end

        res[service] = mode

      end

    elsif os.eql?(:ubuntu) or os.eql?(:debian)

      raw.split("\n").each do |line|
        if provider.eql?(:systemv)
            next if line.match(/\[(.*?)\]\s+(.*)$/).nil?
            mode    = $1
            service = $2

            if humanize
              mode = 'stopped' if mode.match('-')
              mode = 'running' if mode.match('\+')
              mode = 'unsure'  if mode.match('\?')
            end

            res[service] = mode
        elsif provider.eql?(:upstart)
            if line.match(/(.*?)\s.*?(.*?),/)
              # tty (/dev/tty3) start/running, process 1601
              # named start/running, process 8959
              service = $1
              mode    = $2
            elsif line.match(/(.*?)\s(.*)/)
              # rcS stop/waiting
              service = $1
              mode    = $2
            else
              @logger.warn("unable to process upstart line[#{line}], skipping")
              next
            end

            if humanize
              mode = 'stopped' if mode.match('stop/waiting')
              mode = 'running' if mode.match('start/running')
              mode = 'unsure'  unless mode.eql?('stopped') or mode.eql?('running')
            end

            res[service] = mode
        end
      end

    elsif os.eql?(:rhel)

      raw.split("\n").each do |line|
        if provider.eql?(:systemv)
          if humanize
            if line.match(/^(\w+?)\sis\s(.*)$/)
              # <service> is <state>
              name = $1
              state = $2
              res[name] = state

              if state.match(/^not/)
                # this catches 'Kdump is not operational'
                res[name] = 'stopped'
              end

            elsif line.match(/^(\w+?)\s\(pid.*?\)\sis\s(\w+)$/)
              # <service> (pid <pid> [pid]) is <state>...
              res[$1] = $2
            elsif line.match(/^(\w+?)\sis\s(\w+)\.*$/) # not sure this is actually needed
              @logger.debug('triggered supposedly unnecessary regex')
              # <service> is <state>. whatever
              res[$1] = $2
            elsif line.match(/razor_daemon:\s(\w+).*$/)
              # razor_daemon: running [pid 11325]
              # razor_daemon: no instances running
              res['razor_daemon'] = $1.eql?('running') ? $1 : 'stopped'
            elsif line.match(/^(\w+?)\:.*?(\w+)$/)
              # <service>: whatever <state>
              res[$1] = $2
            elsif line.match(/^(\w+?):.*?\sis\snot\srunning\.$/)
              # ip6tables: Firewall is not running.
              res[$1] = 'stopped'
            elsif line.match(/^(\w+?)\s.*?\s(.*)$/)
              # netconsole module not loaded
              state = $2
              res[$1] = $2.match(/not/) ? 'stopped' : 'running'
            elsif line.match(/^(\w+)\s(\w+).*$/)
              # <process> <state> whatever
              res[$1] = $2
            else
              # original regex implementation, if we didn't match anything else, failover to this
              next if line.match(/^([^\s:]*).*\s(\w*)(?:\.?){3}$/).nil?
              res[$1] = $2
            end
          else
            next if line.match(/^([^\s:]*).*\s(\w*)(?:\.?){3}$/).nil?
            res[$1] = $2
          end
        elsif provider.eql?(:upstart)

          if line.match(/(.*?)\s.*?(.*?),/)
            # tty (/dev/tty3) start/running, process 1601
            # named start/running, process 8959
            service = $1
            mode    = $2
          elsif line.match(/(.*?)\s(.*)/)
            # rcS stop/waiting
            service = $1
            mode    = $2
          else
            @logger.warn("unable to process upstart line[#{line}], skipping")
            next
          end

          if humanize
            mode = 'stopped' if mode.match('stop/waiting')
            mode = 'running' if mode.match('start/running')
            mode = 'unsure'  unless mode.eql?('stopped') or mode.eql?('running')
          end

          res[service] = mode unless res.has_key?(service)

        elsif provider.eql?(:systemd)
          # UNIT              LOAD   ACTIVE   SUB      DESCRIPTION
          # nfs-utils.service loaded inactive dead     NFS server and client services
          # crond.service     loaded active   running  Command Scheduler

          if line.match(/^\W*(.*?)\.service\s+(?:.*?)\s+(.*?)\s+(.*?)\s+(?:.*?)$/) # 5 space separated characters
            service = $1
            active  = $2
            sub     = $3

            if humanize
              mode = sub.match('running') ? 'running' : 'stopped'
              mode = 'unsure'  unless mode.eql?('stopped') or mode.eql?('running')
            end

            res[service] = mode
          else
            # not logging here, there is a bunch of garbage output at the end of the output that we can't seem to suppress
            next
          end

        end

      end
    else
      raise InternalError.new(sprintf('unable to get service information from VM operating system[%s]', os))
    end


    # end of provider processing
  end

  # issue #63 handling
  # TODO should we consider using symbols here instead?
  allowed_modes = %w(exists installed operational running stopped unsure)
  failover_mode = 'unsure'

  if humanize
    res.each_pair do |k,v|
      next if allowed_modes.member?(v)
      @logger.debug(sprintf('replacing service[%s] status of [%s] with [%s] for uniformity', k, v, failover_mode))
      res[k] = failover_mode
    end
  end

  if cache
    @logger.debug(sprintf('caching [services] at [%s]', Time.now.asctime))
    self.deltas[:services] = res
    self.cache[:services]  = Time.now.to_i
  end

  res
end
get_ssh_info() click to toggle source

get_ssh_info

runs `vagrant ssh-config <name>` from the Vagrantfile path

returns a hash containing required data for opening an SSH connection to a VM, to be consumed by connect_ssh_tunnel()

# File lib/rouster.rb, line 403
def get_ssh_info

  h = Hash.new()

  if @ssh_info.class.eql?(Hash)
    @logger.debug('using cached SSH info')
    h = @ssh_info
  else

    res = self.vagrant(sprintf('ssh-config %s', @name))

    res.split("\n").each do |line|
      if line.match(/HostName (.*?)$/)
        h[:hostname] = $1
      elsif line.match(/User (\w*?)$/)
        h[:user] = $1
      elsif line.match(/Port (\d*?)$/)
        h[:ssh_port] = $1
      elsif line.match(/IdentityFile (.*?)$/)
        h[:identity_file] = $1
        @logger.info(sprintf('vagrant specified key[%s] differs from provided[%s], will use both', @sshkey, h[:identity_file]))
      end
    end

    @ssh_info = h
  end

  h
end
get_users(cache=true) click to toggle source

get_users

cats /etc/passwd and parses output, returns hash: {

userN => {
  :gid => gid,
  :home  => path_of_homedir,
  :home_exists => boolean_of_is_dir?(:home),
  :shell => path_to_shell,
  :uid => uid
}

} parameters

  • cache
    • boolean controlling whether data retrieved/parsed is cached, defaults to true

# File lib/rouster/deltas.rb, line 754
def get_users(cache=true)
  if cache and ! self.deltas[:users].nil?

    if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:users]) > self.cache_timeout
      @logger.debug(sprintf('invalidating [users] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:users]), self.cache_timeout))
      self.deltas.delete(:users)
    else
      @logger.debug(sprintf('using cached [users] from [%s]', self.cache[:users]))
      return self.deltas[:users]
    end

  end

  res = Hash.new()

  {
    :file    => self.run('cat /etc/passwd'),
    :dynamic => self.run('getent passwd', [0,127]),
  }.each do |source, raw|

    raw.split("\n").each do |line|
      next if line.match(/([\w\.-]+)(?::\w+){3,}/).nil?

      user = $1
      data = line.split(':')

      shell       = data[-1]
      home        = data[-2]
      home_exists = self.is_dir?(data[-2])
      uid         = data[2]
      gid         = data[3]

      if res.has_key?(user)
        @logger.info(sprintf('for[%s] old shell[%s], new shell[%s]', user, res[user][:shell], shell)) unless shell.eql?(res[user][:shell])
        @logger.info(sprintf('for[%s] old home[%s], new home[%s]', user, res[user][:home], home)) unless home.eql?(res[user][:home])
        @logger.info(sprintf('for[%s] old home_exists[%s], new home_exists[%s]', user, res[user][:home_exists], home_exists)) unless home_exists.eql?(res[user][:home_exists])
        @logger.info(sprintf('for[%s] old UID[%s], new UID[%s]', user, res[user][:uid], uid)) unless uid.eql?(res[user][:uid])
        @logger.info(sprintf('for[%s] old GID[%s], new GID[%s]', user, res[user][:gid], gid)) unless gid.eql?(res[user][:gid])
      end

      res[user] = Hash.new()
      res[user][:shell]       = shell
      res[user][:home]        = home
      res[user][:home_exists] = home_exists
      res[user][:uid]         = uid
      res[user][:gid]         = gid
      res[user][:source]      = source
    end
  end

  if cache
    @logger.debug(sprintf('caching [users] at [%s]', Time.now.asctime))
    self.deltas[:users] = res
    self.cache[:users]  = Time.now.to_i
  end

  res
end
halt() click to toggle source

halt runs `vagrant halt <name>` from the Vagrantfile path

# File lib/rouster/vagrant.rb, line 70
def halt
  @logger.info('halt()')
  self.vagrant(sprintf('halt %s', @name))
end
hiera(key, facts=nil, config='/etc/puppet/hiera.yaml', options=nil) click to toggle source

hiera

returns hiera results from self

parameters

  • <key> - hiera key to look up

  • facts
    • hash of facts to be used in hiera lookup (technically optional, but most useful hiera lookups are based on facts)

  • config
    • path to hiera configuration – this is only optional if you have a hiera.yaml file in ~/vagrant, default option is correct for most puppet installations

  • options
    • any additional parameters to be passed to hiera directly

note

  • if no facts are provided, facter() will be called - to really run hiera without facts, send an empty hash

  • this method is mostly useful on your puppet master, as your agents won't likely have /etc/puppet/hiera.yaml - to get data on another node, specify it's facts and call hiera on your ppm

# File lib/rouster/puppet.rb, line 187
def hiera(key, facts=nil, config='/etc/puppet/hiera.yaml', options=nil)
  # TODO implement caching? where do we keep it? self.hiera{}? or self.deltas{} -- leaning towards #1

  cmd = 'hiera'

  if facts.nil?
    @logger.info('no facts provided, calling facter() automatically')
    facts = self.facter()
  end

  if facts.keys.size > 0
    scope_file = sprintf('/tmp/rouster-hiera_scope.%s.%s.json', $$, Time.now.to_i)

    File.write(scope_file, facts.to_json)
    self.put(scope_file, scope_file)
    File.delete(scope_file)

    cmd << sprintf(' -j %s', scope_file)
  end

  cmd << sprintf(' -c %s', config) unless config.nil?
  cmd << sprintf(' %s', options) unless options.nil?
  cmd << sprintf(' %s', key)

  raw = self.run(cmd)

  JSON.parse(raw)
end
inspect() click to toggle source

inspect

overloaded method to return useful information about Rouster objects

# File lib/rouster.rb, line 275
def inspect
  s = self.status()
  "name [#{@name}]:
    is_available_via_ssh?[#{self.is_available_via_ssh?}],
    passthrough[#{@passthrough}],
    sshkey[#{@sshkey}],
    status[#{s}],
    sudo[#{@sudo}],
    vagrantfile[#{@vagrantfile}],
    verbosity console[#{@verbosity_console}] / log[#{@verbosity_logfile} - #{@logfile}]\n"
end
is_available_via_ssh?() click to toggle source

is_available_via_ssh?

returns true or false after:

  • attempting to establish SSH tunnel if it is not currently up/open

  • running a functional test of the tunnel

# File lib/rouster.rb, line 357
def is_available_via_ssh?
  res = nil

  if @cache_timeout
    if @cache.has_key?(:is_available_via_ssh?)
      if (Time.now.to_i - @cache[:is_available_via_ssh?][:time]) < @cache_timeout
        @logger.debug(sprintf('using cached is_available_via_ssh?[%s] from [%s]', @cache[:is_available_via_ssh?][:status], @cache[:is_available_via_ssh?][:time]))
        return @cache[:is_available_via_ssh?][:status]
      end
    end
  end

  if @ssh.nil? or @ssh.closed?
    begin
      res = self.connect_ssh_tunnel()
    rescue Rouster::InternalError, Net::SSH::Disconnect, Errno::ECONNREFUSED, Errno::ECONNRESET => e
      res = false
    end

  end

  if res.nil? or res.is_a?(Net::SSH::Connection::Session)
    begin
      self.run('echo functional test of SSH tunnel')
        res = true
    rescue
      res = false
    end
  end

  if @cache_timeout
    @cache[:is_available_via_ssh?] = Hash.new unless @cache[:is_available_via_ssh?].class.eql?(Hash)
    @cache[:is_available_via_ssh?][:time] = Time.now.to_i
    @cache[:is_available_via_ssh?][:status] = res
    @logger.debug(sprintf('caching is_available_via_ssh?[%s] at [%s]', @cache[:is_available_via_ssh?][:status], @cache[:is_available_via_ssh?][:time]))
  end

  res
end
is_dir?(dir) click to toggle source

is_dir?

uses dir() to return boolean indicating whether parameter passed is a directory

parameters

  • <dir> - path of directory to validate

# File lib/rouster/tests.rb, line 168
def is_dir?(dir)
  res = nil
  begin
    res = self.dir(dir)
  rescue => e
    return false
  end

  res.class.eql?(Hash) ? res[:directory?] : false
end
is_executable?(filename, level='u') click to toggle source

is_executable?

uses file() to return boolean indicating whether parameter passed is an executable file

parameters

  • <filename> - path of filename to validate

  • level
    • string indicating 'u'ser, 'g'roup or 'o'ther context, defaults to 'u'

# File lib/rouster/tests.rb, line 187
def is_executable?(filename, level='u')
  res = nil

  begin
    res = file(filename)
  rescue Rouster::InternalError
    res = dir(filename)
  end

  # for cases that are directories, but don't throw exceptions
  if res.nil? or res[:directory?]
    res = dir(filename)
  end

  if res
    array = res[:executable?]

    case level
      when 'u', 'U', 'user'
        array[0]
      when 'g', 'G', 'group'
        array[1]
      when 'o', 'O', 'other'
        array[2]
      else
        raise InternalError.new(sprintf('unknown level[%s]'))
    end

  else
    false
  end

end
is_file?(file) click to toggle source

is_file?

uses file() to return boolean indicating whether parameter passed is a file

parameters

  • <file> - path of filename to validate

# File lib/rouster/tests.rb, line 228
def is_file?(file)
  res = nil

  begin
    res = self.file(file)
  rescue => e
    return false
  end

  res.class.eql?(Hash) ? res[:file?] : false
end
is_group?(group) click to toggle source

is_group?

uses get_groups() to return boolean indicating whether parameter passed is a group

parameters

  • <group> - name of group to validate

# File lib/rouster/tests.rb, line 247
def is_group?(group)
  groups = self.get_groups()
  groups.has_key?(group)
end
is_in_file?(file, regex, flags='', scp=false) click to toggle source

is_in_file?

calls `grep -c '<regex>' <file>` and returns boolean for whether one or more matches are found in file

parameters

  • <file> - path of filename to examine

  • <regex> - regular expression/string to be passed to grep

  • <flags> - flags to include in grep command

  • scp
    • downloads file to host machine before grepping (functionality not implemented, was planned when a new SSH connection was required for each run() command, not sure it is necessary any longer)

# File lib/rouster/tests.rb, line 262
def is_in_file?(file, regex, flags='', scp=false)

  res = nil

  if scp
    # download the file to a temporary directory
    @logger.warn('is_in_file? scp option not implemented yet')
  end

  begin
    command = sprintf("grep -c%s '%s' %s", flags, regex, file)
    res     = self.run(command)
  rescue Rouster::RemoteExecutionError
    return false
  end

  if res.nil?.false? and res.match(/^0/)
    false
  else
    true
  end

end
is_in_path?(filename) click to toggle source

is_in_path?

runs `which <filename>`, returns boolean of whether the filename is exectuable and in $PATH

parameters

  • <filename> - name of executable to validate

# File lib/rouster/tests.rb, line 293
def is_in_path?(filename)
  begin
    self.run(sprintf('which %s', filename))
  rescue Rouster::RemoteExecutionError
    return false
  end

  true
end
is_package?(package, cache=true) click to toggle source

is_package?

uses get_packages() to return boolean indicating whether passed parameter is an installed package

parameters

  • <package> - name of package to validate

  • cache
    • boolean controlling whether to cache results from get_packages(), defaults to true (for performance)

# File lib/rouster/tests.rb, line 311
def is_package?(package, cache=true)
  # TODO should we implement something like is_package_version?()
  packages = self.get_packages(cache)
  packages.has_key?(package)
end
is_passthrough?() click to toggle source

is_passthrough?

convenience getter for @passthrough truthiness

# File lib/rouster.rb, line 627
def is_passthrough?
  @passthrough.class.eql?(Hash)
end
is_port_active?(port, proto='tcp', cache=false) click to toggle source

is_port_active?

uses get_ports() to return boolean indicating whether passed port is in use

parameters

  • <port> - port number to validate

  • proto
    • specification of protocol to examine, defaults to tcp

  • cache
    • boolean controlling whether to cache get_ports() data, defaults to false

# File lib/rouster/tests.rb, line 326
def is_port_active?(port, proto='tcp', cache=false)
  # TODO is this the right name?
  ports = self.get_ports(cache)
  port  = port.to_s
  if ports[proto].class.eql?(Hash) and ports[proto].has_key?(port)

    if proto.eql?('tcp')
      ['ACTIVE', 'ESTABLISHED', 'LISTEN']. each do |allowed|
        return true if ports[proto][port][:address].values.member?(allowed)
      end
    else
      return true
    end

  end

  false
end
is_port_open?(port, proto='tcp', cache=false) click to toggle source

is_port_open?

uses get_ports() to return boolean indicating whether passed port is open

parameters

  • <port> - port number to validate

  • proto
    • specification of protocol to examine, defaults to tcp

  • cache
    • boolean controlling whether to cache get_ports() data, defaults to false

# File lib/rouster/tests.rb, line 354
def is_port_open?(port, proto='tcp', cache=false)
  ports = self.get_ports(cache)
  port  = port.to_s
  if ports[proto].class.eql?(Hash) and ports[proto].has_key?(port)
    return false
  end

  true
end
is_process_running?(name) click to toggle source

is_process_running?

runs `ps ax | grep -c <process>` looking for more than 2 results

parameters

  • <name> - name of process to look for

supported OS

  • OSX

  • RedHat

  • Ubuntu

# File lib/rouster/tests.rb, line 376
def is_process_running?(name)
  # TODO support Solaris
  # TODO do better validation than just grepping for a matching filename, start with removing 'grep' from output
  begin

    os = self.os_type()

    case os
      when :rhel, :osx, :ubuntu, :debian
        res = self.run(sprintf('ps ax | grep -c %s', name))
      else
        raise InternalError.new(sprintf('currently unable to determine running process list on OS[%s]', os))
    end

  rescue Rouster::RemoteExecutionError
    return false
  end

  res.chomp.to_i > 2 # because of the weird way our process is run through the ssh tunnel
end
is_readable?(filename, level='u') click to toggle source

is_readable?

uses file() to return boolean indicating whether parameter passed is an readable file

parameters

  • <filename> - path of filename to validate

  • level
    • string indicating 'u'ser, 'g'roup or 'o'ther context, defaults to 'u'

# File lib/rouster/tests.rb, line 405
def is_readable?(filename, level='u')
  res = nil

  begin
    res = file(filename)
  rescue Rouster::InternalError
    res = dir(filename)
  end

  # for cases that are directories, but don't throw exceptions
  if res.nil? or res[:directory?]
    res = dir(filename)
  end

  if res
    array = res[:readable?]

    case level
      when 'u', 'U', 'user'
        array[0]
      when 'g', 'G', 'group'
        array[1]
      when 'o', 'O', 'other'
        array[2]
      else
        raise InternalError.new(sprintf('unknown level[%s]'))
    end

  else
    false
  end

end
is_service?(service, cache=true) click to toggle source

is_service?

uses get_services() to return boolean indicating whether passed parameter is an installed service

parameters

  • <service> - name of service to validate

  • cache
    • boolean controlling whether to cache results from get_services(), defaults to true

# File lib/rouster/tests.rb, line 447
def is_service?(service, cache=true)
  services = self.get_services(cache)
  services.has_key?(service)
end
is_service_running?(service, cache=false) click to toggle source

is_service_running?

uses get_services() to return boolean indicating whether passed parameter is a running service

parameters

  • <service> - name of service to validate

  • cache
    • boolean controlling whether to cache results from get_services(), defaults to false

# File lib/rouster/tests.rb, line 461
def is_service_running?(service, cache=false)
  services = self.get_services(cache)

  if services.has_key?(service)
    services[service].eql?('running').true?
  else
    false
  end
end
is_user?(user, cache=true) click to toggle source

is_user?

uses get_users() to return boolean indicating whether passed parameter is a user

parameters

  • <user> - username to validate

  • cache
    • boolean controlling whether to cache results from get_users(), defaults to true

# File lib/rouster/tests.rb, line 498
def is_user?(user, cache=true)
  users = self.get_users(cache)
  users.has_key?(user)
end
is_user_in_group?(user, group, cache=true) click to toggle source

is_user_in_group?

uses get_users() and get_groups() to return boolean indicating whether passed user is in passed group

parameters

  • <user> - username to validate

  • <group> - group expected to contain user

  • cache
    • boolean controlling whether to cache results from get_users() and get_groups(), defaults to true

# File lib/rouster/tests.rb, line 512
def is_user_in_group?(user, group, cache=true)
  # TODO can we scope this down to just use get_groups?
  users  = self.get_users(cache)
  groups = self.get_groups(cache)

  users.has_key?(user) and groups.has_key?(group) and groups[group][:users].member?(user)
end
is_vagrant_running?() click to toggle source

is_vagrant_running?()

returns true|false if a vagrant process is running on the host machine

meant to be used to prevent race-y conditions when interacting with VirtualBox (potentially others, haven't tested)

# File lib/rouster/vagrant.rb, line 194
def is_vagrant_running?
  res = false

  begin
    # TODO would like to get the 2 -v greps into a single call..
    raw = self._run("ps -ef | grep -v 'grep' | grep -v 'ssh' | grep '#{self.vagrantbinary}'")
    res = true
  rescue
  end

  @logger.debug(sprintf('is_vagrant_running?[%s]', res))
  res
end
is_writeable?(filename, level='u') click to toggle source

is_writeable?

uses file() to return boolean indicating whether parameter passed is an executable file

parameters

  • <filename> - path of filename to validate

  • level
    • string indicating 'u'ser, 'g'roup or 'o'ther context, defaults to 'u'

# File lib/rouster/tests.rb, line 528
def is_writeable?(filename, level='u')
  res = nil

  begin
    res = file(filename)
  rescue Rouster::InternalError
    res = dir(filename)
  end

  # for cases that are directories, but don't throw exceptions
  if res.nil? or res[:directory?]
    res = dir(filename)
  end

  if res
    array = res[:writeable?]

    case level
      when 'u', 'U', 'user'
        array[0]
      when 'g', 'G', 'group'
        array[1]
      when 'o', 'O', 'other'
        array[2]
      else
        raise InternalError.new(sprintf('unknown level[%s]'))
    end

  else
    false
  end

end
os_type() click to toggle source

os_type

attempts to determine VM operating system based on `uname -a` output, supports OSX, Sun|Solaris, Ubuntu and Redhat

# File lib/rouster.rb, line 512
def os_type

  if @ostype
    return @ostype
  end

  res = :invalid

  Rouster.os_files.each_pair do |os, f|
    [ f ].flatten.each do |candidate|
      if self.is_file?(candidate)
        next if candidate.eql?('/etc/os-release') and ! self.is_in_file?(candidate, os.to_s, 'i') # CentOS detection
        @logger.debug(sprintf('determined OS to be[%s] via[%s]', os, candidate))
        res = os
      end
    end
    break unless res.eql?(:invalid)
  end

  @logger.error(sprintf('unable to determine OS, looking for[%s]', Rouster.os_files)) if res.eql?(:invalid)

  @ostype = res
  res
end
os_version(os_type) click to toggle source

os_version

# File lib/rouster.rb, line 541
def os_version(os_type)
  return @osversion if @osversion

  res = :invalid

  [ Rouster.os_files[os_type] ].flatten.each do |candidate|
    if self.is_file?(candidate)
      next if candidate.eql?('/etc/os-release') and ! self.is_in_file?(candidate, os_type.to_s, 'i') # CentOS detection
      contents = self.run(sprintf('cat %s', candidate))
      if os_type.eql?(:ubuntu)
        version = $1 if contents.match(/.*VERSION\="(\d+\.\d+).*"/) # VERSION="13.10, Saucy Salamander"
        res = version unless version.nil?
      elsif os_type.eql?(:rhel)
        version = $1 if contents.match(/.*VERSION\="(\d+)"/) # VERSION="7 (Core)"
        version = $1 if version.nil? and contents.match(/.*(\d+.\d+)/) # CentOS release 6.4 (Final)
        res = version unless version.nil?
      elsif os_type.eql?(:osx)
        version = $1 if contents.match(/<key>ProductVersion<\/key>.*<string>(.*)<\/string>/m) # <key>ProductVersion</key>\n          <string>10.12.1</string>
        res = version unless version.nil?
      end

    end
    break unless res.eql?(:invalid)
  end

  @logger.error(sprintf('unable to determine OS version, looking for[%s]', Rouster.os_files[os_type])) if res.eql?(:invalid)

  @osversion = res

  res
end
package(provider='virtualbox') click to toggle source

package – though vagrant docs still refer to 'repackage' runs `vagrant package <name> <provider>`

# File lib/rouster/vagrant.rb, line 78
def package(provider='virtualbox') # TODO get the provider as a first class citizen on the rouster object
  @logger.info(sprintf('package(%s)', provider))
  self.vagrant(sprintf('package %s %s', @name, provider))
end
parse_catalog(catalog) click to toggle source

parse_catalog

looks at the ['data’] keys in catalog for Files, Groups, Packages, Services and Users, returns hash of expectations compatible with validate_*

this is a very lightly tested implementation, please open issues as necessary

parameters

  • <catalog> - JSON string or Hash representation of catalog, typically from get_catalog()

# File lib/rouster/puppet.rb, line 225
def parse_catalog(catalog)
  classes   = nil
  resources = nil
  results   = Hash.new()

  if catalog.is_a?(String)
    begin
      JSON.parse!(catalog)
    rescue
      raise InternalError.new(sprintf('unable to parse catalog[%s] as JSON', catalog))
    end
  end

  unless catalog.has_key?('data') and catalog['data'].has_key?('classes')
    raise InternalError.new(sprintf('catalog does not contain a classes key[%s]', catalog))
  end

  unless catalog.has_key?('data') and catalog['data'].has_key?('resources')
    raise InternalError.new(sprintf('catalog does not contain a resources key[%s]', catalog))
  end

  raw_resources = catalog['data']['resources']

  raw_resources.each do |r|
    # samples of eacb type of resource is available at
    # https://github.com/chorankates/rouster/issues/20#issuecomment-18635576
    #
    # we can do a lot better here
    type = r['type']
    case type
      when 'Class'
        classes.push(r['title'])
      when 'File'
        name = r['title']
        resources[name] = Hash.new()

        resources[name][:type]      = :file
        resources[name][:directory] = false
        resources[name][:ensure]    = r['ensure'] ||= 'present'
        resources[name][:file]      = true
        resources[name][:group]     = r['parameters'].has_key?('group') ? r['parameters']['group'] : nil
        resources[name][:mode]      = r['parameters'].has_key?('mode')  ? r['parameters']['mode']  : nil
        resources[name][:owner]     = r['parameters'].has_key?('owner') ? r['parameters']['owner'] : nil
        resources[name][:contains]  = r.has_key?('content') ? r['content'] : nil

      when 'Group'
        name = r['title']
        resources[name] = Hash.new()

        resources[name][:type]   = :group
        resources[name][:ensure] = r['ensure'] ||= 'present'
        resources[name][:gid]    = r['parameters'].has_key?('gid') ? r['parameters']['gid'] : nil

      when 'Package'
        name = r['title']
        resources[name] = Hash.new()

        resources[name][:type]    = :package
        resources[name][:ensure]  = r['ensure'] ||= 'present'
        resources[name][:version] = r['ensure'] =~ /\d/ ? r['ensure'] : nil

      when 'Service'
        name = r['title']
        resources[name] = Hash.new()

        resources[name][:type]   = :service
        resources[name][:ensure] = r['ensure'] ||= 'present'
        resources[name][:state]  = r['ensure']

      when 'User'
        name = r['title']
        resources[name] = Hash.new()

        resources[name][:type]   = :user
        resources[name][:ensure] = r['ensure'] ||= 'present'
        resources[name][:home]   = r['parameters'].has_key?('home')   ? r['parameters']['home']   : nil
        resources[name][:gid]    = r['parameters'].has_key?('gid')    ? r['parameters']['gid']    : nil
        resources[name][:group]  = r['parameters'].has_key?('groups') ? r['parameters']['groups'] : nil
        resources[name][:shell]  = r['parameters'].has_key?('shell')  ? r['parameters']['shell']  : nil
        resources[name][:uid]    = r['parameters'].has_key?('uid')    ? r['parameters']['uid']    : nil

      else
        raise NotImplementedError.new(sprintf('parsing support for [%s] is incomplete', type))
    end

  end

  # remove all nil references
  resources.each_key do |name|
    resources[name].each_pair do |k,v|
      unless v
        resources[name].delete(k)
      end
    end
  end

  results[:classes]   = classes
  results[:resources] = resources

  results
end
parse_ls_string(string) click to toggle source
non-test, helper methods

private

# File lib/rouster/tests.rb, line 564
def parse_ls_string(string)
  # ht avaghti

  res = Hash.new()

  tokens = string.split(/\s+/)

  # eww - do better here
  modes = [ tokens[0][1..3], tokens[0][4..6], tokens[0][7..9] ]
  mode  = 0

  # can't use modes.size here (or could, but would have to -1)
  for i in 0..2 do
    value   = 0
    element = modes[i]

    for j in 0..2 do
      chr = element[j].chr
      case chr
        when 'r'
          value += 4
        when 'w'
          value += 2
        when 'x', 't', 's'
          # is 't' / 's' really right here? copying Salesforce::Vagrant
          value += 1
        when '-'
          # noop
        else
          raise InternalError.new(sprintf('unexpected character[%s] in string[%s]', chr, string))
      end

    end

    mode = sprintf('%s%s', mode, value)
  end

  res[:mode]  = mode
  res[:owner] = tokens[2]
  res[:group] = tokens[3]
  res[:size]  = tokens[4]

  res[:directory?]  = tokens[0][0].chr.eql?('d')
  res[:file?]       = ! res[:directory?]
  res[:symlink?]    = tokens[0][0].chr.eql?('l')
  res[:executable?] = [ tokens[0][3].chr.eql?('x'), tokens[0][6].chr.eql?('x'), tokens[0][9].chr.eql?('x') || tokens[0][9].chr.eql?('t') ]
  res[:writeable?]  = [ tokens[0][2].chr.eql?('w'), tokens[0][5].chr.eql?('w'), tokens[0][8].chr.eql?('w') ]
  res[:readable?]   = [ tokens[0][1].chr.eql?('r'), tokens[0][4].chr.eql?('r'), tokens[0][7].chr.eql?('r') ]

  # TODO better here: this does not support files/dirs with spaces
  if res[:symlink?]
    # not sure if we should only be adding this value if we're a symlink, or adding it to all results and just using nil if not a link
    res[:target] = tokens[-1]
    res[:name]   = tokens[-3]
  else
    res[:name] = tokens[-1]
  end

  res
end
put(local_file, remote_file=nil) click to toggle source

put

uploads a file from host to VM

parameters

  • <local_file> - full or relative path (based on $PWD) of file to upload

  • remote_file
    • full or relative path (based on ~vagrant) of filename to upload to

# File lib/rouster.rb, line 608
def put(local_file, remote_file=nil)
  remote_file = remote_file.nil? ? File.basename(local_file) : remote_file
  @logger.debug(sprintf('scp from host[%s] to VM[%s]', local_file, remote_file))

  raise FileTransferError.new(sprintf('unable to put[%s], local file does not exist', local_file)) unless File.file?(local_file)

  begin
    @ssh.scp.upload!(local_file, remote_file)
  rescue => e
    raise FileTransferError.new(sprintf('unable to put[%s], exception[%s]', local_file, e.message()))
  end

  return true
end
rebuild() click to toggle source

rebuild

destroy and then up the machine in question

# File lib/rouster.rb, line 643
def rebuild
  @logger.debug('rebuild()')
  self.destroy
  self.up
end
reload(no_provision = true) click to toggle source

reload

runs `vagrant reload <name> [–no-provision]` from the Vagrantfile path no_provision Boolean whether or not to stop reprovisioning

# File lib/rouster/vagrant.rb, line 167
def reload(no_provision = true)

  if self.is_passthrough?
    @logger.warn('calling [vagrant reload] on a passthrough host is a noop')
    return nil
  end

  @logger.info('reload()')
  self.vagrant(sprintf('reload %s %s', @name, no_provision ? '--no-provision' : ''))
end
remove_existing_certs(except) click to toggle source

remove_existing_certs

… removes existing certificates - really only useful when called on a puppetmaster useful in testing environments where you want to destroy/rebuild agents without rebuilding the puppetmaster every time (think autosign)

parameters

  • <puppetmaster> - string/partial regex of certificate names to keep

# File lib/rouster/puppet.rb, line 337
def remove_existing_certs (except)
  except = except.kind_of?(Array) ? except : [except] # need to move from <>.class.eql? to <>.kind_of? in a number of places
  hosts  = Array.new()

  res = self.run('puppet cert list --all')

  # TODO refactor this away from the hacky_break
  res.each_line do |line|
    hacky_break = false

    except.each do |exception|
      next if hacky_break
      hacky_break = line.match(/#{exception}/)
    end

    next if hacky_break

    host = $1 if line.match(/^\+\s"(.*?)"/)

    hosts.push(host) unless host.nil? # only want to clear signed certs
  end

  hosts.each do |host|
    self.run(sprintf('puppet cert --clean %s', host))
  end

end
remove_specific_cert(targets) click to toggle source

remove_specific_cert

… removes a specific (or several specific) certificates, effectively the reverse of remove_existing_certs() - and again, really only useful when called on a puppet master

# File lib/rouster/puppet.rb, line 369
def remove_specific_cert (targets)
  targets = targets.kind_of?(Array) ? targets : [targets]
  hosts = Array.new()

  res = self.run('puppet cert list --all')

  res.each_line do |line|
    hacky_break = true

    targets.each do |target|
      next unless hacky_break
      hacky_break = line.match(/#{target}/)
    end

    next unless hacky_break

    host = $1 if line.match(/^\+\s"(.*?)"/)
    hosts.push(host) unless host.nil?

  end

  hosts.each do |host|
    self.run(sprintf('puppet cert --clean %s', host))
  end

end
restart(wait=nil, expected_exitcodes = [0]) click to toggle source

restart

runs `shutdown -rf now` in the VM, optionally waits for machine to come back to life

parameters

  • wait
    • number of seconds to wait until is_available_via_ssh?() returns true before assuming failure

# File lib/rouster.rb, line 656
def restart(wait=nil, expected_exitcodes = [0])
  @logger.debug('restart()')

  if self.is_passthrough? and self.passthrough[:type].eql?(:local)
    @logger.warn(sprintf('intercepted [restart] sent to a local passthrough, no op'))
    return nil
  end

  if @vagrant_reboot
    # leading vagrant handle this through 'reload --no-provision'
    self.reload
  else
    # trying to do it ourselves
    case os_type
      when :osx
        self.run('shutdown -r now', expected_exitcodes)
      when :rhel, :ubuntu
        if os_type.eql?(:rhel) and os_version(os_type).match(/7/)
          self.run('shutdown --halt --reboot now', expected_exitcodes << 256)
        else
          self.run('shutdown -rf now')
        end
      when :solaris
        self.run('shutdown -y -i5 -g0', expected_exitcodes)
      else
        raise InternalError.new(sprintf('unsupported OS[%s]', @ostype))
    end
  end

  @ssh, @ssh_info = nil # severing the SSH tunnel, getting ready in case this box is brought back up on a different port

  if wait
    inc = wait.to_i / 10
    0.upto(9) do |e|
      @logger.debug(sprintf('waiting for reboot: round[%s], step[%s], total[%s]', e, inc, wait))
      return true if self.is_available_via_ssh?()
      sleep inc
    end

    return false
  end

  return true
end
run(command, expected_exitcode=[0]) click to toggle source

run

runs a command inside the Vagrant VM

returns output (STDOUT and STDERR) from command run, sets @exitcode currently determines exitcode by tacking a 'echo $?' onto the command being run, which is then parsed out before returning

parameters

  • <command> - the command to run (sudo will be prepended if specified in object instantiation)

  • expected_exitcode
    • allows for non-0 exit codes to be returned without requiring exception handling

# File lib/rouster.rb, line 301
def run(command, expected_exitcode=[0])

  if @ssh.nil?
    self.connect_ssh_tunnel
  end

  output = nil
  expected_exitcode = [expected_exitcode] unless expected_exitcode.class.eql?(Array) # yuck, but 2.0 no longer coerces strings into single element arrays

  cmd = sprintf('%s%s; echo ec[$?]', self.uses_sudo? ? 'sudo ' : '', command)
  @logger.info(sprintf('vm running: [%s]', cmd)) # TODO decide whether this should be changed in light of passthroughs.. 'remotely'?

  0.upto(@retries) do |try|
    begin
      if self.is_passthrough? and self.passthrough[:type].eql?(:local)
        output = `#{cmd}`
      else
        output = @ssh.exec!(cmd)
      end

      break
    rescue => e
      @logger.error(sprintf('failed to run [%s] with [%s], attempt[%s/%s]', cmd, e, try, retries)) if self.retries > 0
      sleep 10 # TODO need to expose this as a variable
    end

  end

  if output.nil?
    output    = "error gathering output, last logged output[#{self.get_output()}]"
    @exitcode = 256
  elsif output.match(/ec\[(\d+)\]/)
    @exitcode = $1.to_i
    output.gsub!(/ec\[(\d+)\]\n/, '')
  else
    @exitcode = 1
  end

  self.output.push(output)
  @logger.debug(sprintf('output: [%s]', output))

  unless expected_exitcode.member?(@exitcode)
    # TODO technically this could be a 'LocalPassthroughExecutionError' now too if local passthrough.. should we update?
    raise RemoteExecutionError.new("output[#{output}], exitcode[#{@exitcode}], expected[#{expected_exitcode}]")
  end

  @exitcode ||= 0
  output
end
run_puppet(mode='master', passed_opts={}) click to toggle source

run_puppet

… runs puppet on self, returns nothing

currently supports 2 methods of running puppet:

* master - runs 'puppet agent -t'
  * supported options
    * expected_exitcode - string/integer/array of acceptable exit code(s)
    * configtimeout - string/integer of the acceptable configtimeout value
    * environment - string of the environment to use
    * certname - string of the certname to use in place of the host fqdn
    * pluginsync - bool value if pluginsync should be used
    * server - string value of the puppetmasters fqdn / ip
    * additional_options - string of various options that would be passed to puppet
* masterless - runs 'puppet apply <options>' after determining version of puppet running and adjusting arguments
  * supported options
    * expected_exitcode - string/integer/array of acceptable exit code(s)
    * hiera_config - path to hiera configuration -- only supported by Puppet 3.0+
    * manifest_file - string/array of strings of paths to manifest(s) to apply
    * manifest_dir - string/array of strings of directories containing manifest(s) to apply - is recursive
    * module_dir - path to module directory -- currently a required parameter, is this correct?
    * environment - string of the environment to use (default: production)
    * certname - string of the certname to use in place of the host fqdn (default: unused)
    * pluginsync - bool value if pluginsync should be used (default: true)
    * additional_options - string of various options that would be passed to puppet

parameters

  • mode
    • method to run puppet, defaults to 'master'

  • opts
    • hash of additional options

# File lib/rouster/puppet.rb, line 427
def run_puppet(mode='master', passed_opts={})

  if mode.eql?('master')
    opts = {
      :expected_exitcode  => 0,
      :configtimeout      => nil,
      :environment        => nil,
      :certname           => nil,
      :server             => nil,
      :pluginsync         => false,
      :additional_options => nil
    }.merge!(passed_opts)

    cmd = 'puppet agent -t'
    cmd << sprintf(' --configtimeout %s', opts[:configtimeout]) unless opts[:configtimeout].nil?
    cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
    cmd << sprintf(' --certname %s', opts[:certname]) unless opts[:certname].nil?
    cmd << sprintf(' --server %s', opts[:server]) unless opts[:server].nil?
    cmd << ' --pluginsync' if opts[:pluginsync]
    cmd << opts[:additional_options] unless opts[:additional_options].nil?

    self.run(cmd, opts[:expected_exitcode])

  elsif mode.eql?('masterless')
    opts = {
      :expected_exitcode  => 2,
      :hiera_config       => nil,
      :manifest_file      => nil, # can be a string or array, will 'puppet apply' each
      :manifest_dir       => nil, # can be a string or array, will 'puppet apply' each module in the dir (recursively)
      :module_dir         => nil,
      :environment        => nil,
      :certname           => nil,
      :pluginsync         => false,
      :additional_options => nil
    }.merge!(passed_opts)

    ## validate arguments -- can do better here (:manifest_dir, :manifest_file)
    puppet_version = self.get_puppet_version() # hiera_config specification is only supported in >3.0, but NOT required anywhere

    if opts[:hiera_config]
      if puppet_version > '3.0'
        raise InternalError.new(sprintf('invalid hiera config specified[%s]', opts[:hiera_config])) unless self.is_file?(opts[:hiera_config])
      else
        @logger.error(sprintf('puppet version[%s] does not support --hiera_config, ignoring', puppet_version))
      end
    end

    if opts[:module_dir]
      raise InternalError.new(sprintf('invalid module dir specified[%s]', opts[:module_dir])) unless self.is_dir?(opts[:module_dir])
    end

    if opts[:manifest_file]
      opts[:manifest_file] = opts[:manifest_file].class.eql?(Array) ? opts[:manifest_file] : [opts[:manifest_file]]
      opts[:manifest_file].each do |file|
        raise InternalError.new(sprintf('invalid manifest file specified[%s]', file)) unless self.is_file?(file)

        cmd = 'puppet apply --detailed-exitcodes'
        cmd << sprintf(' --modulepath=%s', opts[:module_dir]) unless opts[:module_dir].nil?
        cmd << sprintf(' --hiera_config=%s', opts[:hiera_config]) unless opts[:hiera_config].nil? or puppet_version < '3.0'
        cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
        cmd << sprintf(' --certname %s', opts[:certname]) unless opts[:certname].nil?
        cmd << ' --pluginsync' if opts[:pluginsync]
        cmd << sprintf(' %s', opts[:additional_options]) unless opts[:additional_options].nil?
        cmd << sprintf(' %s', file)

        self.last_puppet_run = self.run(cmd, opts[:expected_exitcode])
      end
    end

    if opts[:manifest_dir]
      opts[:manifest_dir] = opts[:manifest_dir].class.eql?(Array) ? opts[:manifest_dir] : [opts[:manifest_dir]]
      opts[:manifest_dir].each do |dir|
        raise InternalError.new(sprintf('invalid manifest dir specified[%s]', dir)) unless self.is_dir?(dir)

        manifests = self.files(dir, '*.pp', true)

        manifests.each do |m|

          cmd = 'puppet apply --detailed-exitcodes'
          cmd << sprintf(' --modulepath=%s', opts[:module_dir]) unless opts[:module_dir].nil?
          cmd << sprintf(' --hiera_config=%s', opts[:hiera_config]) unless opts[:hiera_config].nil? or puppet_version < '3.0'
          cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
          cmd << sprintf(' --certname %s', opts[:certname]) unless opts[:certname].nil?
          cmd << ' --pluginsync' if opts[:pluginsync]
          cmd << sprintf(' %s', opts[:additional_options]) unless opts[:additional_options].nil?
          cmd << sprintf(' %s', m)

          self.last_puppet_run = self.run(cmd, opts[:expected_exitcode])
        end

      end
    end

  else
    raise InternalError.new(sprintf('unknown mode [%s]', mode))
  end


end
sandbox_available?() click to toggle source

sandbox_available?

returns true or false after attempting to find out if the sandbox subcommand is available

# File lib/rouster/vagrant.rb, line 213
def sandbox_available?
  if self.is_passthrough?
    @logger.warn('sandbox* methods on a passthrough host is a noop')
    return nil
  end

  if @cache.has_key?(:sandbox_available?)
    @logger.debug(sprintf('using cached sandbox_available?[%s]', @cache[:sandbox_available?]))
    return @cache[:sandbox_available?]
  end

  @logger.info('sandbox_available()')
  begin
    # at some point, vagrant changed its behavior on exit code here, so rescuing
    self._run(sprintf('cd %s; vagrant', File.dirname(@vagrantfile))) # calling 'vagrant' without parameters to determine available faces
  rescue
  end

  sandbox_available = false
  if self.get_output().match(/^\s+sandbox$/)
    sandbox_available = true
  end

  @cache[:sandbox_available?] = sandbox_available
  @logger.debug(sprintf('caching sandbox_available?[%s]', @cache[:sandbox_available?]))
  @logger.error('sandbox support is not available, please install the "sahara" gem first, https://github.com/jedi4ever/sahara') unless sandbox_available

  return sandbox_available
end
sandbox_commit() click to toggle source

sandbox_commit runs `vagrant sandbox commit` from the Vagrantfile path

# File lib/rouster/vagrant.rb, line 296
def sandbox_commit
  if self.is_passthrough?
    @logger.warn('sandbox* methods on a passthrough host is a noop')
    return nil
  end

  if self.sandbox_available?
    self.disconnect_ssh_tunnel
    self.vagrant(sprintf('sandbox commit %s', @name))
    self.connect_ssh_tunnel
  else
    raise ExternalError.new('sandbox plugin not installed')
  end
end
sandbox_off() click to toggle source

sandbox_off runs `vagrant sandbox off` from the Vagrantfile path

# File lib/rouster/vagrant.rb, line 262
def sandbox_off
  if self.is_passthrough?
    @logger.warn('sandbox* methods on a passthrough host is a noop')
    return nil
  end

  if self.sandbox_available?
    return self.vagrant(sprintf('sandbox off %s', @name))
  else
    raise ExternalError.new('sandbox plugin not installed')
  end
end
sandbox_on() click to toggle source

sandbox_on runs `vagrant sandbox on` from the Vagrantfile path

# File lib/rouster/vagrant.rb, line 246
def sandbox_on
  if self.is_passthrough?
    @logger.warn('sandbox* methods on a passthrough host is a noop')
    return nil
  end

  if self.sandbox_available?
    return self.vagrant(sprintf('sandbox on %s', @name))
  else
    raise ExternalError.new('sandbox plugin not installed')
  end
end
sandbox_rollback() click to toggle source

sandbox_rollback runs `vagrant sandbox rollback` from the Vagrantfile path

# File lib/rouster/vagrant.rb, line 278
def sandbox_rollback
  if self.is_passthrough?
    @logger.warn('sandbox* methods on a passthrough host is a noop')
    return nil
  end

  if self.sandbox_available?
    self.disconnect_ssh_tunnel
    self.vagrant(sprintf('sandbox rollback %s', @name))
    self.connect_ssh_tunnel
  else
    raise ExternalError.new('sandbox plugin not installed')
  end
end
status() click to toggle source

status

runs `vagrant status <name>` from the Vagrantfile path parses the status and provider out of output, but only status is returned

# File lib/rouster/vagrant.rb, line 110
def status
  status = nil

  if @cache_timeout
    if @cache.has_key?(:status)
      if (Time.now.to_i - @cache[:status][:time]) < @cache_timeout
        @logger.debug(sprintf('using cached status[%s] from [%s]', @cache[:status][:status], @cache[:status][:time]))
        return @cache[:status][:status]
      end
    end
  end

  # don't like putting this here, may be refactored
  @logger.info('status()')
  if self.is_passthrough?
    if (self.passthrough[:type].equal?(:aws) or self.passthrough[:type].equal?(:raiden))
      status = self.aws_status()
    elsif self.passthrough[:type].equal?(:openstack)
      status = self.ostack_status()
    else
      raise InternalError.new(sprintf('failed to execute status(), unsupported passthrough type %s', self.passthrough[:type]))
    end
  else
    self.vagrant(sprintf('status %s', @name))

    # else case here (both for nil/non-matching output) is handled by non-0 exit code
    output = self.get_output()
    if output.nil?
      if self.is_passthrough?() and self.passthrough[:type].eql?(:local)
        status = 'running'
      else
        status = 'not-created'
      end
    elsif output.match(/^#{@name}\s*(.*\s?\w+)\s\((.+)\)$/)
      # vagrant 1.2+, $1 = status, $2 = provider
      status = $1
    elsif output.match(/^#{@name}\s+(.+)$/)
      # vagrant 1.2-, $1 = status
      status = $1
    end
  end

  if @cache_timeout
    @cache[:status] = Hash.new unless @cache[:status].class.eql?(Hash)
    @cache[:status][:time] = Time.now.to_i
    @cache[:status][:status] = status
    @logger.debug(sprintf('caching status[%s] at [%s]', @cache[:status][:status], @cache[:status][:time]))
  end

  return status
end
suspend() click to toggle source

suspend

runs `vagrant suspend <name>` from the Vagrantfile path

# File lib/rouster/vagrant.rb, line 182
def suspend
  @logger.info('suspend()')
  self.vagrant(sprintf('suspend %s', @name))
  disconnect_ssh_tunnel() unless self.is_passthrough?()
end
traverse_up(startdir=Dir.pwd, filename=nil, levels=10) click to toggle source

traverse_up

overly complex function to find a file (Vagrantfile, in our case) somewhere up the tree

returns the first matching filename or nil if none found

parameters

  • startdir
    • directory to start looking in, default is current directory

  • filename
    • filename you are looking for

  • levels
    • number of directory levels to examine, default is 10

# File lib/rouster.rb, line 768
def traverse_up(startdir=Dir.pwd, filename=nil, levels=10)
  raise InternalError.new('must specify a filename') if filename.nil?

  @logger.debug(sprintf('traverse_up() looking for [%s] in [%s], up to [%s] levels', filename, startdir, levels)) unless @logger.nil?

  dirs  = startdir.split('/')
  count = 0

  while count < levels and ! dirs.nil?

    potential = sprintf('%s/Vagrantfile', dirs.join('/'))

    if File.file?(potential)
      return potential
    end

    dirs.pop()
    count += 1
  end
end
up() click to toggle source

up runs `vagrant up <name>` from the Vagrantfile path if :sshtunnel is passed to the object during instantiation, the tunnel is created here as well

# File lib/rouster/vagrant.rb, line 47
def up
  @logger.info('up()')

  # don't like putting this here, may be refactored
  if self.is_passthrough?
    if (self.passthrough[:type].equal?(:aws) or self.passthrough[:type].equal?(:raiden))
       self.aws_up()
    elsif (self.passthrough[:type].equal?(:openstack))
       self.ostack_up()
    else
       self.vagrant(sprintf('up %s', @name))
    end
  else
    self.vagrant(sprintf('up %s', @name))
  end

  @ssh_info = nil # in case the ssh-info has changed, a la destroy/rebuild
  self.connect_ssh_tunnel() if @sshtunnel
end
uses_sudo?() click to toggle source

uses_sudo?

convenience getter for @sudo truthiness

# File lib/rouster.rb, line 635
def uses_sudo?
   @sudo.eql?(true)
end
vagrant(face, sleep_time=10) click to toggle source

vagrant

abstraction layer to call vagrant faces

parameters

  • <face> - vagrant face to call (include arguments)

# File lib/rouster/vagrant.rb, line 14
def vagrant(face, sleep_time=10)
  if self.is_passthrough?
    @logger.warn(sprintf('calling [vagrant %s] on a passthrough host is a noop', face))
    return nil
  end

  unless @vagrant_concurrency.eql?(true)
    # TODO don't (ab|re)use variables
    0.upto(@retries) do |try|
      break if self.is_vagrant_running?().eql?(false)

      sleep sleep_time # TODO log a message?
    end
  end

  0.upto(@retries) do |try| # TODO should really be doing this with 'retry', but i think this code is actually cleaner
    begin
      return self._run(sprintf('cd %s; vagrant %s', File.dirname(@vagrantfile), face))
    rescue
      @logger.error(sprintf('failed vagrant command[%s], attempt[%s/%s]', face, try, retries)) if self.retries > 0
      sleep sleep_time
    end
  end

  raise InternalError.new(sprintf('failed to execute [%s], exitcode[%s], output[%s]', face, self.exitcode, self.get_output()))


end
validate_cron(user, name, expectations, fail_fast=false) click to toggle source

validate_cron

given the name of the user who owns crontab, the cron's command to execute and a hash of expectations, returns true|false whether cron matches expectations

parameters

  • <user> - name of user who owns crontab

  • <name> - the cron's command to execute

  • <expectations> - hash of expectations, see examples

  • <fail_fast> - return false immediately on any failure (default is false)

example expectations: 'username', '/home/username/test.pl', {

:ensure => 'present',
:minute => 1,
:hour   => 0,
:dom    => '*',
:mon    => '*',
:dow    => '*',

}

'root', 'printf > /var/log/apache/error_log', {

:minute => 59,
:hour   => [8, 12],
:dom    => '*',
:mon    => '*',
:dow    => '*',

}

supported keys:

* :exists|:ensure -- defaults to present if not specified
* :minute
* :hour
* :dom -- day of month
* :mon -- month
* :dow  -- day of week
* :constrain
# File lib/rouster/testing.rb, line 47
def validate_cron(user, name, expectations, fail_fast=false)
  if user.nil?
    raise InternalError.new('no user specified constraint')
  end

  crontabs = self.get_crontab(user)

  if expectations[:ensure].nil? and expectations[:exists].nil?
    expectations[:ensure] = 'present'
  end

  if expectations.has_key?(:constrain)
    expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]

    expectations[:constrain].each do |constraint|
      fact, expectation = constraint.split("\s")
      unless meets_constraint?(fact, expectation)
        @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
        return true
      end
    end

    expectations.delete(:constrain)
  end

  results = Hash.new()
  local = nil

  expectations.each do |k,v|
    case k
      when :ensure, :exists
        if crontabs.has_key?(name)
          if v.to_s.match(/absent|false/).nil?
            local = true
          else
            local = false
          end
        else
          local = v.to_s.match(/absent|false/).nil? ? false : true
        end
      when :minute, :hour, :dom, :mon, :dow
        if crontabs.has_key?(name) and crontabs[name].has_key?(k) and crontabs[name][k].to_s.eql?(v.to_s)
          local = true
        else
          local = false
        end
      else
        raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
    end

    return false if local.eql?(false) and fail_fast.eql?(true)
    results[k] = local
  end

  @logger.info("#{name} [#{expectations}] => #{results}")
  results.find{|k,v| v.false? }.nil?
end
validate_file(name, expectations, fail_fast=false, cache=false) click to toggle source

validate_file

given a filename and a hash of expectations, returns true|false whether file matches expectations

parameters

  • <name> - full file name or relative (to ~vagrant)

  • <expectations> - hash of expectations, see examples

  • <fail_fast> - return false immediately on any failure (default is false)

example expectations: '/sys/kernel/mm/redhat_transparent_hugepage/enabled', {

:contains => 'never',

},

'/etc/fstab', {

:contains  => '/dev/fioa*/iodata*xfs',
:constrain => 'is_virtual false' # syntax is '<fact> <expected>', file is only tested if <expected> matches <actual>
:exists    => 'file',
:mode      => '0644'

},

'/etc/hosts', {

:constrain => ['! is_virtual true', 'is_virtual false'],
:mode      => '0644'

}

'/etc/nrpe.cfg', {

:ensure   => 'file',
:contains => ['dont_blame_nrpe=1', 'allowed_hosts=' ]

}

supported keys:

* :exists|:ensure -- defaults to file if not specified
* :file
* :directory
* :contains (string or array)
* :mode/:permissions
* :size
* :owner
* :group
* :constrain
# File lib/rouster/testing.rb, line 147
def validate_file(name, expectations, fail_fast=false, cache=false)

  if expectations[:ensure].nil? and expectations[:exists].nil? and expectations[:directory].nil? and expectations[:file?].nil?
    expectations[:ensure] = 'file'
  end

  if expectations.has_key?(:constrain)
    expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]

    expectations[:constrain].each do |constraint|
      valid = constraint.match(/^(\S+?)\s(.*)$/)

      if valid.nil?
        raise InternalError.new(sprintf('invalid constraint[%s] specified', constraint))
      end

      fact        = $1
      expectation = $2

      unless meets_constraint?(fact, expectation)
        @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
        return true
      end
    end

    expectations.delete(:constrain)
  end

  properties = (expectations[:ensure].eql?('file')) ?  self.file(name, cache) : self.dir(name, cache)
  results = Hash.new()
  local = nil

  expectations.each do |k,v|

    case k
      when :ensure, :exists
        if properties.nil? and v.to_s.match(/absent|false/)
          local = true
        elsif properties.nil?
          local = false
        elsif v.to_s.match(/symlink|link/)
          if expectations[:target].nil?
            # don't validate the link path, just check whether we're a link
            local = properties[:symlink?]
          else
            # validate the link path
            local = properties[:target].eql?(expectations[:target])
          end
        else
          case v
            when 'dir', 'directory'
              local = properties[:directory?]
            else
              local = properties[:file?]
          end
        end
      when :file
        if properties.nil?
          if v.to_s.match(/absent|false/)
            local = true
          else
            local = false
          end
        elsif properties[:file?].true?
          local = ! v.to_s.match(/absent|false/)
        else
          false
        end
      when :dir, :directory
        if properties.nil?
          if v.to_s.match(/absent|false/)
            local = true
          else
            local = false
          end
        elsif properties.has_key?(:directory?)
          if properties[:directory?]
            local = v.to_s.match(/absent|false/).nil?
          else
            local = ! v.to_s.match(/absent|false/).nil?
          end
        else
          local = false
        end
      when :contains
        v = v.class.eql?(Array) ? v : [v]
        v.each do |regex|
          local = true
          begin
            self.run(sprintf("grep -c '%s' %s", regex, name))
          rescue => e
            local = false
          end
          break if local.false?
        end
      when :notcontains, :doesntcontain # TODO determine the appropriate attribute title here
        v = v.class.eql?(Array) ? v : [v]
        v.each do |regex|
          local = true
          begin
            self.run(sprintf("grep -c '%s' %s", regex, name))
            local = false
          rescue => e
            local = true
          end
          break if local.false?
        end
      when :mode, :permissions
        if properties.nil?
          local = false
        elsif v.to_s.match(/#{properties[:mode].to_s}/)
          local = true
        else
          local = false
        end
      when :size
        if properties.nil?
          local = false
        else
          local = v.to_i.eql?(properties[:size].to_i)
        end
      when :owner
        if properties.nil?
          local = false
        elsif v.to_s.match(/#{properties[:owner].to_s}/)
          local = true
        else
          local = false
        end
      when :group
        if properties.nil?
          local = false
        elsif v.match(/#{properties[:group]}/)
          local = true
        else
          local = false
        end
      when :type
        # noop allowing parse_catalog() output to be passed directly
      when :target
        # noop allowing ensure => 'link' / 'symlink' to specify their .. target
      else
        raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
    end

    return false if local.eql?(false) and fail_fast.eql?(true)
    results[k] = local
  end

  @logger.info("#{name} [#{expectations}] => #{results}")
  results.find{|k,v| v.false? }.nil?
end
validate_group(name, expectations, fail_fast=false) click to toggle source

validate_group

given a group and a hash of expectations, returns true|false whether group matches expectations

paramaters

  • <name> - group name

  • <expectations> - hash of expectations, see examples

  • <fail_fast> - return false immediately on any failure (default is false)

example expectations: 'root', {

# if ensure is not specified, 'present' is implied
:gid => 0,
:user => 'root'

} 'sys', {

:ensure => 'present',
:user   => ['root', 'bin', 'daemon']

},

'fizz', {

:exists => false

},

supported keys:

* :exists|:ensure
* :gid
* :user|:users (string or array)
* :constrain
# File lib/rouster/testing.rb, line 330
def validate_group(name, expectations, fail_fast=false)
  groups = self.get_groups(true)

  if expectations[:ensure].nil? and expectations[:exists].nil?
    expectations[:ensure] = 'present'
  end

  if expectations.has_key?(:constrain)
    expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]

    expectations[:constrain].each do |constraint|
      fact, expectation = constraint.split("\s")
      unless meets_constraint?(fact, expectation)
        @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
        return true
      end
    end

    expectations.delete(:constrain)
  end

  results = Hash.new()
  local = nil

  expectations.each do |k,v|
    case k
      when :ensure, :exists
        if groups.has_key?(name)
          if v.to_s.match(/absent|false/).nil?
            local = true
          else
            local = false
          end
        else
          local = v.to_s.match(/absent|false/).nil? ? false : true
        end
      when :gid
        if groups[name].is_a?(Hash) and groups[name].has_key?(:gid)
          local = v.to_s.eql?(groups[name][:gid].to_s)
        else
          local = false
        end
      when :user, :users
        v = v.class.eql?(Array) ? v : [v]
        v.each do |user|
          if groups[name].is_a?(Hash) and groups[name].has_key?(:users)
            local = groups[name][:users].member?(user)
          else
            local = false
          end
          break unless local.true? # need to make the return value smarter if we want to store data on which user failed
        end
      when :type
        # noop allowing parse_catalog() output to be passed directly
      else
        raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
    end

    return false if local.eql?(false) and fail_fast.eql?(true)
    results[k] = local
  end

  @logger.info("#{name} [#{expectations}] => #{results}")
  results.find{|k,v| v.false? }.nil?
end
validate_package(name, expectations, fail_fast=false) click to toggle source

validate_package

given a package name and a hash of expectations, returns true|false whether package meets expectations

parameters

  • <name> - package name

  • <expectations> - hash of expectations, see examples

  • <fail_fast> - return false immediately on any failure (default is false)

example expectations: 'perl-Net-SNMP', {

:ensure => 'absent'

},

'pixman', {

:ensure => 'present',
:version => '1.0',

},

'rrdtool', {

# if ensure is not specified, 'present' is implied
:version => '> 2.1',
:constrain => 'is_virtual false',

}, supported keys:

* :exists|ensure
* :version  (literal or basic comparison)
* :constrain
# File lib/rouster/testing.rb, line 425
def validate_package(name, expectations, fail_fast=false)
  packages = self.get_packages(true)

  if expectations[:ensure].nil? and expectations[:exists].nil?
    expectations[:ensure] = 'present'
  end

  if expectations.has_key?(:constrain)
    expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]

    expectations[:constrain].each do |constraint|
      fact, expectation = constraint.split("\s")
      unless meets_constraint?(fact, expectation)
        @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
        return true
      end
    end

    expectations.delete(:constrain)
  end

  results = Hash.new()
  local = nil

  expectations.each do |k,v|
    case k
      when :ensure, :exists
        if packages.has_key?(name)
          if v.to_s.match(/absent|false/).nil?
            local = true
          else
            local = false
          end
        else
          local = v.to_s.match(/absent|false/).nil? ? false : true
        end
      when :version
        # TODO support determination based on multiple versions of the same package installed (?)
        if packages.has_key?(name)

          lps = packages[name].is_a?(Array) ? packages[name] : [ packages[name] ]

          lps.each do |lp|
            if v.split("\s").size > 1
              ## generic comparator functionality
              comp, expectation = v.split("\s")
              local = generic_comparator(lp[:version], comp, expectation)
              break unless local.eql?(true)
            else
              local = ! v.to_s.match(/#{lp[:version]}/).nil?
              break unless local.eql?(true)
            end
          end
        else
          local = false
        end
      when :arch, :architecture
        if packages.has_key?(name)
          archs = []
          lps   = packages[name].is_a?(Array) ? packages[name] : [ packages[name] ]
          lps.each { |p| archs << p[:arch] }
          if v.is_a?(Array)
            v.each do |arch|
              local = archs.member?(arch)
              break unless local.eql?(true) # fail fast - if we are looking for an arch that DNE, bail out
            end
          else
            local = archs.member?(v)
          end
        end
      when :type
        # noop allowing parse_catalog() output to be passed directly
      else
        raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
    end

    return false if local.eql?(false) and fail_fast.eql?(true)
    results[k] = local
  end

  # TODO figure out a good way to allow access to the entire hash, not just boolean -- for now just print at an info level

  @logger.info("#{name} [#{expectations}] => #{results}")
  results.find{|k,v| v.false? }.nil?
end
validate_port(number, expectations, fail_fast=false) click to toggle source

given a port nnumber and a hash of expectations, returns true|false whether port meets expectations

parameters

  • <number> - port number

  • <expectations> - hash of expectations, see examples

example expectations: '22', {

:ensure => 'active',
:protocol => 'tcp',
:address => '0.0.0.0'

},

'1234', {

:ensure => 'open',
:address => '*',
:constrain => 'is_virtual false'

}

supported keys:

* :exists|ensure|state
* :address
* :protocol|proto
* :constrain
# File lib/rouster/testing.rb, line 535
def validate_port(number, expectations, fail_fast=false)
  number = number.to_s
  ports  = self.get_ports(true)

  if expectations[:ensure].nil? and expectations[:exists].nil? and expectations[:state].nil?
    expectations[:ensure] = 'present'
  end

  if expectations[:protocol].nil? and expectations[:proto].nil?
    expectations[:protocol] = 'tcp'
  elsif ! expectations[:proto].nil?
    expectations[:protocol] = expectations[:proto]
  end

  if expectations.has_key?(:constrain)
    expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]

    expectations[:constrain].each do |constraint|
      fact, expectation = constraint.split("\s")
      unless meets_constraint?(fact, expectation)
        @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
        return true
      end
    end

    expectations.delete(:constrain)
  end

  results = Hash.new()
  local = nil

  expectations.each do |k,v|
    case k
      when :ensure, :exists, :state
        if v.to_s.match(/absent|false|open/)
          local = ports[expectations[:protocol]][number].nil?
        else
          local = ! ports[expectations[:protocol]][number].nil?
        end
      when :protocol, :proto
        # TODO rewrite this in a less hacky way
        if expectations[:ensure].to_s.match(/absent|false|open/) or expectations[:exists].to_s.match(/absent|false|open/) or expectations[:state].to_s.match(/absent|false|open/)
          local = true
        else
          local = ports[v].has_key?(number)
        end

      when :address
        lr = Array.new
        if ports[expectations[:protocol]][number]
          addresses = ports[expectations[:protocol]][number][:address]
          addresses.each_key do |address|
            lr.push(address.eql?(v.to_s))
          end

          local = ! lr.find{|e| e.true? }.nil? # this feels jankity
        else
          # this port isn't open in the first place, won't match any addresses we expect to see it on
          local = false
        end
      else
        raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
    end

    return false if local.eql?(false) and fail_fast.eql?(true)
    results[k] = local
  end

  @logger.info("#{name} [#{expectations}] => #{results}")
  results.find{|k,v| v.false? }.nil?
end
validate_service(name, expectations, fail_fast=false) click to toggle source

validate_service

given a service name and a hash of expectations, returns true|false whether package meets expectations

parameters

  • <name> - service name

  • <expectations> - hash of expectations, see examples

  • <fail_fast> - return false immediately on any failure (default is false)

example expectations: 'ntp', {

:ensure => 'present',
:state  => 'started'

},

'ypbind', {

:state => 'stopped',

}

supported keys:

* :exists|:ensure
* :state,:status
* :constrain
# File lib/rouster/testing.rb, line 631
def validate_service(name, expectations, fail_fast=false)
  services = self.get_services(true)

  if expectations[:ensure].nil? and expectations[:exists].nil?
    expectations[:ensure] = 'present'
  end

  if expectations.has_key?(:constrain)
    expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]

    expectations[:constrain].each do |constraint|
      fact, expectation = constraint.split("\s")
      unless meets_constraint?(fact, expectation)
        @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
        return true
      end
    end

    expectations.delete(:constrain)
  end

  results = Hash.new()
  local = nil

  expectations.each do |k,v|
    case k
      when :ensure, :exists
        if services.has_key?(name)
          if v.to_s.match(/absent|false/)
            local = false
          else
            local = true
          end
        else
          local = v.to_s.match(/absent|false/).nil? ? false : true
        end
      when :state, :status
        if services.has_key?(name)
          local = ! v.match(/#{services[name]}/).nil?
        else
          local = false
        end
      when :type
        # noop allowing parse_catalog() output to be passed directly
      else
        raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
    end

    return false if local.eql?(false) and fail_fast.eql?(true)
    results[k] = local
  end

  @logger.info("#{name} [#{expectations}] => #{results}")
  results.find{|k,v| v.false? }.nil?
end
validate_user(name, expectations, fail_fast=false) click to toggle source

validate_user

given a user name and a hash of expectations, returns true|false whether user meets expectations

parameters

  • <name> - user name

  • <expectations> - hash of expectations, see examples

  • <fail_fast> - return false immediately on any failure (default is false)

example expectations: 'root' => {

:uid => 0

},

'ftp' => {

:exists => true,
:home   => '/var/ftp',
:shell  => 'nologin'

},

'developer' => {

:exists    => 'false',
:constrain => 'environment != production'

}

supported keys:

* :exists|ensure
* :home
* :group
* :shell
* :uid
* :gid
* :constrain
# File lib/rouster/testing.rb, line 721
def validate_user(name, expectations, fail_fast=false)
  users = self.get_users(true)

  if expectations[:ensure].nil? and expectations[:exists].nil?
    expectations[:ensure] = 'present'
  end

  if expectations.has_key?(:constrain)
    expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]

    expectations[:constrain].each do |constraint|
      fact, expectation = constraint.split("\s")
      unless meets_constraint?(fact, expectation)
        @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
        return true
      end
    end

    expectations.delete(:constrain)
  end

  results = Hash.new()
  local = nil

  expectations.each do |k,v|
    case k
      when :ensure, :exists
        if users.has_key?(name)
          if v.to_s.match(/absent|false/).nil?
            local = true
          else
            local = false
          end
        else
          local = v.to_s.match(/absent|false/).nil? ? false : true
        end
      when :group
        v = v.class.eql?(Array) ? v : [v]
        v.each do |group|
          local = is_user_in_group?(name, group)
          break unless local.true?
        end
      when :gid
        if users[name].is_a?(Hash) and users[name].has_key?(:gid)
          local = v.to_i.eql?(users[name][:gid].to_i)
        else
          local = false
        end
      when :home
        if users[name].is_a?(Hash) and users[name].has_key?(:home)
          local = ! v.match(/#{users[name][:home]}/).nil?
        else
          local = false
        end
      when :home_exists
        if users[name].is_a?(Hash) and users[name].has_key?(:home_exists)
          local = ! v.to_s.match(/#{users[name][:home_exists].to_s}/).nil?
        else
          local = false
        end
      when :shell
        if users[name].is_a?(Hash) and users[name].has_key?(:shell)
          local = ! v.match(/#{users[name][:shell]}/).nil?
        else
          local = false
        end
      when :uid
        if users[name].is_a?(Hash) and users[name].has_key?(:uid)
          local = v.to_i.eql?(users[name][:uid].to_i)
        else
          local = false
        end
      when :type
        # noop allowing parse_catalog() output to be passed directly
      else
        raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
    end

    return false if local.eql?(false) and fail_fast.eql?(true)
    results[k] = local
  end

  @logger.info("#{name} [#{expectations}] => #{results}")
  results.find{|k,v| v.false? }.nil?
end

Private Instance Methods

generic_comparator(comparand1, comparator, comparand2) click to toggle source

generic_comparator

powers the 3 argument form of constraint (i.e. 'is_virtual != true', '<package_version> > 3.0', etc)

should really be an eval{} of some sort (or would be in the perl world)

parameters

  • <comparand1> - left side of the comparison

  • <comparator> - comparison to make

  • <comparand2> - right side of the comparison

# File lib/rouster/testing.rb, line 866
def generic_comparator(comparand1, comparator, comparand2)

  # TODO rewrite this as an eval so we don't have to support everything..
  # TODO come up with mechanism to determine when is it appropriate to call .to_i vs. otherwise -- comparisons will mainly be numerical (?), but need to support text matching too
  case comparator
    when '!='
      # ugh
      if comparand1.to_s.match(/\d/) or comparand2.to_s.match(/\d/)
        res = ! comparand1.to_i.eql?(comparand2.to_i)
      else
        res = ! comparand1.eql?(comparand2)
      end
    when '<'
      res = comparand1.to_i < comparand2.to_i
    when '<='
      res = comparand1.to_i <= comparand2.to_i
    when '>'
      res = comparand1.to_i > comparand2.to_i
    when '>='
      res = comparand1.to_i >= comparand2.to_i
    when '=='
      # ugh ugh
      if comparand1.to_s.match(/\d/) or comparand2.to_s.match(/\d/)
        res = comparand1.to_i.eql?(comparand2.to_i)
      else
        res = comparand1.eql?(comparand2)
      end
    else
      raise NotImplementedError.new(sprintf('unknown comparator[%s]', comparator))
  end



  res
end
meets_constraint?(key, expectation, cache=true) click to toggle source

meets_constraint?

powers the :constrain value in expectations passed to validate_* gets facts from node, and if fact expectation regex matches actual fact, returns true

parameters

  • <key> - fact/hiera key to look up (actual value)

  • <expectation> -

  • cache
    • boolean controlling whether facter lookups are cached

# File lib/rouster/testing.rb, line 820
def meets_constraint?(key, expectation, cache=true)

  expectation = expectation.to_s

  unless self.respond_to?('facter') or self.respond_to?('hiera')
    # if we haven't loaded puppet.rb, we won't have access to facts/hiera lookups
    @logger.warn('using constraints without loading [rouster/puppet] will not work, forcing no-op')
    return false
  end

  facts  = self.facter(cache)
  actual = nil

  if facts[key]
    actual = facts[key]
  else
    # value is not a fact, lets try to find it in hiera
    # TODO how to handle the fact that this will really only work on the puppetmaster
    actual = self.hiera(key, facts)
  end

  res = nil

  if expectation.split("\s").size > 1
    ## generic comparator functionality
    comp, expectation = expectation.split("\s")
    res = generic_comparator(actual, comp, expectation)
  else
    res = ! actual.to_s.match(/#{expectation}/).nil?
  end

  @logger.debug(sprintf('meets_constraint?(%s, %s): %s', key, expectation, res.nil?))
  res
end