diff --git a/lib/resources/mysql_conf.rb b/lib/resources/mysql_conf.rb new file mode 100644 index 000000000..ef9841672 --- /dev/null +++ b/lib/resources/mysql_conf.rb @@ -0,0 +1,61 @@ +# encoding: utf-8 +# copyright: 2015, Dominik Richter +# license: All rights reserved + +require 'utils/parseconfig' + +class MysqlConf + + def initialize( conf_path ) + @runner = Specinfra::Runner + @conf_path = conf_path + @files_contents = {} + @content = nil + @params = nil + read_content + end + + def content + @content ||= read_content + end + + def params *opts + @params || read_content + res = @params + opts.each do |opt| + res = res[opt] unless res.nil? + end + res + end + + def read_content + @content = "" + @params = {} + to_read = [@conf_path] + while !to_read.empty? + raw_conf = read_file(to_read[0]) + @content += raw_conf + + params = ParseConfig.new(raw_conf).params + @params.merge!(params) + + to_read = to_read.drop(1) + # see if there is more stuff to include + include_files = raw_conf.scan(/^!include\s+(.*)\s*/).flatten.compact + include_dirs = raw_conf.scan(/^!includedir\s+(.*)\s*/).flatten.compact + include_dirs.map do |include_dir| + include_files += Dir.glob(File.join include_dir, '*') + end + to_read += include_files.find_all do |fp| + not @files_contents.key? fp + end + end + # + @content + end + + def read_file(path) + @files_contents[path] ||= @runner.get_file_content(path).stdout + end +end + diff --git a/lib/resources/mysql_session.rb b/lib/resources/mysql_session.rb new file mode 100644 index 000000000..fb3029a04 --- /dev/null +++ b/lib/resources/mysql_session.rb @@ -0,0 +1,35 @@ +# encoding: utf-8 +# copyright: 2015, Dominik Richter +# license: All rights reserved + +class MysqlSession + def initialize user, pass + @user = user + @pass = pass + end + + def describe(query, db = "", &block) + # TODO: simple escape, must be handled by a library + # that does this securely + escaped_query = query.gsub(/\\/, '\\\\').gsub(/"/,'\\"').gsub(/\$/,'\\$') + # run the query + cmd = Serverspec::Type::Command.new("mysql -u#{@user} -p#{@pass} #{db} -s -e \"#{escaped_query}\"") + out = cmd.stdout + "\n" + cmd.stderr + if out =~ /Can't connect to .* MySQL server/ or + out.downcase =~ /^error/ + # skip this test if the server can't run the query + RSpec.describe( cmd ) do + it "is skipped", skip: out do + end + end + else + RSpec.__send__( 'describe', cmd, &block ) + end + end + +end + +def start_mysql_session( user, password ) + MysqlSession.new(user, password) +end + diff --git a/lib/resources/postgres_conf.rb b/lib/resources/postgres_conf.rb new file mode 100644 index 000000000..4fd4a0d7a --- /dev/null +++ b/lib/resources/postgres_conf.rb @@ -0,0 +1,63 @@ +# encoding: utf-8 +# copyright: 2015, Dominik Richter +# license: All rights reserved + +require 'utils/simpleconfig' + +class PostgresConf + + def initialize( conf_path ) + @runner = Specinfra::Runner + @conf_path = conf_path + @conf_dir = File.expand_path(File.dirname @conf_path) + @files_contents = {} + @content = nil + @params = nil + read_content + end + + def content + @content ||= read_content + end + + def params *opts + @params || read_content + res = @params + opts.each do |opt| + res = res[opt] unless res.nil? + end + res + end + + def read_content + @content = "" + @params = {} + to_read = [@conf_path] + while !to_read.empty? + raw_conf = read_file(to_read[0]) + @content += raw_conf + + params = SimpleConfig.new(raw_conf).params + @params.merge!(params) + + to_read = to_read.drop(1) + # see if there is more config files to include + include_files = params['include'] || [] + include_files += params['include_if_exists'] || [] + (params['include_dir'] || []).each do |id| + id = File.join(@conf_dir, id) if id[0] != '/' + include_files += Dir.glob(File.join id, '*') + end + to_read += include_files.find_all do |fp| + not @files_contents.key? fp + end + end + # + @content + end + + def read_file(path) + @files_contents[path] ||= @runner.get_file_content(path).stdout + end +end + diff --git a/lib/resources/postgres_session.rb b/lib/resources/postgres_session.rb new file mode 100644 index 000000000..f28b03bbe --- /dev/null +++ b/lib/resources/postgres_session.rb @@ -0,0 +1,64 @@ +# encoding: utf-8 +# copyright: 2015, Dominik Richter +# license: All rights reserved + +module Serverspec end +module Serverspec::Type + class Lines + def initialize raw, desc + @raw = raw + @desc = desc + end + + def output + @raw + end + + def lines + @raw.split("\n") + end + + def to_s + @desc + end + end +end + +class PostgresSession + def initialize user, pass + @user = user || 'postgres' + @pass = pass + end + + def describe(query, db = [], &block) + dbs = db.map{|x| "-d #{x}" }.join(' ') + # TODO: simple escape, must be handled by a library + # that does this securely + escaped_query = query.gsub(/\\/, '\\\\').gsub(/"/,'\\"').gsub(/\$/,'\\$') + # run the query + cmd = Serverspec::Type::Command.new("PGPASSWORD='#{@pass}' psql -U #{@user} #{dbs} -c \"#{escaped_query}\"") + out = cmd.stdout + "\n" + cmd.stderr + if out =~ /Can't connect to .*/ or + out.downcase =~ /^error/ + # skip this test if the server can't run the query + RSpec.describe( cmd ) do + it "is skipped", skip: out do + end + end + else + lines = cmd.stdout. + # remove the whole header (i.e. up to the first ^-----+------+------$) + sub(/(.*\n)+([-]+[+])*[-]+\n/,''). + # remove the tail + sub(/\n[^\n]*\n\n$/,'') + l = Serverspec::Type::Lines.new(lines.strip, "PostgreSQL query: #{query}") + RSpec.__send__( 'describe', l, &block ) + end + end + +end + +def start_postgres_session( user, password ) + PostgresSession.new(user, password) +end + diff --git a/lib/resources/processes.rb b/lib/resources/processes.rb new file mode 100644 index 000000000..17c9a1760 --- /dev/null +++ b/lib/resources/processes.rb @@ -0,0 +1,52 @@ +# encoding: utf-8 +# copyright: 2015, Dominik Richter +# license: All rights reserved + +module Serverspec + module Type + class Processes < Base + def initialize grep + # turn into a regexp if it isn't one yet + if grep.class == String + grep = '(/[^/]*)*'+grep if grep[0] != '/' + grep = Regexp.new('^'+grep+'(\s|$)') + end + # get all running processes + cmd = Serverspec::Type::Command.new('ps aux') + all = cmd.stdout.split("\n")[1..-1] + all_cmds = all.map do |line| + # user 32296 0.0 0.0 42592 7972 pts/15 Ss+ Apr06 0:00 zsh + line.match(/^([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+(.*)$/) + end.compact.map do |m| + { + user: m[1], + pid: m[2], + cpu: m[3], + mem: m[4], + vsz: m[5], + rss: m[6], + tty: m[7], + stat: m[8], + start: m[9], + time: m[10], + command: m[11] + } + end + + @list = all_cmds.find_all do |hm| + hm[:command] =~ grep + end + end + end + + def list + @list + end + + def processes( grep ) + Processes.new(grep) + end + end +end + +include Serverspec::Type \ No newline at end of file diff --git a/lib/resources/ssh_conf.rb b/lib/resources/ssh_conf.rb new file mode 100644 index 000000000..ef5d8f078 --- /dev/null +++ b/lib/resources/ssh_conf.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 +# copyright: 2015, Dominik Richter +# license: All rights reserved + +require 'utils/simpleconfig' + +class SshConf + + def initialize( conf_path, type = nil ) + @runner = Specinfra::Runner + @conf_path = conf_path + @conf_dir = File.expand_path(File.dirname @conf_path) + @files_contents = {} + @content = nil + @params = nil + typename = ( conf_path.include?('sshd') ? 'server' : 'client' ) + @type = type || "SSH #{typename} configuration" + read_content + end + + def to_s + @type + end + + def content + @content ||= read_content + end + + def params *opts + @params || read_content + res = @params + opts.each do |opt| + res = res[opt] unless res.nil? + end + res + end + + def method_missing name + @params || read_content + @params[name.to_s] + end + + def read_content + @content = read_file(@conf_path) + @params = SimpleConfig.new(@content, assignment_re: /^\s*(\S+?)\s+(.*?)\s*$/).params + @content + end + + def read_file(path) + @files_contents[path] ||= @runner.get_file_content(path).stdout + end +end + diff --git a/lib/utils/parseconfig.rb b/lib/utils/parseconfig.rb new file mode 100644 index 000000000..66ecf9608 --- /dev/null +++ b/lib/utils/parseconfig.rb @@ -0,0 +1,182 @@ +# +# Author:: BJ Dierkes +# Copyright:: Copyright (c) 2006,2013 BJ Dierkes +# License:: MIT +# URL:: https://github.com/datafolklabs/ruby-parseconfig +# + +# This class was written to simplify the parsing of configuration +# files in the format of "param = value". Please review the +# demo files included with this package. +# +# For further information please refer to the './doc' directory +# as well as the ChangeLog and README files included. +# + +# Note: A group is a set of parameters defined for a subpart of a +# config file + +class ParseConfig + + Version = '1.0.6' + + attr_accessor :conf, :params, :groups + + # Initialize the class with raw config data 'conf' + # The class objects are dynamically generated by the + # name of the 'param' in the config file. Therefore, if + # the config file is 'param = value' then the itializer + # will eval "@param = value" + # + def initialize(conf=nil, separator = '=') + @params = {} + @groups = [] + @splitRegex = '\s*' + separator + '\s*' + + self.import_config(conf) + end + + # Import data from the config to our config object. + def import_config(raw) + # The config is top down.. anything after a [group] gets added as part + # of that group until a new [group] is found. + group = nil + raw.split("\n").each_with_index do |line, i| + line.strip! + + # force_encoding not available in all versions of ruby + begin + if i.eql? 0 and line.include?("\xef\xbb\xbf".force_encoding("UTF-8")) + line.delete!("\xef\xbb\xbf".force_encoding("UTF-8")) + end + rescue NoMethodError + end + + unless (/^\#/.match(line)) + if(/#{@splitRegex}/.match(line)) + param, value = line.split(/#{@splitRegex}/, 2) + var_name = "#{param}".chomp.strip + value = value.chomp.strip + new_value = '' + if (value) + if value =~ /^['"](.*)['"]$/ + new_value = $1 + else + new_value = value + end + else + new_value = '' + end + + if group + self.add_to_group(group, var_name, new_value) + else + self.add(var_name, new_value) + end + + elsif(/^\[(.+)\]$/.match(line).to_a != []) + group = /^\[(.+)\]$/.match(line).to_a[1] + self.add(group, {}) + + end + end + end + end + + # This method will provide the value held by the object "@param" + # where "@param" is actually the name of the param in the config + # file. + # + # DEPRECATED - will be removed in future versions + # + def get_value(param) + puts "ParseConfig Deprecation Warning: get_value() is deprecated. Use " + \ + "config['param'] or config['group']['param'] instead." + return self.params[param] + end + + # This method is a shortcut to accessing the @params variable + def [](param) + return self.params[param] + end + + # This method returns all parameters/groups defined in a config file. + def get_params() + return self.params.keys + end + + # List available sub-groups of the config. + def get_groups() + return self.groups + end + + # This method adds an element to the config object (not the config file) + # By adding a Hash, you create a new group + def add(param_name, value) + if value.class == Hash + if self.params.has_key?(param_name) + if self.params[param_name].class == Hash + self.params[param_name].merge!(value) + elsif self.params.has_key?(param_name) + if self.params[param_name].class != value.class + raise ArgumentError, "#{param_name} already exists, and is of different type!" + end + end + else + self.params[param_name] = value + end + if ! self.groups.include?(param_name) + self.groups.push(param_name) + end + else + self.params[param_name] = value + end + end + + # Add parameters to a group. Note that parameters with the same name + # could be placed in different groups + def add_to_group(group, param_name, value) + if ! self.groups.include?(group) + self.add(group, {}) + end + self.params[group][param_name] = value + end + + # Writes out the config file to output_stream + def write(output_stream=STDOUT, quoted=true) + self.params.each do |name,value| + if value.class.to_s != 'Hash' + if quoted == true + output_stream.puts "#{name} = \"#{value}\"" + else + output_stream.puts "#{name} = #{value}" + end + end + end + output_stream.puts "\n" + + self.groups.each do |group| + output_stream.puts "[#{group}]" + self.params[group].each do |param, value| + if quoted == true + output_stream.puts "#{param} = \"#{value}\"" + else + output_stream.puts "#{param} = #{value}" + end + end + output_stream.puts "\n" + end + end + + # Public: Compare this ParseConfig to some other ParseConfig. For two config to + # be equivalent, they must have the same sections with the same parameters + # + # other - The other ParseConfig. + # + # Returns true if ParseConfig are equivalent and false if they differ. + + def eql?(other) + self.params == other.params && self.groups == other.groups + end + alias == eql? +end diff --git a/lib/utils/simpleconfig.rb b/lib/utils/simpleconfig.rb new file mode 100644 index 000000000..df73118f0 --- /dev/null +++ b/lib/utils/simpleconfig.rb @@ -0,0 +1,81 @@ +# encoding: utf-8 +# copyright: 2015, Dominik Richter +# license: All rights reserved + +class SimpleConfig + attr_reader :params + def initialize( raw_data, opts = {} ) + parse(raw_data, opts) + end + + # Parse some data + # quotes: quoting characters, which are parsed, so everything inside + # it will be part of a string + # multiline: allow quoted text to span multiple lines + # comment_char: char which identifies comments + # standalone_comments: comments must appear alone in a line; if set to true, + # no comments can be added to the end of an assignment/statement line + def parse( raw_data, opts = {} ) + @params = {} + options = default_options.merge(opts) + rest = raw_data + rest = parse_rest(rest, options) while rest.length > 0 + end + + private + + def parse_rest( rest, opts ) + idx_nl = rest.index("\n") + idx_comment = rest.index('#') + idx_nl = rest.length if idx_nl.nil? + idx_comment = idx_nl+1 if idx_comment.nil? + # is a comment inside this line + if idx_comment < idx_nl + if idx_comment == 0 + line = '' + else + line = rest[0..(idx_comment-1)] + # in case we don't allow comments at the end + # of an assignment/statement, ignore it and fall + # back to treating this as a regular line + if opts[:standalone_comments] && !is_empty_line(line) + line = rest[0..(idx_nl-1)] + end + end + # if there is no comment in this line + else + if idx_nl == 0 + line = '' + else + line = rest[0..(idx_nl-1)] + end + end + # now line contains what we are interested in parsing + # check if it is an assignment + m = opts[:assignment_re].match(line) + if !m.nil? + @params[m[1]] ||= [] + @params[m[1]].push(m[2]) + elsif !is_empty_line(line) + @params[line.strip] ||= [] + end + + # return whatever is left + return rest[(idx_nl+1)..-1] || '' + end + + def is_empty_line l + l =~ /^\s*$/ + end + + def default_options + { + quotes: '', + multiline: false, + comment_char: '#', + assignment_re: /^\s*([^=]*?)\s*=\s*(.*?)\s*$/, + standalone_comments: false + } + end + +end \ No newline at end of file diff --git a/lib/utils/spec_helper.rb b/lib/utils/spec_helper.rb new file mode 100644 index 000000000..090b7e694 --- /dev/null +++ b/lib/utils/spec_helper.rb @@ -0,0 +1,40 @@ +# encoding: utf-8 +# copyright: 2015, Dominik Richter +# license: All rights reserved + +require 'serverspec' + +# Run spec on an ssh target +if ENV['SSH_SPEC'] + + require 'pathname' + require 'net/ssh' + + set :backend, :ssh + + RSpec.configure do |c| + options = {} + c.sudo_password = ENV['SUDO_PASSWORD'] || ENV['sudo_password'] + c.host = ENV['TARGET_HOST'] + options[:password] = ENV['LOGIN_PASSWORD'] || ENV['password'] + options[:user] = ENV['LOGIN_USERNAME'] || ENV['user'] || Etc.getlogin + + if !ENV['LOGIN_KEY'].nil? + options[:keys] = [ENV['LOGIN_KEY']] + options[:keys_only] = true + end + + # TODO: optional, will be removed + if options[:user].nil? + raise 'specify a user for login via env LOGIN_USERNAME= or by adding user=' + end + + ssh_conf = Net::SSH::Config.for(c.host) + c.ssh_options = options.merge(ssh_conf) + end + +# Run spec on local machine +else + require 'serverspec' + set :backend, :exec +end diff --git a/lib/vulcano.rb b/lib/vulcano.rb new file mode 100644 index 000000000..26b94c47f --- /dev/null +++ b/lib/vulcano.rb @@ -0,0 +1,19 @@ +# encoding: utf-8 +# copyright: 2015, Dominik Richter +# license: All rights reserved + +require 'utils/spec_helper' + +require 'resources/mysql_conf' +require 'resources/mysql_session' +require 'resources/postgres_conf' +require 'resources/postgres_session' +require 'resources/processes' +require 'resources/ssh_conf' + +# Dummy module for handling additional attributes +# which may be injected by the user. This covers data +# like passwords, usernames, or configuration flags. +def attributes what, required: false + return nil +end