nodespec 0.1.9 → 0.1.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +90 -0
- data/LICENSE.md +21 -0
- data/README.md +107 -0
- data/Rakefile +5 -0
- data/lib/nodespec/backend_proxy/base.rb +20 -0
- data/lib/nodespec/backend_proxy/cmd.rb +9 -0
- data/lib/nodespec/backend_proxy/exec.rb +21 -0
- data/lib/nodespec/backend_proxy/ssh.rb +28 -0
- data/lib/nodespec/backend_proxy/unixshell_utility.rb +32 -0
- data/lib/nodespec/backend_proxy/winrm.rb +23 -0
- data/lib/nodespec/backends.rb +13 -0
- data/lib/nodespec/command_execution.rb +16 -0
- data/lib/nodespec/communication_adapters/aws_ec2.rb +24 -0
- data/lib/nodespec/communication_adapters/local_backend.rb +15 -0
- data/lib/nodespec/communication_adapters/native_communicator.rb +32 -0
- data/lib/nodespec/communication_adapters/remote_backend.rb +15 -0
- data/lib/nodespec/communication_adapters/ssh.rb +14 -0
- data/lib/nodespec/communication_adapters/ssh_communicator.rb +54 -0
- data/lib/nodespec/communication_adapters/vagrant.rb +37 -0
- data/lib/nodespec/communication_adapters/winrm.rb +13 -0
- data/lib/nodespec/communication_adapters/winrm_communicator.rb +65 -0
- data/lib/nodespec/communication_adapters.rb +22 -0
- data/lib/nodespec/local_command_runner.rb +17 -0
- data/lib/nodespec/node.rb +56 -0
- data/lib/nodespec/node_configurations.rb +29 -0
- data/lib/nodespec/provisioning/ansible.rb +96 -0
- data/lib/nodespec/provisioning/chef.rb +68 -0
- data/lib/nodespec/provisioning/puppet.rb +55 -0
- data/lib/nodespec/provisioning/shellscript.rb +19 -0
- data/lib/nodespec/provisioning.rb +14 -0
- data/lib/nodespec/run_options.rb +14 -0
- data/lib/nodespec/runtime_gem_loader.rb +19 -0
- data/lib/nodespec/shared_examples_support.rb +13 -0
- data/lib/nodespec/verbose_output.rb +9 -0
- data/lib/nodespec/version.rb +3 -0
- data/nodespec.gemspec +28 -0
- data/spec/backend_proxy/base_spec.rb +29 -0
- data/spec/backend_proxy/exec_spec.rb +34 -0
- data/spec/backend_proxy/ssh_spec.rb +32 -0
- data/spec/backend_proxy/unixshell_utility_spec.rb +29 -0
- data/spec/backend_proxy/winrm_spec.rb +34 -0
- data/spec/command_execution_spec.rb +36 -0
- data/spec/communication_adapters/aws_ec2_spec.rb +70 -0
- data/spec/communication_adapters/local_backend_spec.rb +38 -0
- data/spec/communication_adapters/native_communicator_spec.rb +53 -0
- data/spec/communication_adapters/remote_backend_spec.rb +46 -0
- data/spec/communication_adapters/ssh_communicator_spec.rb +121 -0
- data/spec/communication_adapters/ssh_spec.rb +18 -0
- data/spec/communication_adapters/vagrant_spec.rb +61 -0
- data/spec/communication_adapters/winrm_communicator_spec.rb +111 -0
- data/spec/communication_adapters/winrm_spec.rb +18 -0
- data/spec/communication_adapters_spec.rb +29 -0
- data/spec/local_command_runner_spec.rb +26 -0
- data/spec/node_configurations_spec.rb +41 -0
- data/spec/node_spec.rb +110 -0
- data/spec/provisioning/ansible_spec.rb +143 -0
- data/spec/provisioning/chef_spec.rb +87 -0
- data/spec/provisioning/puppet_spec.rb +54 -0
- data/spec/provisioning/shellscript_spec.rb +20 -0
- data/spec/provisioning_spec.rb +52 -0
- data/spec/runtime_gem_loader_spec.rb +33 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/backend.rb +10 -0
- data/spec/support/init_with_current_node.rb +4 -0
- data/spec/support/local_command.rb +12 -0
- data/spec/support/node_command.rb +12 -0
- data/spec/support/ssh_communicator.rb +9 -0
- data/spec/support/winrm_communicator.rb +9 -0
- metadata +105 -3
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'nodespec/verbose_output'
|
2
|
+
require 'nodespec/runtime_gem_loader'
|
3
|
+
require_relative 'remote_backend'
|
4
|
+
|
5
|
+
module NodeSpec
|
6
|
+
module CommunicationAdapters
|
7
|
+
class WinrmCommunicator
|
8
|
+
include RemoteBackend
|
9
|
+
include VerboseOutput
|
10
|
+
DEFAULT_PORT = 5985
|
11
|
+
DEFAULT_TRANSPORT = :plaintext
|
12
|
+
DEFAULT_TRANSPORT_OPTIONS = {disable_sspi: true}
|
13
|
+
|
14
|
+
attr_reader :session, :os
|
15
|
+
|
16
|
+
def initialize(hostname, os = nil, options = {})
|
17
|
+
@os = os
|
18
|
+
@hostname = hostname
|
19
|
+
opts = options.dup
|
20
|
+
port = opts.delete('port') || DEFAULT_PORT
|
21
|
+
@endpoint = "http://#{hostname}:#{port}/wsman"
|
22
|
+
|
23
|
+
if opts.has_key?('transport')
|
24
|
+
@transport = opts.delete('transport').to_sym
|
25
|
+
@options = opts
|
26
|
+
else
|
27
|
+
@transport = DEFAULT_TRANSPORT
|
28
|
+
@options = DEFAULT_TRANSPORT_OPTIONS.merge(opts)
|
29
|
+
end
|
30
|
+
@options = @options.inject({}) {|h,(k,v)| h[k.to_sym] = v; h}
|
31
|
+
end
|
32
|
+
|
33
|
+
def bind_to(configuration)
|
34
|
+
if configuration.ssh
|
35
|
+
close_ssh_session(configuration.ssh)
|
36
|
+
configuration.ssh = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
current_session = configuration.winrm
|
40
|
+
if current_session.nil? || current_session.endpoint != @endpoint
|
41
|
+
current_session = start_winrm_session
|
42
|
+
configuration.winrm = current_session
|
43
|
+
configuration.host = @hostname
|
44
|
+
end
|
45
|
+
@session = current_session
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def close_ssh_session(session)
|
51
|
+
msg = "\nClosing connection to #{session.host}"
|
52
|
+
msg << ":#{session.options[:port]}" if session.options[:port]
|
53
|
+
verbose_puts msg
|
54
|
+
session.close
|
55
|
+
end
|
56
|
+
|
57
|
+
def start_winrm_session
|
58
|
+
RuntimeGemLoader.require_or_fail('winrm') do
|
59
|
+
verbose_puts "\nConnecting to #{@endpoint}..."
|
60
|
+
WinRM::WinRMWebService.new(@endpoint, @transport, @options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'nodespec/communication_adapters/native_communicator'
|
2
|
+
|
3
|
+
module NodeSpec
|
4
|
+
module CommunicationAdapters
|
5
|
+
def self.get_communicator(node_name, os = nil, adapter_name = nil, adapter_options = {})
|
6
|
+
if adapter_name
|
7
|
+
require_relative "communication_adapters/#{adapter_name}.rb"
|
8
|
+
clazz = adapter_class(adapter_name)
|
9
|
+
clazz.communicator_for(node_name, os, adapter_options)
|
10
|
+
else
|
11
|
+
NativeCommunicator.new(os)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def self.adapter_class(name)
|
18
|
+
adapter_classname = name.split('_').map(&:capitalize).join('')
|
19
|
+
self.const_get(adapter_classname)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'nodespec/run_options'
|
3
|
+
require 'nodespec/command_execution'
|
4
|
+
|
5
|
+
module NodeSpec
|
6
|
+
module LocalCommandRunner
|
7
|
+
include CommandExecution
|
8
|
+
|
9
|
+
def run_command command
|
10
|
+
execute_within_timeout(command) do
|
11
|
+
output, status = Open3.capture2e(command)
|
12
|
+
verbose_puts(output)
|
13
|
+
status.success?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'specinfra/helper'
|
2
|
+
require 'nodespec/communication_adapters'
|
3
|
+
require 'nodespec/communication_adapters/native_communicator'
|
4
|
+
|
5
|
+
module NodeSpec
|
6
|
+
class Node
|
7
|
+
class BadNodeNameError < StandardError; end
|
8
|
+
|
9
|
+
WORKING_DIR = '.nodespec'
|
10
|
+
attr_reader :os, :communicator, :name
|
11
|
+
|
12
|
+
def initialize(node_name, options = nil)
|
13
|
+
@name = validate(node_name)
|
14
|
+
opts = (options || {}).dup
|
15
|
+
@os = opts.delete('os')
|
16
|
+
adapter_name = opts.delete('adapter')
|
17
|
+
@communicator = CommunicationAdapters.get_communicator(@name, @os, adapter_name, opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
def backend
|
21
|
+
@communicator.backend
|
22
|
+
end
|
23
|
+
|
24
|
+
[:create_directory, :create_file].each do |met|
|
25
|
+
define_method(met) do |*args|
|
26
|
+
path_argument = args.shift
|
27
|
+
unless path_argument.start_with?('/')
|
28
|
+
backend_proxy.create_directory WORKING_DIR
|
29
|
+
path_argument = "#{WORKING_DIR}/#{path_argument}"
|
30
|
+
end
|
31
|
+
backend_proxy.send(met, path_argument, *args)
|
32
|
+
path_argument
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_temp_directory(path)
|
37
|
+
path = path[1..-1] if path.start_with?('/')
|
38
|
+
create_directory("#{backend_proxy.temp_directory}/#{path}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute(command)
|
42
|
+
backend_proxy.execute(command)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def backend_proxy
|
48
|
+
@backend_proxy ||= @communicator.backend_proxy
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate(name)
|
52
|
+
raise BadNodeNameError.new unless name =~ /^[a-zA-Z0-9][a-zA-Z0-9. \-_]+\s*$/
|
53
|
+
name.strip.gsub(' ', '-')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'singleton'
|
3
|
+
require_relative 'node'
|
4
|
+
|
5
|
+
module NodeSpec
|
6
|
+
class NodeConfigurations
|
7
|
+
include Singleton
|
8
|
+
attr_reader :current_settings
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
filename = ENV['NODESPEC_CONFIG'] || 'nodespec_config.yml'
|
12
|
+
data = YAML.load_file(filename) if File.exists?(filename)
|
13
|
+
@predefined_settings = data || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(node_name, options = nil)
|
17
|
+
case options
|
18
|
+
when String
|
19
|
+
raise "Cannot find nodespec settings '#{options}'" unless @predefined_settings.key?(options)
|
20
|
+
opts = @predefined_settings[options]
|
21
|
+
when Hash
|
22
|
+
opts = options
|
23
|
+
else
|
24
|
+
opts = {}
|
25
|
+
end
|
26
|
+
Node.new(node_name, opts)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'json'
|
4
|
+
require 'erb'
|
5
|
+
require 'nodespec/local_command_runner'
|
6
|
+
|
7
|
+
module NodeSpec
|
8
|
+
module Provisioning
|
9
|
+
class Ansible
|
10
|
+
include LocalCommandRunner
|
11
|
+
CUSTOM_CONFIG_FILENAME = 'nodespec_ansible_cfg'
|
12
|
+
CUSTOM_INVENTORY_FILENAME = 'nodespec_ansible_hosts'
|
13
|
+
AUTO_DISCOVERY_HOST_TEMPLATE = <<-EOS
|
14
|
+
<%= "[" + group + "]" if group %>
|
15
|
+
<%= @node.name %> ansible_ssh_host=<%= @node.communicator.session.transport.host %> ansible_ssh_port=<%= @node.communicator.session.transport.port %>
|
16
|
+
EOS
|
17
|
+
|
18
|
+
def initialize(node)
|
19
|
+
@node = node
|
20
|
+
@sudo_enabled = true
|
21
|
+
@cmd_prefix_entries = []
|
22
|
+
@tmp_files = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_config_path(path)
|
26
|
+
@cmd_prefix_entries << "ANSIBLE_CONFIG=#{path.shellescape}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def ansible_config(text)
|
30
|
+
file = create_temp_file(CUSTOM_CONFIG_FILENAME, text)
|
31
|
+
@cmd_prefix_entries << "ANSIBLE_CONFIG=#{file.path.shellescape}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def enable_host_auto_discovery(group = nil)
|
35
|
+
file = create_temp_file(CUSTOM_INVENTORY_FILENAME, ERB.new(AUTO_DISCOVERY_HOST_TEMPLATE).result(binding))
|
36
|
+
@hostfile_option = "-i #{file.path.shellescape}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_hostfile_path(path)
|
40
|
+
@hostfile_option = "-i #{path.shellescape}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_host_key_checking(enabled)
|
44
|
+
@cmd_prefix_entries << "ANSIBLE_HOST_KEY_CHECKING=#{enabled.to_s.capitalize}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def run_as_sudo(enabled = true)
|
48
|
+
@sudo_enabled = enabled
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_extra_vars(vars = {})
|
52
|
+
@extra_vars_option = "-e '#{JSON.generate(vars)}'"
|
53
|
+
end
|
54
|
+
|
55
|
+
def ansible_playbook(playbook_path, options = [])
|
56
|
+
build_and_run("ansible-playbook #{playbook_path.shellescape} -l #{@node.name}", options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def ansible_module(module_name, module_arguments, options = [])
|
60
|
+
build_and_run("ansible #{@node.name} -m #{module_name} -a #{module_arguments.shellescape}", options)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def build_and_run(cmd, options = [])
|
66
|
+
ssh_session = @node.communicator.session
|
67
|
+
key_option = ssh_session.options[:keys].is_a?(Array) ? ssh_session.options[:keys].join(',') : ssh_session.options[:keys]
|
68
|
+
cmd = [
|
69
|
+
(@cmd_prefix_entries.join(' ') unless @cmd_prefix_entries.empty?),
|
70
|
+
cmd,
|
71
|
+
@hostfile_option,
|
72
|
+
"-u #{ssh_session.options[:user]}",
|
73
|
+
"--private-key=#{key_option.shellescape}",
|
74
|
+
sudo_option(ssh_session.options[:user]),
|
75
|
+
"#{options.join(' ')}",
|
76
|
+
@extra_vars_option
|
77
|
+
].compact.join(' ')
|
78
|
+
|
79
|
+
run_command(cmd)
|
80
|
+
@tmp_files.each(&:close!)
|
81
|
+
end
|
82
|
+
|
83
|
+
def sudo_option(user)
|
84
|
+
'--sudo' if user != 'root' and @sudo_enabled
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_temp_file(filename, content)
|
88
|
+
Tempfile.new(filename).tap do |f|
|
89
|
+
f.write(content)
|
90
|
+
f.flush
|
91
|
+
@tmp_files << f
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module NodeSpec
|
5
|
+
module Provisioning
|
6
|
+
class Chef
|
7
|
+
CLIENT_CONFIG_FILENAME = 'chef_client.rb'
|
8
|
+
ATTRIBUTES_FILENAME = 'chef_client_attributes.json'
|
9
|
+
NODES_DIRNAME = 'chef_nodes'
|
10
|
+
|
11
|
+
def initialize(node)
|
12
|
+
@node = node
|
13
|
+
@custom_attributes = {}
|
14
|
+
@configuration_entries = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def chef_apply_execute(snippet, options = [])
|
18
|
+
@node.execute("chef-apply #{options.join(' ')} -e #{snippet.shellescape}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def chef_apply_recipe(recipe_file, options = [])
|
22
|
+
@node.execute("chef-apply #{recipe_file.shellescape} #{options.join(' ')}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_cookbook_paths(*paths)
|
26
|
+
unless paths.empty?
|
27
|
+
paths_in_quotes = paths.map {|p| "'#{p}'"}
|
28
|
+
@configuration_entries << %Q(cookbook_path [#{paths_in_quotes.join(",")}])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_attributes(attributes)
|
33
|
+
@custom_attributes = attributes
|
34
|
+
end
|
35
|
+
|
36
|
+
def chef_client_config(text)
|
37
|
+
@configuration_entries << text
|
38
|
+
end
|
39
|
+
|
40
|
+
def chef_client_runlist(*args)
|
41
|
+
run_list_items, options = [], []
|
42
|
+
run_list_items << args.take_while {|arg| arg.is_a? String}
|
43
|
+
options += args.last if args.last.is_a? Array
|
44
|
+
options << configuration_option
|
45
|
+
options << attributes_option
|
46
|
+
@node.execute("chef-client -z #{options.compact.join(' ')} -o #{run_list_items.join(',').shellescape}")
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def configuration_option
|
52
|
+
unless @configuration_entries.any? {|c| c =~ /^node_path .+$/}
|
53
|
+
nodes_directory = @node.create_temp_directory(NODES_DIRNAME)
|
54
|
+
@configuration_entries.unshift("node_path '#{nodes_directory}'")
|
55
|
+
end
|
56
|
+
config_file = @node.create_file(CLIENT_CONFIG_FILENAME, @configuration_entries.join("\n"))
|
57
|
+
"-c #{config_file}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def attributes_option
|
61
|
+
unless @custom_attributes.empty?
|
62
|
+
attr_file = @node.create_file(ATTRIBUTES_FILENAME, JSON.generate(@custom_attributes))
|
63
|
+
"-j #{attr_file}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module NodeSpec
|
6
|
+
module Provisioning
|
7
|
+
class Puppet
|
8
|
+
HIERADATA_DIRNAME = 'puppet_hieradata'
|
9
|
+
HIERA_CONFIG_FILENAME = 'puppet_hiera.yaml'
|
10
|
+
HIERA_DEFAULT_HIERARCHY = 'common'
|
11
|
+
HIERA_CONFIG_TEMPLATE = <<-EOS
|
12
|
+
:backends:
|
13
|
+
- yaml
|
14
|
+
:yaml:
|
15
|
+
:datadir: <%= hieradata_dir %>
|
16
|
+
:hierarchy:
|
17
|
+
- #{HIERA_DEFAULT_HIERARCHY}
|
18
|
+
EOS
|
19
|
+
def initialize(node)
|
20
|
+
@node = node
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_modulepaths(*paths)
|
24
|
+
@modulepath_option = "--modulepath #{paths.join(':').shellescape}" unless paths.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_facts(facts)
|
28
|
+
@facts = facts.reduce("") { |fact, pair| "FACTER_#{pair[0]}=#{pair[1].shellescape} #{fact}" }
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_hieradata(values)
|
32
|
+
unless values.empty?
|
33
|
+
hieradata_dir = @node.create_directory(HIERADATA_DIRNAME)
|
34
|
+
@node.create_file("#{HIERADATA_DIRNAME}/#{HIERA_DEFAULT_HIERARCHY}.yaml", YAML.dump(values))
|
35
|
+
hiera_config = @node.create_file(HIERA_CONFIG_FILENAME, ERB.new(HIERA_CONFIG_TEMPLATE).result(binding))
|
36
|
+
@hiera_option = "--hiera_config #{hiera_config}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def puppet_apply_execute(snippet, options = [])
|
41
|
+
@node.execute("#{group_command_options(options)} -e #{snippet.shellescape}")
|
42
|
+
end
|
43
|
+
|
44
|
+
def puppet_apply_manifest(manifest_file, options = [])
|
45
|
+
@node.execute("#{group_command_options(options)} #{manifest_file.shellescape}")
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def group_command_options(options)
|
51
|
+
%Q[#{@facts}puppet apply #{@modulepath_option} #{@hiera_option} #{options.join(' ')}]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module NodeSpec
|
4
|
+
module Provisioning
|
5
|
+
class Shellscript
|
6
|
+
def initialize(node)
|
7
|
+
@node = node
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute_file(path)
|
11
|
+
@node.execute(path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute_script(script)
|
15
|
+
@node.execute("sh -c #{script.shellescape}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Dir[File.join(File.dirname(__FILE__), 'provisioning/*.rb')].each {|f| require f}
|
2
|
+
|
3
|
+
module NodeSpec
|
4
|
+
module Provisioning
|
5
|
+
self.constants.each do |provisioner_name|
|
6
|
+
provisioner_class = self.const_get(provisioner_name)
|
7
|
+
define_method("provision_node_with_#{provisioner_name.downcase}".to_sym) do |&block|
|
8
|
+
@provisioners ||= {}
|
9
|
+
@provisioners[provisioner_name.downcase] = provisioner_class.new(NodeSpec.current_node) unless @provisioners.key?(provisioner_name.downcase)
|
10
|
+
@provisioners[provisioner_name.downcase].instance_eval(&block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module NodeSpec
|
2
|
+
module RunOptions
|
3
|
+
class << self
|
4
|
+
[:verbose, :run_local_with_sudo].each do |attr|
|
5
|
+
attr_accessor attr
|
6
|
+
alias_method("#{attr}?".to_sym, attr)
|
7
|
+
end
|
8
|
+
attr_writer :command_timeout
|
9
|
+
def command_timeout
|
10
|
+
@command_timeout || 600
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module NodeSpec
|
2
|
+
module RuntimeGemLoader
|
3
|
+
DEFAULT_ERROR_MSG = 'Consider installing the missing gem'
|
4
|
+
def self.require_or_fail(gem_name, error_message = nil)
|
5
|
+
begin
|
6
|
+
require gem_name
|
7
|
+
yield if block_given?
|
8
|
+
rescue LoadError => e
|
9
|
+
err = <<-EOS
|
10
|
+
Error: #{e.message}
|
11
|
+
#{error_message || DEFAULT_ERROR_MSG}
|
12
|
+
|
13
|
+
gem install '#{gem_name}'
|
14
|
+
EOS
|
15
|
+
fail(err)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
module NodeSpec
|
3
|
+
module SharedExamplesSupport
|
4
|
+
def it_is_node_with_roles *instance_roles
|
5
|
+
instance_roles.each {|role| it_behaves_like role}
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.alias_it_behaves_like_to :it_is_node_configured_with, 'is a node configured with:'
|
12
|
+
config.extend NodeSpec::SharedExamplesSupport
|
13
|
+
end
|
data/nodespec.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
libdir = File.join(File.dirname(__FILE__), 'lib')
|
2
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
3
|
+
|
4
|
+
require 'nodespec/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'nodespec'
|
8
|
+
gem.version = NodeSpec::VERSION
|
9
|
+
gem.summary = 'RSpec style tests for multiple nodes/server instances with support for provisioning instructions'
|
10
|
+
gem.description = 'RSpec style tests for multiple nodes/server instances with support for provisioning instructions'
|
11
|
+
|
12
|
+
gem.authors = ['Silvio Montanari']
|
13
|
+
gem.homepage = 'https://github.com/smontanari/nodespec'
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.test_files = gem.files.grep(%r{^spec/})
|
16
|
+
gem.require_paths = ['lib']
|
17
|
+
|
18
|
+
gem.required_ruby_version = '>= 1.9.3'
|
19
|
+
|
20
|
+
gem.add_runtime_dependency 'net-ssh'
|
21
|
+
gem.add_runtime_dependency 'serverspec'
|
22
|
+
gem.add_runtime_dependency 'specinfra', '>= 1.18.4'
|
23
|
+
gem.add_development_dependency 'rspec', '~> 3.0'
|
24
|
+
gem.add_development_dependency 'aws-sdk'
|
25
|
+
gem.add_development_dependency 'winrm'
|
26
|
+
gem.add_development_dependency 'bundler'
|
27
|
+
gem.add_development_dependency 'rake'
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'nodespec/backend_proxy/base'
|
2
|
+
|
3
|
+
module NodeSpec
|
4
|
+
module BackendProxy
|
5
|
+
describe Base do
|
6
|
+
describe '#create_file' do
|
7
|
+
it 'executes the generated command' do
|
8
|
+
allow(subject).to receive(:cmd_create_file).with('test/path', 'test content').and_return('command')
|
9
|
+
expect(subject).to receive(:execute).with('command')
|
10
|
+
|
11
|
+
subject.create_file('test/path', 'test content')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#create_directory' do
|
16
|
+
it 'executes the generated command' do
|
17
|
+
allow(subject).to receive(:cmd_create_directory).with('test/path').and_return('command')
|
18
|
+
expect(subject).to receive(:execute).with('command')
|
19
|
+
|
20
|
+
subject.create_directory('test/path')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'behaves like a command execution' do
|
25
|
+
expect(subject).to respond_to(:execute_within_timeout)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'nodespec/backend_proxy/exec'
|
3
|
+
|
4
|
+
module NodeSpec
|
5
|
+
module BackendProxy
|
6
|
+
describe Exec do
|
7
|
+
shared_examples 'a command run' do |original_command, actual_command, sudo_option|
|
8
|
+
let(:cmd_status) { double('status') }
|
9
|
+
|
10
|
+
before do
|
11
|
+
NodeSpec::RunOptions.run_local_with_sudo = sudo_option
|
12
|
+
allow(Open3).to receive(:capture2e).with(actual_command).and_return(['test output', cmd_status])
|
13
|
+
allow(subject).to receive(:execute_within_timeout).with(actual_command).and_yield
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
it 'returns true if the command succeeds' do
|
18
|
+
allow(cmd_status).to receive(:success?).and_return(true)
|
19
|
+
|
20
|
+
expect(subject.execute(original_command)).to be_truthy
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns false if the command fails' do
|
24
|
+
allow(cmd_status).to receive(:success?).and_return(false)
|
25
|
+
|
26
|
+
expect(subject.execute(original_command)).to be_falsy
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it_behaves_like 'a command run', 'test command', 'test command', false
|
31
|
+
it_behaves_like 'a command run', 'test command', 'sudo test command', true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'nodespec/backend_proxy/ssh'
|
2
|
+
|
3
|
+
module NodeSpec
|
4
|
+
module BackendProxy
|
5
|
+
describe Ssh do
|
6
|
+
let(:ssh_session) { double('ssh session') }
|
7
|
+
let(:subject) {Ssh.new(ssh_session)}
|
8
|
+
|
9
|
+
shared_examples 'an ssh session command run' do |user, original_command, actual_command|
|
10
|
+
before do
|
11
|
+
allow(ssh_session).to receive(:options).and_return({user: user})
|
12
|
+
allow(subject).to receive(:execute_within_timeout).with(actual_command).and_yield
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'returns true if the command succeeds' do
|
16
|
+
allow(ssh_session).to receive(:exec!).with(actual_command).and_yield(nil, 'a stream', 'test data')
|
17
|
+
|
18
|
+
expect(subject.execute(original_command)).to be_truthy
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns false if the command fails' do
|
22
|
+
allow(ssh_session).to receive(:exec!).with(actual_command).and_yield(nil, :stderr, 'test data')
|
23
|
+
|
24
|
+
expect(subject.execute(original_command)).to be_falsy
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it_behaves_like 'an ssh session command run', 'root', 'test command', 'test command'
|
29
|
+
it_behaves_like 'an ssh session command run', 'some_user', 'test command', 'sudo test command'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'nodespec/backend_proxy/unixshell_utility'
|
2
|
+
|
3
|
+
module NodeSpec
|
4
|
+
module BackendProxy
|
5
|
+
describe UnixshellUtility do
|
6
|
+
let(:subject) {Object.new.extend UnixshellUtility}
|
7
|
+
|
8
|
+
it 'returns the command as run by sudo' do
|
9
|
+
expect(subject.run_as_sudo('command')).to eq 'sudo command'
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns the command to create a directory' do
|
13
|
+
expect(subject.cmd_create_directory('/path to/dir')).to eq('sh -c "mkdir -p /path\ to/dir"')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns the path to the temp directory' do
|
17
|
+
expect(subject.temp_directory).to eq('/tmp')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'writes the given content to a file' do
|
21
|
+
content = <<-eos
|
22
|
+
some 'text'
|
23
|
+
some "other" text
|
24
|
+
eos
|
25
|
+
expect(subject.cmd_create_file('/path to/file', content)).to eq %Q[sh -c "cat > /path\\ to/file << EOF\nsome 'text'\nsome \\"other\\" text\nEOF"]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|