nodespec 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|