24ccdd51e5
Change-Id: Id4518617fc89dde9de10326b0b958917db404110 Closes-Bug: #1523973
760 lines
20 KiB
Ruby
760 lines
20 KiB
Ruby
require 'hiera'
|
|
require 'test/unit'
|
|
require 'open-uri'
|
|
require 'timeout'
|
|
require 'facter'
|
|
require 'socket'
|
|
|
|
module TestCommon
|
|
module Cmd
|
|
# Run a shell command and return stdout and return code as an array
|
|
# @param command [String] the command to run
|
|
# @return [Array<String,Numeric>] Stdout and return code
|
|
def self.run(command)
|
|
out = `#{command}`
|
|
code = $?.exitstatus
|
|
[out, code]
|
|
end
|
|
|
|
# set the OpenStack CLI auth data
|
|
def self.openstack_auth
|
|
ENV['LC_ALL'] = 'C'
|
|
ENV['OS_NO_CACHE'] = 'true'
|
|
ENV['OS_TENANT_NAME'] = TestCommon::Settings.access['tenant']
|
|
ENV['OS_USERNAME'] = TestCommon::Settings.access['user']
|
|
ENV['OS_PASSWORD'] = TestCommon::Settings.access['password']
|
|
ENV['OS_AUTH_URL'] = "http://#{TestCommon::Settings.management_vip}:5000/v2.0"
|
|
ENV['OS_AUTH_STRATEGY'] = 'keystone'
|
|
ENV['OS_REGION_NAME'] = 'RegionOne'
|
|
ENV['OS_ENDPOINT_TYPE'] = 'internalURL'
|
|
end
|
|
|
|
# run the openstack cli command with auth
|
|
# and parse the results into a structure
|
|
# @return [Array<Hash>]
|
|
def self.openstack_cli(command)
|
|
openstack_auth
|
|
out, code = run command
|
|
return [nil, code] unless code == 0
|
|
headers = nil
|
|
data = []
|
|
out.split("\n").each do |line|
|
|
next unless line.start_with? '|'
|
|
columns = line.split('|')
|
|
next unless columns.length > 2
|
|
columns = columns[1..-2].map do |column|
|
|
column.chomp.strip
|
|
end
|
|
unless headers
|
|
headers = columns
|
|
next
|
|
end
|
|
record = {}
|
|
field_number = 0
|
|
columns.each do |column|
|
|
header = headers[field_number]
|
|
next unless header
|
|
record[header] = column
|
|
field_number += 1
|
|
end
|
|
data << record if record.any?
|
|
end
|
|
data
|
|
end
|
|
|
|
end
|
|
|
|
module Settings
|
|
# get a Hiera class instance
|
|
# @return [Hiera]
|
|
def self.hiera
|
|
return @hiera if @hiera
|
|
@hiera = Hiera.new(:config => '/etc/puppet/hiera.yaml')
|
|
end
|
|
|
|
# lookup a value using the Hiera class
|
|
# @param key [String,Symbol] a value to look for
|
|
# @return [String,Array,Hash,nil] found value or nil if not found
|
|
def self.lookup(key, default=nil)
|
|
key = key.to_s
|
|
key = 'rabbit_hash' if key == 'rabbit'
|
|
@keys = {} unless @keys
|
|
return @keys[key] if @keys[key]
|
|
@keys[key] = hiera.lookup key, default, {}
|
|
end
|
|
|
|
# access lookup values as methods
|
|
def self.method_missing(key)
|
|
lookup key
|
|
end
|
|
|
|
# access lookup methods as indices
|
|
def self.[](key)
|
|
lookup key
|
|
end
|
|
|
|
# reset mnemoization
|
|
def self.reset
|
|
@hiera = nil
|
|
@keys = nil
|
|
end
|
|
end
|
|
|
|
module HAProxy
|
|
# get the URL of HAProxy stats page
|
|
# @return [String] the stats url
|
|
def self.stats_url
|
|
ip = Settings.management_vip
|
|
ip = Settings.controller_node_address unless ip
|
|
raise 'Could not get internal address!' unless ip
|
|
port = 10000
|
|
"http://#{ip}:#{port}/;csv"
|
|
end
|
|
|
|
# read the CSV from the Stats URL
|
|
# @return [String] csv data
|
|
def self.csv
|
|
return @csv if @csv
|
|
begin
|
|
url = open(stats_url)
|
|
csv = url.read
|
|
rescue
|
|
nil
|
|
end
|
|
return nil unless csv and not csv.empty?
|
|
@csv = csv
|
|
end
|
|
|
|
# parse the csv data to the backends and their statuses
|
|
# 'backend_name' => 'UP/DOWN'
|
|
# @return [Hash<String => String>] the backends structure
|
|
def self.backends
|
|
return @backends if @backends
|
|
raise 'Could not get CSV from HAProxy stats!' unless csv
|
|
backends = {}
|
|
csv.split("\n").each do |line|
|
|
next if line.start_with? '#'
|
|
next unless line.include? 'BACKEND'
|
|
fields = line.split(',')
|
|
backend = fields[0]
|
|
status = fields[17]
|
|
backends[backend] = status
|
|
end
|
|
@backends = backends
|
|
end
|
|
|
|
# check if the backend is present
|
|
# @param backend [String] the backend name
|
|
# @return [true,false]
|
|
def self.backend_present?(backend)
|
|
backends.keys.include? backend
|
|
end
|
|
|
|
# check if the backend is online
|
|
# @param backend [String] the backend name
|
|
# @return [true,false]
|
|
def self.backend_up?(backend)
|
|
backends[backend] == 'UP'
|
|
end
|
|
|
|
# reset mnemoization
|
|
def self.reset
|
|
@csv = nil
|
|
@backends = nil
|
|
end
|
|
end
|
|
|
|
module Process
|
|
|
|
# try to run the command and return true if the run was successful
|
|
# @param cmd [String] the command to run
|
|
# @return [true,false]
|
|
def self.run_successful?(cmd)
|
|
out = TestCommon::Cmd.run cmd
|
|
out.last == 0
|
|
end
|
|
|
|
# try to run find if the command executable is installed
|
|
# @param cmd [String] the command to run
|
|
# @return [true,false]
|
|
def self.command_present?(command)
|
|
run_successful? "which '#{command}' 1>/dev/null 2>/dev/null"
|
|
end
|
|
|
|
# use the 'ps' to get the list of all running processes
|
|
# @return [Array<String>] the list of running commands
|
|
def self.list
|
|
return @process_list if @process_list
|
|
@process_list = []
|
|
ps = TestCommon::Cmd.run 'ps haxo cmd'
|
|
ps.first.split("\n").each do |cmd|
|
|
@process_list << cmd
|
|
end
|
|
@process_list
|
|
end
|
|
|
|
# check if there is a running command which
|
|
# command line contains this string
|
|
# @param process [String] look for this string in the list
|
|
# @return [true,false]
|
|
def self.running?(process)
|
|
not list.find { |cmd| cmd.include? process }.nil?
|
|
end
|
|
|
|
# use the 'ps' tool to build the process tree
|
|
# using pids and ppids of the processes
|
|
# and the pid as the hash key
|
|
# @return [Hash<Numeric => Hash>]
|
|
def self.tree
|
|
return @process_tree if @process_tree
|
|
@process_tree = {}
|
|
ps = TestCommon::Cmd.run 'ps haxo pid,ppid,cmd'
|
|
ps.first.split("\n").each do |p|
|
|
f = p.split
|
|
pid = f.shift.to_i
|
|
ppid = f.shift.to_i
|
|
cmd = f.join ' '
|
|
|
|
# create entry for this pid if not present
|
|
@process_tree[pid] = {
|
|
:children => []
|
|
} unless @process_tree.key? pid
|
|
|
|
# fill this entry
|
|
@process_tree[pid][:ppid] = ppid
|
|
@process_tree[pid][:pid] = pid
|
|
@process_tree[pid][:cmd] = cmd
|
|
|
|
unless ppid == 0
|
|
# create entry for parent process if not present
|
|
@process_tree[ppid] = {
|
|
:children => [],
|
|
:cmd => '',
|
|
} unless @process_tree.key? ppid
|
|
|
|
# fill parent's children
|
|
@process_tree[ppid][:children] << pid
|
|
end
|
|
end
|
|
@process_tree
|
|
end
|
|
|
|
# reset mnemoization
|
|
def self.reset
|
|
@process_tree = nil
|
|
@process_list = nil
|
|
end
|
|
end
|
|
|
|
module MySQL
|
|
@pass = nil
|
|
@user = nil
|
|
@host = nil
|
|
@port = nil
|
|
@db = nil
|
|
@options = '--raw --skip-column-names --batch'
|
|
|
|
# turn off the MySQL auth and use the saved credentials if any
|
|
def self.no_auth
|
|
@pass = nil
|
|
@user = nil
|
|
@host = nil
|
|
@port = nil
|
|
@db = nil
|
|
end
|
|
|
|
def self.pass=(pass)
|
|
@pass = pass
|
|
end
|
|
|
|
def self.pass
|
|
@pass
|
|
end
|
|
|
|
def self.user=(user)
|
|
@user = user
|
|
end
|
|
|
|
def self.user
|
|
@user
|
|
end
|
|
|
|
def self.host=(host)
|
|
@host = host
|
|
end
|
|
|
|
def self.host
|
|
@host
|
|
end
|
|
|
|
def self.port=(port)
|
|
@port = port
|
|
end
|
|
|
|
def self.port
|
|
@port
|
|
end
|
|
|
|
def self.db
|
|
@db
|
|
end
|
|
|
|
def self.db=(db)
|
|
@db = db
|
|
end
|
|
|
|
def self.options=(options)
|
|
@options = options
|
|
end
|
|
|
|
def self.options
|
|
@options
|
|
end
|
|
|
|
# execute the mysql query using the 'mysql' command
|
|
# return the array of the output and the exit code
|
|
# @param query [String] the query to run
|
|
# @return [Array<String, Numeric>] command output
|
|
def self.query(query)
|
|
query = query.gsub %q('), %q(")
|
|
command = %Q(mysql #{options} --execute='#{query}')
|
|
command += %Q( --host='#{host}') if host
|
|
command += %Q( --user='#{user}') if user
|
|
command += %Q( --password='#{pass}') if pass
|
|
command += %Q( --port='#{port}') if port
|
|
command += %Q( --database='#{db}') if db
|
|
TestCommon::Cmd.run command
|
|
end
|
|
|
|
# check if mysql can connect ot the server
|
|
# @return [true,false]
|
|
def self.connection?
|
|
result = query 'show databases'
|
|
result.last == 0
|
|
end
|
|
|
|
# get the list of databases on the server
|
|
# @return [Array<String>] the list of database names
|
|
def self.databases
|
|
return @databases if @databases
|
|
out, code = query 'show databases'
|
|
return unless code == 0
|
|
@databases = []
|
|
out.split("\n").each do |db|
|
|
@databases << db
|
|
end
|
|
@databases
|
|
end
|
|
|
|
# check is the database is present on the server
|
|
# @param database [String] the database name
|
|
# @return [true,false]
|
|
def self.database_exists?(database)
|
|
return unless databases
|
|
databases.include?(database)
|
|
end
|
|
|
|
# reset mnemoization
|
|
def self.reset
|
|
@databases = nil
|
|
no_auth
|
|
end
|
|
|
|
end
|
|
|
|
module Pacemaker
|
|
# check if pacemaker is online
|
|
# @return [true,false]
|
|
def self.online?
|
|
begin
|
|
out = Timeout::timeout(5) do
|
|
TestCommon::Cmd.run 'cibadmin -Q'
|
|
end
|
|
rescue
|
|
return false
|
|
end
|
|
out.last == 0
|
|
end
|
|
|
|
# get the list of pacemaker primitives
|
|
# @return [Array<String>] the list of primitive names
|
|
def self.primitives
|
|
begin
|
|
out = Timeout::timeout(5) do
|
|
TestCommon::Cmd.run 'crm_resource -l'
|
|
end
|
|
rescue
|
|
return
|
|
end
|
|
return unless out.last == 0
|
|
primitives = []
|
|
out.first.split("\n").each do |line|
|
|
primitives << line.split(':').first
|
|
end
|
|
primitives
|
|
end
|
|
|
|
# remove prefix from primitive name
|
|
# @param primitive [String] primitive name
|
|
# @return [String] primitive name without prefix
|
|
def self.clean_primitive_name(primitive)
|
|
primitive = primitive.gsub /^clone_/, ''
|
|
primitive = primitive.gsub /^master_/, ''
|
|
primitive
|
|
end
|
|
|
|
# check if the primitive is present in paceamaker
|
|
# @param primitive [String] primitive name
|
|
# @return [true,false]
|
|
def self.primitive_present?(primitive)
|
|
primitive = clean_primitive_name primitive
|
|
primitives.include? primitive
|
|
end
|
|
|
|
# check if the pacemaker primitive is started
|
|
# at least on a single cluster node
|
|
# @param primitive [String] primitive name
|
|
# @return [true,false]
|
|
def self.primitive_started?(primitive)
|
|
primitive = clean_primitive_name primitive
|
|
begin
|
|
out = TestCommon::Cmd.run "crm_resource -r #{primitive} -W 2>&1"
|
|
rescue
|
|
return
|
|
end
|
|
return true if out.first.include? 'is running on'
|
|
return false if out.first.include? 'is NOT running'
|
|
nil
|
|
end
|
|
|
|
# reset mnemoization
|
|
def self.reset
|
|
nil
|
|
end
|
|
end
|
|
|
|
module Facts
|
|
# use the Puppet's Facter to lookup a value
|
|
# @param fact [String,Symbol] the fact's name
|
|
def self.value(fact)
|
|
@facts = {} unless @facts
|
|
fact = fact.to_s
|
|
return @facts[fact] if @facts[fact]
|
|
@facts[fact] = Facter.value fact
|
|
@facts[fact]
|
|
end
|
|
|
|
# access using indices
|
|
def self.[](fact)
|
|
value fact
|
|
end
|
|
|
|
# access using method names
|
|
def self.method_missing(fact)
|
|
value fact
|
|
end
|
|
|
|
# reset mnemoization
|
|
def self.reset
|
|
@facts = nil
|
|
end
|
|
end
|
|
|
|
module Package
|
|
# obtain the list of rpm packages
|
|
# using the 'rpm' tool
|
|
# @returns [String] packages
|
|
def self.get_rpm_packages
|
|
out = TestCommon::Cmd.run "rpm -qa --queryformat '%{NAME}|%{VERSION}-%{RELEASE}\n'"
|
|
out.first
|
|
end
|
|
|
|
# obtain the list of deb packages
|
|
# using the 'dpkg-query' tool
|
|
# @returns [String] packages
|
|
def self.get_deb_packages
|
|
out = TestCommon::Cmd.run "dpkg-query --show -f='${Package}|${Version}|${Status}\n'"
|
|
out.first
|
|
end
|
|
|
|
# parse the received rpm packages list
|
|
# to get the structure of package names and versions
|
|
# 'package_name' => 'package_version'
|
|
# @returns [Hash<String => String>] packages
|
|
def self.parse_rpm_packages
|
|
packages = {}
|
|
get_rpm_packages.split("\n").each do |package|
|
|
fields = package.split('|')
|
|
name = fields[0]
|
|
version = fields[1]
|
|
if name
|
|
packages.store name, version
|
|
end
|
|
end
|
|
packages
|
|
end
|
|
|
|
# parse the received deb packages list
|
|
# to get the structure of package names and versions
|
|
# 'package_name' => 'package_version'
|
|
# @returns [Hash<String => String>] packages
|
|
def self.parse_deb_packages
|
|
packages = {}
|
|
get_deb_packages.split("\n").each do |package|
|
|
fields = package.split('|')
|
|
name = fields[0]
|
|
version = fields[1]
|
|
if fields[2] == 'install ok installed'
|
|
installed = true
|
|
else
|
|
installed = false
|
|
end
|
|
if installed and name
|
|
packages.store name, version
|
|
end
|
|
end
|
|
packages
|
|
end
|
|
|
|
# get the installed packages either on Debian or on RedHat system
|
|
# 'package_name' => 'package_version'
|
|
# @returns [Hash<String => String>] packages
|
|
def self.installed_packages
|
|
return @installed_packages if @installed_packages
|
|
if Facts.osfamily == 'RedHat'
|
|
@installed_packages = parse_rpm_packages
|
|
elsif Facts.osfamily == 'Debian'
|
|
@installed_packages = parse_deb_packages
|
|
else
|
|
raise "Unknown osfamily: #{Facts.osfamily}"
|
|
end
|
|
end
|
|
|
|
# reset mnemoization
|
|
def self.reset
|
|
@installed_packages = nil
|
|
end
|
|
|
|
# check if this package is installed
|
|
# @param package [String] package name
|
|
# @return [true,false]
|
|
def self.is_installed?(package)
|
|
installed_packages.key? package
|
|
end
|
|
end
|
|
|
|
module Network
|
|
# check if this url is accessible and gives success HTTP status
|
|
# @param url [String] the url to check
|
|
# @return [true,false]
|
|
def self.url_accessible?(url)
|
|
out = TestCommon::Cmd.run "curl --fail '#{url}' 1>/dev/null 2>/dev/null"
|
|
out.last == 0
|
|
end
|
|
|
|
# check is TCP connection can be open to this socket
|
|
# @param host [String] hostname of IP address
|
|
# @param port [String,Numeric] the port number to connect to
|
|
# @return [true,false]
|
|
def self.connection?(host, port)
|
|
begin
|
|
Timeout::timeout(5) do
|
|
sock = TCPSocket.open(host, port)
|
|
sock.close if sock
|
|
end
|
|
rescue
|
|
return false
|
|
end
|
|
true
|
|
end
|
|
|
|
# check id TCP connection is closed to this socket
|
|
# inversion on connection?
|
|
# @param host [String] hostname of IP address
|
|
# @param port [String,Numeric] the port number to connect to
|
|
# @return [true,false]
|
|
def self.no_connection?(host, port)
|
|
not connection?(host, port)
|
|
end
|
|
|
|
# get the list of names from the named iptables rules
|
|
# most likely created by Puppet
|
|
# @return [Array<String>] the list of rule names
|
|
def self.iptables_rules
|
|
return @iptables_rules if @iptables_rules
|
|
output, code = TestCommon::Cmd.run 'iptables-save'
|
|
return unless code == 0
|
|
comments = []
|
|
output.split("\n").each do |line|
|
|
line =~ %r(--comment\s+"(.*?)")
|
|
next unless $1
|
|
comment = $1.chomp.strip.gsub /^\d+\s+/, ''
|
|
comment.gsub! /\sfrom\s\d+\.\d+\.\d+\.\d+\/\d+/, ''
|
|
comments << comment
|
|
end
|
|
@iptables_rules = comments
|
|
end
|
|
|
|
# get the list of ip addresses found on this system's interfaces
|
|
# @return [Array<String>] the list of addresses
|
|
def self.ips
|
|
return @ips if @ips
|
|
ip_out, code = TestCommon::Cmd.run 'ip -4 -o a'
|
|
return unless code == 0
|
|
ips = []
|
|
ip_out.split("\n").each do |line|
|
|
if line =~ /\s+inet\s+([\d\.]*)/
|
|
ips << $1
|
|
end
|
|
end
|
|
@ips = ips
|
|
end
|
|
|
|
# get this systems default router
|
|
# @return [String] the default router ip
|
|
def self.default_router
|
|
return @default_router if @default_router
|
|
routes, code = TestCommon::Cmd.run 'ip route'
|
|
return unless code == 0
|
|
routes.split("\n").each do |line|
|
|
if line =~ /^default via ([\d\.]*)/
|
|
return @default_router = $1
|
|
end
|
|
end
|
|
nil
|
|
end
|
|
|
|
# try to ping this host and return success
|
|
# @param host [String] the hostname or the ip address to ping
|
|
# @return [true,false]
|
|
def self.ping?(host)
|
|
begin
|
|
out = Timeout::timeout(5) do
|
|
TestCommon::Cmd.run "ping -q -c 1 -W 3 '#{host}'"
|
|
end
|
|
rescue
|
|
return false
|
|
end
|
|
out.last == 0
|
|
end
|
|
|
|
# reset mnemoization
|
|
def self.reset
|
|
@iptables_rules = nil
|
|
@ips = nil
|
|
@default_router = nil
|
|
end
|
|
end
|
|
|
|
module AMQP
|
|
# use python's kombu library to check is connection
|
|
# to AMQP server is possible with this credentials
|
|
# python's library is used because it's installed everywhere
|
|
# and ruby's library is not
|
|
# @param [String] user
|
|
# @param [String] password
|
|
# @param [String] host
|
|
# @param [String,Numeric] port
|
|
# @param [String] vhost
|
|
# @param [String] protocol
|
|
# @return [true,false]
|
|
def self.connection?(
|
|
user=Settings.rabbit['user'],
|
|
password=Settings.rabbit['password'],
|
|
host='localhost',
|
|
port='5673',
|
|
vhost='/',
|
|
protocol='amqp')
|
|
url = "#{protocol}://#{user}:#{password}@#{host}:#{port}/#{vhost}"
|
|
python = <<-eof
|
|
import sys
|
|
import kombu
|
|
|
|
connected = False
|
|
try:
|
|
connection = kombu.Connection("#{url}")
|
|
connection.connect()
|
|
connected = connection.connected
|
|
connection.close()
|
|
connection.release()
|
|
except Exception as e:
|
|
sys.stdout.write("AMQP error: %s\\n" % e)
|
|
if connected:
|
|
sys.exit(0)
|
|
else:
|
|
sys.exit(1)
|
|
eof
|
|
system "python -c '#{python}'"
|
|
$?.exitstatus == 0
|
|
end
|
|
end
|
|
|
|
module Config
|
|
# parse the ini-style config file
|
|
# 'section/key' => 'value'
|
|
# @param [String] file path to the file
|
|
# @return [Hash<String => String>]
|
|
def self.ini_file(file)
|
|
content = File.read file
|
|
data = {}
|
|
return data unless content
|
|
section = 'default'
|
|
content.split("\n").each do |line|
|
|
line = line.strip
|
|
next if line.start_with? '#'
|
|
next if line == ''
|
|
if line =~ /\[(\S+)\]/
|
|
section = $1.downcase
|
|
elsif line =~ /(\S+)\s*=\s*(.*)/
|
|
data["#{section}/#{$1.downcase}"] = $2
|
|
end
|
|
end
|
|
data
|
|
end
|
|
|
|
# check if this ini-style config file hash a value
|
|
# @param [String] file path to the file
|
|
# @param [String] key section/key of the param
|
|
# @param [String] value the value of the param
|
|
# @return [true,false]
|
|
def self.value?(file, key, value)
|
|
key = key.downcase
|
|
key = 'default/' + key unless key.include? '/'
|
|
data = ini_file file
|
|
return !data.key?(key) if value.nil?
|
|
value = value.to_s
|
|
value.capitalize! if %w(true false).include? value
|
|
data[key] == value
|
|
end
|
|
|
|
# check if this file contains either a string or a regexp
|
|
# @param file [String] path to the file
|
|
# @param line [String, Regexp] look for this string or regexp
|
|
# @return [true,false]
|
|
def self.has_line?(file, line)
|
|
content = File.read file
|
|
if line.is_a? String
|
|
content.include? line
|
|
elsif line.is_a? Regexp
|
|
not (content =~ line).nil?
|
|
else
|
|
raise 'Line should be a string or a regexp!'
|
|
end
|
|
end
|
|
end
|
|
|
|
module Cron
|
|
# check if cronjob exists, ignores comments
|
|
# @param user [String] usernam to check cron for
|
|
# @param cronjob [String, Regexp] pattern look for in cron
|
|
# @return [true,false]
|
|
def self.cronjob_exists?(user, cronjob)
|
|
cmd = "crontab -u #{user} -l"
|
|
out = TestCommon::Cmd.run cmd
|
|
false unless out.last == 0
|
|
out.first[/^\s*[^#]*#{cronjob}/].nil? == false
|
|
end
|
|
end
|
|
|
|
end
|