class Rouster
TODO better document keys :constrain and :version
Vagrant specific (and related) methods
Constants
- VERSION
sporadically updated version number
Attributes
Public Class Methods
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
# 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
(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
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
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 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?
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
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
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
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
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
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
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
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
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
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
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
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
-
# 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
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
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
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
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
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
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
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
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
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 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
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
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?
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?
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?
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?
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?
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?
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?
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?
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?
convenience getter for @passthrough truthiness
# File lib/rouster.rb, line 627 def is_passthrough? @passthrough.class.eql?(Hash) end
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?
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?
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?
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?
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?
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_symlink?
uses file() to return boolean indicating whether parameter passed is a symlink
parameters
-
<file> - path of filename to validate
# File lib/rouster/tests.rb, line 478 def is_symlink?(file) res = nil begin res = self.file(file) rescue => e return false end res.class.eql?(Hash) ? res[:symlink?] : false end
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?
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
() andget_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?()
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?
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
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
# 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 – 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
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
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
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
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
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
… 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
… 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
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
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
… 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?
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
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
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
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
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
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
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
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 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?
convenience getter for @sudo truthiness
# File lib/rouster.rb, line 635 def uses_sudo? @sudo.eql?(true) end
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
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
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
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
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
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
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
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
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?
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