├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── lib ├── nodespec.rb └── nodespec │ ├── backend_proxy.rb │ ├── backend_proxy │ ├── base.rb │ ├── cmd.rb │ ├── exec.rb │ ├── ssh.rb │ ├── unixshell_utility.rb │ └── winrm.rb │ ├── command_execution.rb │ ├── communication_adapters.rb │ ├── communication_adapters │ ├── aws_ec2.rb │ ├── native_communicator.rb │ ├── ssh.rb │ ├── ssh_communicator.rb │ ├── vagrant.rb │ ├── winrm.rb │ └── winrm_communicator.rb │ ├── configuration_binding.rb │ ├── local_command_runner.rb │ ├── node.rb │ ├── node_configurations.rb │ ├── provisioning.rb │ ├── provisioning │ ├── ansible.rb │ ├── chef.rb │ ├── puppet.rb │ └── shellscript.rb │ ├── run_options.rb │ ├── runtime_gem_loader.rb │ ├── shared_examples_support.rb │ ├── verbose_output.rb │ └── version.rb ├── nodespec.gemspec └── spec ├── backend_proxy ├── base_spec.rb ├── exec_spec.rb ├── ssh_spec.rb ├── unixshell_utility_spec.rb └── winrm_spec.rb ├── command_execution_spec.rb ├── communication_adapters ├── aws_ec2_spec.rb ├── native_communicator_spec.rb ├── ssh_communicator_spec.rb ├── ssh_spec.rb ├── vagrant_spec.rb ├── winrm_communicator_spec.rb └── winrm_spec.rb ├── communication_adapters_spec.rb ├── configuration_binding_spec.rb ├── local_command_runner_spec.rb ├── node_configurations_spec.rb ├── node_spec.rb ├── provisioning ├── ansible_spec.rb ├── chef_spec.rb ├── puppet_spec.rb └── shellscript_spec.rb ├── provisioning_spec.rb ├── runtime_gem_loader_spec.rb ├── spec_helper.rb └── support ├── backend.rb ├── communication_adapters.rb ├── init_with_current_node.rb ├── local_command.rb └── node_command.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .rspec 2 | .ruby-version 3 | pkg/ 4 | TODO.txt -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | nodespec (1.0.2) 5 | net-ssh 6 | os 7 | serverspec (~> 2.3) 8 | specinfra (~> 2.4) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | akami (1.2.2) 14 | gyoku (>= 0.4.0) 15 | nokogiri 16 | aws-sdk (1.44.0) 17 | json (~> 1.4) 18 | nokogiri (>= 1.4.4) 19 | builder (3.2.2) 20 | diff-lcs (1.2.5) 21 | ffi (1.9.3) 22 | gssapi (1.0.3) 23 | ffi (>= 1.0.1) 24 | gyoku (1.1.1) 25 | builder (>= 2.1.2) 26 | httpclient (2.4.0) 27 | httpi (0.9.7) 28 | rack 29 | json (1.8.1) 30 | little-plugger (1.1.3) 31 | logging (1.8.2) 32 | little-plugger (>= 1.1.3) 33 | multi_json (>= 1.8.4) 34 | mini_portile (0.6.0) 35 | multi_json (1.10.1) 36 | net-scp (1.2.1) 37 | net-ssh (>= 2.6.5) 38 | net-ssh (2.9.1) 39 | nokogiri (1.6.2.1) 40 | mini_portile (= 0.6.0) 41 | nori (1.1.5) 42 | os (0.9.6) 43 | rack (1.5.2) 44 | rake (10.3.2) 45 | rspec (3.1.0) 46 | rspec-core (~> 3.1.0) 47 | rspec-expectations (~> 3.1.0) 48 | rspec-mocks (~> 3.1.0) 49 | rspec-core (3.1.7) 50 | rspec-support (~> 3.1.0) 51 | rspec-expectations (3.1.2) 52 | diff-lcs (>= 1.2.0, < 2.0) 53 | rspec-support (~> 3.1.0) 54 | rspec-its (1.0.1) 55 | rspec-core (>= 2.99.0.beta1) 56 | rspec-expectations (>= 2.99.0.beta1) 57 | rspec-mocks (3.1.3) 58 | rspec-support (~> 3.1.0) 59 | rspec-support (3.1.2) 60 | rubyntlm (0.1.1) 61 | savon (0.9.5) 62 | akami (~> 1.0) 63 | builder (>= 2.1.2) 64 | gyoku (>= 0.4.0) 65 | httpi (~> 0.9) 66 | nokogiri (>= 1.4.0) 67 | nori (~> 1.0) 68 | wasabi (~> 1.0) 69 | serverspec (2.3.1) 70 | multi_json 71 | rspec (~> 3.0) 72 | rspec-its 73 | specinfra (~> 2.3) 74 | specinfra (2.4.1) 75 | net-scp 76 | net-ssh 77 | uuidtools (2.1.4) 78 | wasabi (1.0.0) 79 | nokogiri (>= 1.4.0) 80 | winrm (1.1.3) 81 | gssapi (~> 1.0.0) 82 | httpclient (~> 2.2, >= 2.2.0.2) 83 | logging (~> 1.6, >= 1.6.1) 84 | nokogiri (~> 1.5) 85 | rubyntlm (~> 0.1.1) 86 | savon (= 0.9.5) 87 | uuidtools (~> 2.1.2) 88 | 89 | PLATFORMS 90 | ruby 91 | 92 | DEPENDENCIES 93 | aws-sdk 94 | bundler 95 | nodespec! 96 | rake 97 | rspec (~> 3.1) 98 | winrm 99 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Silvio Montanari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nodespec 2 | ======== 3 | 4 | RSpec style tests for multiple nodes/server instances with support for provisioning with puppet, chef, ansible. 5 | 6 | - [**Read the documentation**](https://github.com/smontanari/nodespec/wiki) 7 | 8 | ## Nodespec vs Serverspec 9 | [Serverspec](http://serverspec.org) is a popular gem that allows you to write rspec tests to validate your servers/hosts configuration. 10 | 11 | Nodespec is not an alternative to Serverspec, rather it's build on top of it, so you can still leverage all of its features while enjoying some extra goodies. 12 | 13 | #### What's different 14 | Nodespec overcomes some of the limitations of Serverspec, such as mixed OS'/backends support (Windows/WinRM and UN*X/SSH) and easy configurability and connectivity to your target hosts. 15 | 16 | Nodespec enables a declarative way of configuring your remote connections, be it simple Ssh, a Vagrant box, or an Amazon EC2 instance. 17 | 18 | Nodespec adds support for issuing provisioning commands (through Puppet, Chef or Ansible) to your target hosts, that could be incorporated as part of your test setup. 19 | 20 | Below is a quick summary of the main features of nodespec. Refer to the [wiki](https://github.com/smontanari/nodespec/wiki) for more details and examples. 21 | 22 | ## Nodespec features 23 | 24 | #### Hostname declared or inferred in specs 25 | 26 | In serverspec you would typically have to create folders named after your target servers, whereas in nodespec you can simply declare your target server names as your spec example group: 27 | 28 | ```ruby 29 | describe "test.example1.com" do 30 | ... 31 | end 32 | ``` 33 | ```ruby 34 | describe "test.example2.com" do 35 | ... 36 | end 37 | ``` 38 | #### Easy connection configuration 39 | No more ruby code and create programmatically SSH objects, just a quick and easy inline (or file/yaml based) configuration: 40 | 41 | ```ruby 42 | describe "test.example.com", nodespec: { 43 | 'adapter' => 'ssh', 44 | 'user' => 'testuser', 45 | 'keys' => 'path/to/key' 46 | } do 47 | ... 48 | end 49 | ``` 50 | #### Support connections to Windows & Un*x servers 51 | One of the major limitations of serverspec is that you have to make a hard decision beforehand on which OS/backend you are targeting with your tests. In particular you have to include specific specinfra modules in your `spec_helper` depending on whether you're connecting to Un\*x or Windows machines, using ssh or winrm. Practically it is not possible to test different OSs/backends as part of the same spec run, unless you start hacking some conditional logic in your spec_helper, that is. 52 | 53 | With Nodespec that problem is resolved and you can easily connect and test multiple OS and multiple backends in the same rspec run by simply specifying the desired adapter and connection properties for your target host. For instance, to connect to a Windows/WinRM box: 54 | ```ruby 55 | describe "test.windows-example.com", nodespec: { 56 | 'adapter' => 'winrm', 57 | 'user' => 'testuser', 58 | 'pass' => 'somepass', 59 | 'transport' => 'ssl' 60 | 'basic_auth_only' => true 61 | } do 62 | ... 63 | end 64 | ``` 65 | ## Provisioning (aka TDD your infrastructure) 66 | Nodespec provides support for running Chef, Puppet, Ansible or shellscript commands as part of your tests, e.g.: 67 | 68 | #### Chef 69 | ```ruby 70 | describe "server1", nodespec: {'adapter' => 'vagrant'} do 71 | before :all do 72 | provision_node_with_chef do 73 | set_cookbook_paths '/vshared/src/cookbooks' 74 | set_attributes demo: {crontab: {user: 'peter'}} 75 | chef_client_runlist 'demo::folders', 'demo::crontab' 76 | end 77 | end 78 | ... 79 | end 80 | ``` 81 | 82 | #### Puppet 83 | ```ruby 84 | describe "test.server", nodespec: {'adapter' => 'ssh'} do 85 | before :all do 86 | provision_node_with_puppet do 87 | set_modulepaths '/vshared/src/modules' 88 | set_hieradata('users' => {'roger' => {'uid' => 5801}, 'peter' => {'uid' => 5802}}) 89 | puppet_apply_execute "include demo::wheel_users" 90 | end 91 | end 92 | ... 93 | end 94 | ``` 95 | 96 | #### Ansible 97 | ```ruby 98 | describe "i-8f5e74r9", nodespec: {'adapter' => 'aws_ec2'} do 99 | before :all do 100 | provision_node_with_ansible do 101 | enable_host_auto_discovery 102 | set_host_key_checking false 103 | ansible_playbook 'src/ansible/demo.yml', ['--sudo'] 104 | end 105 | end 106 | ... 107 | end 108 | ``` -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new 5 | Rake::Task[:build].prerequisites << Rake::Task[:spec] -------------------------------------------------------------------------------- /lib/nodespec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'serverspec' 3 | require 'specinfra' 4 | 5 | require 'nodespec/node_configurations' 6 | require 'nodespec/configuration_binding' 7 | require 'nodespec/run_options' 8 | require 'nodespec/provisioning' 9 | 10 | module NodeSpec 11 | class << self 12 | attr_reader :current_node 13 | def set_current_node(name, options) 14 | @current_node = NodeConfigurations.instance.get(name, options) 15 | yield(@current_node) if block_given? 16 | end 17 | end 18 | end 19 | 20 | RSpec.configure do |config| 21 | config.before :all do |eg| 22 | if eg.class.metadata.key?(:nodespec) 23 | NodeSpec.set_current_node(eg.class.description, eg.class.metadata[:nodespec]) do |node| 24 | Specinfra.configuration.backend = node.communicator.backend 25 | node.communicator.init_session(NodeSpec::ConfigurationBinding.new(config)) 26 | 27 | property[:os] = nil # prevent os caching so we can switch os for any node test 28 | config.os = Specinfra::Helper::DetectOs.const_get(node.os).detect if node.os 29 | end 30 | end 31 | end 32 | 33 | config.include(NodeSpec::Provisioning) 34 | end 35 | -------------------------------------------------------------------------------- /lib/nodespec/backend_proxy.rb: -------------------------------------------------------------------------------- 1 | require 'specinfra/backend' 2 | %w[exec cmd ssh winrm].each {|f| require_relative "backend_proxy/#{f}"} 3 | 4 | module NodeSpec 5 | module BackendProxy 6 | PROXIES = { 7 | exec: 'Exec', 8 | ssh: 'Ssh', 9 | cmd: 'Cmd', 10 | winrm: 'Winrm' 11 | } 12 | class SpecinfraCompatibilityError < StandardError; end 13 | 14 | PROXIES.values.each do |name| 15 | raise SpecinfraCompatibilityError.new("Specinfra::Backend::#{name} is not defined!") unless Specinfra::Backend.const_defined?(name) 16 | end 17 | 18 | def self.create(backend, *args) 19 | class_name = PROXIES.fetch(backend, PROXIES[:exec]) 20 | self.const_get(class_name).send(:new, *args) 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/nodespec/backend_proxy/base.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/verbose_output' 2 | require 'nodespec/command_execution' 3 | 4 | module NodeSpec 5 | module BackendProxy 6 | class Base 7 | include CommandExecution 8 | 9 | [:create_directory, :create_file].each do |m| 10 | define_method(m) do |*args| 11 | execute(send("cmd_#{m}", *args)) 12 | end 13 | end 14 | 15 | def execute(command) 16 | raise "You must subclass #{self.class} and implement #execute" 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/nodespec/backend_proxy/cmd.rb: -------------------------------------------------------------------------------- 1 | module NodeSpec 2 | module BackendProxy 3 | class Cmd 4 | def execute command 5 | raise 'Not supported yet' 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/nodespec/backend_proxy/exec.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'nodespec/run_options' 3 | require_relative 'base' 4 | require_relative 'unixshell_utility' 5 | 6 | module NodeSpec 7 | module BackendProxy 8 | class Exec < Base 9 | include UnixshellUtility 10 | 11 | def execute command 12 | command = run_as_sudo(command) if NodeSpec::RunOptions.run_local_with_sudo? 13 | execute_within_timeout(command) do 14 | output, status = Open3.capture2e(command) 15 | verbose_puts(output) 16 | status.success? 17 | end 18 | end 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/nodespec/backend_proxy/ssh.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | require_relative 'unixshell_utility' 3 | 4 | module NodeSpec 5 | module BackendProxy 6 | class Ssh < Base 7 | include UnixshellUtility 8 | 9 | ROOT_USER = 'root' 10 | 11 | def initialize(ssh) 12 | @ssh_session = ssh 13 | end 14 | 15 | def execute(command) 16 | command = run_as_sudo(command) if @ssh_session.options[:user] != ROOT_USER 17 | execute_within_timeout(command) do 18 | success = true 19 | @ssh_session.exec!(command) do |ch, stream, data| 20 | verbose_puts(data) 21 | success = stream != :stderr 22 | end 23 | success 24 | end 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/nodespec/backend_proxy/unixshell_utility.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module NodeSpec 4 | module BackendProxy 5 | module UnixshellUtility 6 | SUDO_PREFIX = 'sudo' 7 | TEMP_DIR = '/tmp' 8 | 9 | def run_as_sudo(cmd) 10 | "#{SUDO_PREFIX} #{cmd}" 11 | end 12 | 13 | def cmd_create_directory(path) 14 | shellcmd("mkdir -p #{path.shellescape}") 15 | end 16 | 17 | def cmd_create_file(path, content) 18 | shellcmd("cat > #{path.shellescape} << EOF\n#{content.strip.gsub('"', '\"')}\nEOF") 19 | end 20 | 21 | def temp_directory 22 | TEMP_DIR 23 | end 24 | 25 | private 26 | 27 | def shellcmd(cmd) 28 | %Q[sh -c "#{cmd}"] 29 | end 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /lib/nodespec/backend_proxy/winrm.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/command_execution' 2 | require 'nodespec/run_options' 3 | require_relative 'base' 4 | 5 | module NodeSpec 6 | module BackendProxy 7 | class Winrm < Base 8 | def initialize(winrm) 9 | @winrm_session = winrm 10 | end 11 | 12 | def execute command 13 | @winrm_session.set_timeout(NodeSpec::RunOptions.command_timeout) 14 | result = @winrm_session.powershell(command) 15 | stdout, stderr = [:stdout, :stderr].map do |s| 16 | result[:data].select {|item| item.key? s}.map {|item| item[s]}.join 17 | end 18 | [stdout, stderr].each {|s| verbose_puts s} 19 | result[:exitcode] == 0 and stderr.empty? 20 | end 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/nodespec/command_execution.rb: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | require_relative 'verbose_output' 3 | require_relative 'run_options' 4 | 5 | module NodeSpec 6 | module CommandExecution 7 | class CommandExecutionError < StandardError; end 8 | include VerboseOutput 9 | 10 | def execute_within_timeout(command, timeout = NodeSpec::RunOptions.command_timeout, &block) 11 | verbose_puts "\nExecuting command:\n#{command}" 12 | command_success = Timeout::timeout(timeout, &block) 13 | raise CommandExecutionError.new 'The command execution failed. Enable verbosity to check the output.' unless command_success 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/nodespec/communication_adapters.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/communication_adapters/native_communicator' 2 | 3 | module NodeSpec 4 | module CommunicationAdapters 5 | def self.get_communicator(node_name, 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, adapter_options) 10 | else 11 | NativeCommunicator.new 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 -------------------------------------------------------------------------------- /lib/nodespec/communication_adapters/aws_ec2.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/runtime_gem_loader' 2 | require_relative 'ssh_communicator' 3 | require_relative 'winrm_communicator' 4 | 5 | module NodeSpec 6 | module CommunicationAdapters 7 | class AwsEc2 8 | GEMLOAD_ERROR = 'In order to use any aws adapter you must install the Amazon Web Service gem' 9 | def self.communicator_for(node_name, options = {}) 10 | RuntimeGemLoader.require_or_fail('aws-sdk', GEMLOAD_ERROR) do 11 | instance_name = options['instance'] || node_name 12 | ec2_instance = AWS.ec2.instances[instance_name] 13 | 14 | raise "EC2 Instance #{instance_name} is not reachable" unless ec2_instance.exists? && ec2_instance.status == :running 15 | if options.has_key?('winrm') 16 | WinrmCommunicator.new(ec2_instance.public_ip_address, options['winrm']) 17 | else 18 | SshCommunicator.new(ec2_instance.public_ip_address, options['ssh'] || {}) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /lib/nodespec/communication_adapters/native_communicator.rb: -------------------------------------------------------------------------------- 1 | require 'os' 2 | require 'nodespec/verbose_output' 3 | require 'nodespec/backend_proxy' 4 | 5 | module NodeSpec 6 | module CommunicationAdapters 7 | class NativeCommunicator 8 | include VerboseOutput 9 | 10 | def init_session(configuration) 11 | configuration.unbind_ssh_session 12 | configuration.unbind_winrm_session 13 | verbose_puts "\nRunning on local host..." 14 | end 15 | 16 | def backend_proxy 17 | BackendProxy.create(backend) 18 | end 19 | 20 | def backend 21 | OS.windows? ? :cmd : :exec 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/nodespec/communication_adapters/ssh.rb: -------------------------------------------------------------------------------- 1 | require_relative 'ssh_communicator' 2 | 3 | module NodeSpec 4 | module CommunicationAdapters 5 | class Ssh 6 | def self.communicator_for(node_name, options = {}) 7 | opts = options.dup 8 | host = opts.delete('host') || node_name 9 | SshCommunicator.new(host, opts) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/nodespec/communication_adapters/ssh_communicator.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh' 2 | require 'nodespec/verbose_output' 3 | require 'nodespec/backend_proxy' 4 | 5 | module NodeSpec 6 | module CommunicationAdapters 7 | class SshCommunicator 8 | include VerboseOutput 9 | attr_reader :session 10 | 11 | def initialize(host, options = {}) 12 | @host = host 13 | @ssh_options = Net::SSH.configuration_for(@host) 14 | @user = options['user'] || @ssh_options[:user] 15 | %w[port password keys].each do |param| 16 | @ssh_options[param.to_sym] = options[param] if options[param] 17 | end 18 | end 19 | 20 | def init_session(configuration) 21 | configuration.unbind_winrm_session 22 | 23 | @session = configuration.bind_ssh_session_for({host: @host, port: @ssh_options[:port]}) do 24 | msg = "\nConnecting to #{@host}" 25 | msg << ":#{@ssh_options[:port]}" if @ssh_options[:port] 26 | msg << " as #{@user}..." 27 | verbose_puts msg 28 | Net::SSH.start(@host, @user, @ssh_options) 29 | end 30 | end 31 | 32 | def backend_proxy 33 | BackendProxy.create(:ssh, @session) 34 | end 35 | 36 | def backend 37 | :ssh 38 | end 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /lib/nodespec/communication_adapters/vagrant.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require_relative 'ssh_communicator' 3 | 4 | module NodeSpec 5 | module CommunicationAdapters 6 | class Vagrant 7 | def self.communicator_for(node_name, options = {}) 8 | vm_name = options['vm_name'] || node_name 9 | fetch_connection_details(vm_name) do |host, opts| 10 | SshCommunicator.new(host, opts) 11 | end 12 | end 13 | 14 | private 15 | 16 | def self.fetch_connection_details(vm_name) 17 | cmd = "vagrant --machine-readable ssh-config #{vm_name}" 18 | output, status = Open3.capture2e(cmd) 19 | raise parse_error_data(output) unless status.success? 20 | yield(parse_ssh_config_data(output)) if block_given? 21 | end 22 | 23 | def self.parse_ssh_config_data(data) 24 | /^\s*HostName\s+(?.*)$/ =~ data 25 | /^\s*Port\s+(?\d+)$/ =~ data 26 | /^\s*User\s+(?.*)$/ =~ data 27 | /^\s*IdentityFile\s+(?.*)$/ =~ data 28 | [hostname, {'port' => port.to_i, 'user' => username, 'keys' => private_key_path}] 29 | end 30 | 31 | def self.parse_error_data(data) 32 | /^.*,error-exit,(?.*)$/ =~ data 33 | error 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/nodespec/communication_adapters/winrm.rb: -------------------------------------------------------------------------------- 1 | require_relative 'winrm_communicator' 2 | 3 | module NodeSpec 4 | module CommunicationAdapters 5 | class Winrm 6 | def self.communicator_for(node_name, options = {}) 7 | opts = options.dup 8 | hostname = opts.delete('host') || node_name 9 | WinrmCommunicator.new(hostname, opts) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/nodespec/communication_adapters/winrm_communicator.rb: -------------------------------------------------------------------------------- 1 | require 'winrm' 2 | require 'nodespec/verbose_output' 3 | require 'nodespec/runtime_gem_loader' 4 | require 'nodespec/backend_proxy' 5 | 6 | module NodeSpec 7 | module CommunicationAdapters 8 | class WinrmCommunicator 9 | include VerboseOutput 10 | DEFAULT_PORT = 5985 11 | DEFAULT_TRANSPORT = :plaintext 12 | DEFAULT_TRANSPORT_OPTIONS = {disable_sspi: true} 13 | 14 | attr_reader :session 15 | 16 | def initialize(host, options = {}) 17 | @host = host 18 | opts = options.dup 19 | port = opts.delete('port') || DEFAULT_PORT 20 | @endpoint = "http://#{host}:#{port}/wsman" 21 | 22 | if opts.has_key?('transport') 23 | @transport = opts.delete('transport').to_sym 24 | @options = opts 25 | else 26 | @transport = DEFAULT_TRANSPORT 27 | @options = DEFAULT_TRANSPORT_OPTIONS.merge(opts) 28 | end 29 | @options = @options.inject({}) {|h,(k,v)| h[k.to_sym] = v; h} 30 | end 31 | 32 | def init_session(configuration) 33 | configuration.unbind_ssh_session 34 | 35 | @session = configuration.bind_winrm_session_for({host: @host, endpoint: @endpoint}) do 36 | RuntimeGemLoader.require_or_fail('winrm') do 37 | verbose_puts "\nConnecting to #{@endpoint}..." 38 | WinRM::WinRMWebService.new(@endpoint, @transport, @options) 39 | end 40 | end 41 | end 42 | 43 | def backend_proxy 44 | BackendProxy.create(:winrm, @session) 45 | end 46 | 47 | def backend 48 | :winrm 49 | end 50 | end 51 | end 52 | end -------------------------------------------------------------------------------- /lib/nodespec/configuration_binding.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/verbose_output' 2 | 3 | module NodeSpec 4 | class ConfigurationBinding 5 | include VerboseOutput 6 | 7 | BACKEND_ACTIONS = { 8 | ssh: { 9 | diff_session: lambda { |ssh, params| ssh.host != params[:host] || ssh.options[:port] != params[:port] }, 10 | bind_attributes: lambda { |ssh, config| config.ssh_options = ssh.options } 11 | }, 12 | winrm: { 13 | diff_session: lambda { |winrm, params| winrm.endpoint != params[:endpoint] } 14 | } 15 | } 16 | 17 | def initialize(configuration) 18 | @configuration = configuration 19 | end 20 | 21 | BACKEND_ACTIONS.keys.each do |backend| 22 | define_method("bind_#{backend}_session_for") do |params, &block| 23 | bind_session_for(backend, params, &block) 24 | end 25 | end 26 | 27 | def unbind_ssh_session 28 | if @configuration.ssh 29 | msg = "\nClosing connection to #{@configuration.ssh.host}" 30 | msg << ":#{@configuration.ssh.options[:port]}" if @configuration.ssh.options[:port] 31 | verbose_puts msg 32 | @configuration.ssh.close 33 | end 34 | @configuration.ssh = @configuration.ssh_options = @configuration.host =nil 35 | end 36 | 37 | def unbind_winrm_session 38 | verbose_puts "\nClosing connection to #{@configuration.winrm.endpoint}" if @configuration.winrm 39 | @configuration.winrm = @configuration.host = nil 40 | end 41 | 42 | private 43 | 44 | def bind_session_for(backend, params) 45 | current_session = @configuration.send(backend) 46 | if current_session.nil? || BACKEND_ACTIONS[backend][:diff_session].call(current_session, params) 47 | send("unbind_#{backend}_session") 48 | current_session = yield 49 | @configuration.send("#{backend}=", current_session) 50 | @configuration.host = params[:host] 51 | BACKEND_ACTIONS[backend][:bind_attributes].call(current_session, @configuration) if BACKEND_ACTIONS[backend][:bind_attributes] 52 | end 53 | current_session 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /lib/nodespec/local_command_runner.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/node.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/communication_adapters' 2 | 3 | module NodeSpec 4 | class Node 5 | class BadNodeNameError < StandardError; end 6 | 7 | WORKING_DIR = '.nodespec' 8 | attr_reader :os, :communicator, :name 9 | 10 | def initialize(node_name, options = nil) 11 | @name = validate(node_name) 12 | opts = (options || {}).dup 13 | @os = opts.delete('os') 14 | adapter_name = opts.delete('adapter') 15 | @communicator = CommunicationAdapters.get_communicator(@name, adapter_name, opts) 16 | end 17 | 18 | [:create_directory, :create_file].each do |met| 19 | define_method(met) do |*args| 20 | path_argument = args.shift 21 | unless path_argument.start_with?('/') 22 | backend_proxy.create_directory WORKING_DIR 23 | path_argument = "#{WORKING_DIR}/#{path_argument}" 24 | end 25 | backend_proxy.send(met, path_argument, *args) 26 | path_argument 27 | end 28 | end 29 | 30 | def create_temp_directory(path) 31 | path = path[1..-1] if path.start_with?('/') 32 | create_directory("#{backend_proxy.temp_directory}/#{path}") 33 | end 34 | 35 | def execute(command) 36 | backend_proxy.execute(command) 37 | end 38 | 39 | private 40 | 41 | def backend_proxy 42 | @backend_proxy ||= @communicator.backend_proxy 43 | end 44 | 45 | def validate(name) 46 | raise BadNodeNameError.new unless name =~ /^[a-zA-Z0-9][a-zA-Z0-9. \-_]+\s*$/ 47 | name.strip.gsub(' ', '-') 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /lib/nodespec/node_configurations.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/provisioning.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/provisioning/ansible.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/provisioning/chef.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/provisioning/puppet.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/provisioning/shellscript.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/run_options.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/runtime_gem_loader.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/shared_examples_support.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/nodespec/verbose_output.rb: -------------------------------------------------------------------------------- 1 | require_relative 'run_options' 2 | 3 | module NodeSpec 4 | module VerboseOutput 5 | def verbose_puts(msg) 6 | puts msg if NodeSpec::RunOptions.verbose? 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/nodespec/version.rb: -------------------------------------------------------------------------------- 1 | module NodeSpec 2 | VERSION = '1.0.2' 3 | end 4 | -------------------------------------------------------------------------------- /nodespec.gemspec: -------------------------------------------------------------------------------- 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.license = 'MIT' 15 | 16 | gem.files = `git ls-files`.split($/) 17 | gem.test_files = gem.files.grep(%r{^spec/}) 18 | gem.require_paths = ['lib'] 19 | 20 | gem.required_ruby_version = '>= 1.9.3' 21 | 22 | gem.add_runtime_dependency 'net-ssh' 23 | gem.add_runtime_dependency 'serverspec', '~> 2.3' 24 | gem.add_runtime_dependency 'specinfra', '~> 2.4' 25 | gem.add_runtime_dependency 'os' 26 | gem.add_development_dependency 'rspec', '~> 3.1' 27 | gem.add_development_dependency 'aws-sdk' 28 | gem.add_development_dependency 'winrm' 29 | gem.add_development_dependency 'bundler' 30 | gem.add_development_dependency 'rake' 31 | end 32 | -------------------------------------------------------------------------------- /spec/backend_proxy/base_spec.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/backend_proxy/exec_spec.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/backend_proxy/ssh_spec.rb: -------------------------------------------------------------------------------- 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 | 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 -------------------------------------------------------------------------------- /spec/backend_proxy/unixshell_utility_spec.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/backend_proxy/unixshell_utility' 2 | 3 | module NodeSpec 4 | module BackendProxy 5 | describe UnixshellUtility do 6 | 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 -------------------------------------------------------------------------------- /spec/backend_proxy/winrm_spec.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/backend_proxy/winrm' 2 | 3 | module NodeSpec 4 | module BackendProxy 5 | describe Winrm do 6 | let(:winrm) { double('winrm session') } 7 | subject {Winrm.new(winrm)} 8 | before do 9 | allow(winrm).to receive(:set_timeout).with(NodeSpec::RunOptions.command_timeout) 10 | end 11 | 12 | it 'returns true upon successful execution' do 13 | result = {exitcode: 0, data: [{stdout: 'output line 1'}, {stdout: 'output line 2'}]} 14 | allow(winrm).to receive(:powershell).and_return(result) 15 | 16 | expect(subject.execute('command')).to be_truthy 17 | end 18 | 19 | it 'returns false if stderr not empty' do 20 | result = {exitcode: 0, data: [{stdout: 'output line 1', stderr: 'error line 1'}, {stdout: 'output line 2', stderr: 'error line 2'}]} 21 | allow(winrm).to receive(:powershell).and_return(result) 22 | 23 | expect(subject.execute('command')).to be_falsy 24 | end 25 | 26 | it 'returns false if exitcode not zero' do 27 | result = {exitcode: 1, data: [{stdout: 'output line 1'}, {stdout: 'output line 2'}]} 28 | allow(winrm).to receive(:powershell).and_return(result) 29 | 30 | expect(subject.execute('command')).to be_falsy 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/command_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/command_execution' 2 | 3 | module NodeSpec 4 | describe CommandExecution do 5 | subject {Object.new.extend CommandExecution} 6 | 7 | it 'does not raise an error if the command succeeds' do 8 | subject.execute_within_timeout('test command') { true } 9 | end 10 | 11 | it 'raises an error if the command fails' do 12 | expect { 13 | subject.execute_within_timeout('test command') { false } 14 | }.to raise_error CommandExecution::CommandExecutionError 15 | end 16 | 17 | context 'custom timeout' do 18 | it 'fails if running longer than the set timeout' do 19 | expect { 20 | subject.execute_within_timeout('test command', 1) { sleep(2) } 21 | }.to raise_error Timeout::Error 22 | end 23 | end 24 | 25 | context 'preset timeout' do 26 | before do 27 | NodeSpec::RunOptions.command_timeout = 1 28 | end 29 | it 'fails if running longer than the set timeout' do 30 | expect { 31 | subject.execute_within_timeout('test command') { sleep(2) } 32 | }.to raise_error Timeout::Error 33 | end 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /spec/communication_adapters/aws_ec2_spec.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk' 2 | require 'spec_helper' 3 | require 'nodespec/communication_adapters/aws_ec2' 4 | 5 | module NodeSpec 6 | module CommunicationAdapters 7 | describe AwsEc2 do 8 | let(:ec2_instance) {double('ec2 instance')} 9 | 10 | before do 11 | instance_collection = double 12 | allow(AWS).to receive_message_chain(:ec2, :instances).and_return(instance_collection) 13 | allow(instance_collection).to receive(:[]).with('test-instance').and_return(ec2_instance) 14 | end 15 | 16 | describe 'connecting to a running instance' do 17 | before do 18 | allow(ec2_instance).to receive(:exists?).ordered.and_return(true) 19 | allow(ec2_instance).to receive(:status).ordered.and_return(:running) 20 | allow(ec2_instance).to receive(:public_ip_address).ordered.and_return('ip_address') 21 | end 22 | 23 | %w[ssh winrm].each do |connection| 24 | describe "#{connection} communicator" do 25 | include_context "new_#{connection}_communicator", 'ip_address', 'foo' => 'bar' 26 | 27 | it 'returns communicator with the instance name from the node name' do 28 | expect(AwsEc2.communicator_for('test-instance', connection => {'foo' => 'bar'})).to eq("#{connection} communicator") 29 | end 30 | 31 | it 'returns communicator with the instance name from the options' do 32 | expect(AwsEc2.communicator_for('test_node', 'instance' => 'test-instance', connection => {'foo' => 'bar'})).to eq("#{connection} communicator") 33 | end 34 | end 35 | end 36 | 37 | describe 'openssh default connection' do 38 | include_context "new_ssh_communicator", 'ip_address', {} 39 | 40 | it 'returns an ssh communicator' do 41 | expect(AwsEc2.communicator_for('test-instance')).to eq("ssh communicator") 42 | end 43 | end 44 | end 45 | 46 | describe 'connecting to an unreachable instance' do 47 | context 'not existing instance' do 48 | before do 49 | allow(ec2_instance).to receive(:exists?).ordered.and_return(false) 50 | end 51 | 52 | it 'raises an error' do 53 | expect {AwsEc2.communicator_for('test-instance', 'foo' => 'bar')}.to raise_error 54 | end 55 | end 56 | 57 | context 'not running instance' do 58 | before do 59 | allow(ec2_instance).to receive(:exists?).ordered.and_return(true) 60 | allow(ec2_instance).to receive(:status).ordered.and_return(:whatever_else) 61 | end 62 | 63 | it 'raises an error' do 64 | expect {AwsEc2.communicator_for('test-instance', 'foo' => 'bar')}.to raise_error 65 | end 66 | end 67 | end 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /spec/communication_adapters/native_communicator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/communication_adapters/native_communicator' 3 | 4 | module NodeSpec 5 | module CommunicationAdapters 6 | describe NativeCommunicator do 7 | describe 'providing backend proxy' do 8 | context 'non Windows os' do 9 | before do 10 | allow(OS).to receive(:windows?).and_return(false) 11 | end 12 | include_examples 'providing a backend', :exec, BackendProxy::Exec 13 | end 14 | 15 | context 'Windows os' do 16 | before do 17 | allow(OS).to receive(:windows?).and_return(true) 18 | end 19 | 20 | include_examples 'providing a backend', :cmd, BackendProxy::Cmd 21 | end 22 | end 23 | 24 | describe '#init_session' do 25 | let(:configuration) {double('configuration')} 26 | 27 | it 'unbinds other sessions' do 28 | expect(configuration).to receive(:unbind_ssh_session) 29 | expect(configuration).to receive(:unbind_winrm_session) 30 | subject.init_session(configuration) 31 | end 32 | end 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /spec/communication_adapters/ssh_communicator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/communication_adapters/ssh_communicator' 3 | 4 | module NodeSpec 5 | module CommunicationAdapters 6 | describe SshCommunicator do 7 | let(:configuration) {double('configuration')} 8 | 9 | shared_context 'creating new session' do |hostname, options| 10 | before do 11 | expect(configuration).to receive(:unbind_winrm_session) 12 | allow(Net::SSH).to receive(:start).with(hostname, 'testuser', options).and_return('session') 13 | allow(configuration).to receive(:bind_ssh_session_for) do |params, &block| 14 | expect(params[:host]).to eq hostname 15 | expect(params[:port]).to eq options[:port] 16 | block.call 17 | end 18 | end 19 | end 20 | 21 | describe 'init the session' do 22 | context 'default options' do 23 | subject {SshCommunicator.new('test.host.name')} 24 | before do 25 | allow(Net::SSH).to receive(:configuration_for).and_return({someoption: 'somevalue', user: 'testuser'}) 26 | end 27 | include_context 'creating new session', 'test.host.name', someoption: 'somevalue', user: 'testuser' 28 | include_examples 'initializing communicator session' 29 | end 30 | 31 | context 'custom options' do 32 | subject {SshCommunicator.new('test.host.name', 'port' => 1234, 'user' => 'testuser', 'password' => 'testpassword', 'keys' => 'testkeys')} 33 | before do 34 | allow(Net::SSH).to receive(:configuration_for).and_return({someoption: 'somevalue', port: 22, user: 'testuser'}) 35 | end 36 | include_context 'creating new session', 'test.host.name', someoption: 'somevalue', port: 1234, user: 'testuser', password: 'testpassword', keys: 'testkeys' 37 | include_examples 'initializing communicator session' 38 | end 39 | 40 | context 'same session' do 41 | subject {SshCommunicator.new('test.host.name', 'port' => 1234, 'user' => 'testuser')} 42 | before do 43 | expect(configuration).to receive(:unbind_winrm_session) 44 | allow(configuration).to receive(:bind_ssh_session_for).with({host: 'test.host.name', port: 1234}).and_return('session') 45 | end 46 | 47 | include_examples 'initializing communicator session' 48 | end 49 | end 50 | 51 | describe 'providing backend proxy' do 52 | subject {SshCommunicator.new('test.host.name')} 53 | include_examples 'providing a backend', :ssh, BackendProxy::Ssh 54 | end 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /spec/communication_adapters/ssh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/communication_adapters/ssh' 3 | 4 | module NodeSpec 5 | module CommunicationAdapters 6 | describe Ssh do 7 | include_examples 'new_communicator', Ssh, 'ssh' 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/communication_adapters/vagrant_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/communication_adapters/vagrant' 3 | 4 | module NodeSpec 5 | module CommunicationAdapters 6 | describe Vagrant do 7 | [['test_vm', 'test_os'], ['test_node', {'vm_name' => 'test_vm'}]].each do |args| 8 | describe "#communicator_for" do 9 | let(:cmd_status) { double('status') } 10 | 11 | before(:each) do 12 | expect(Open3).to receive(:capture2e).with('vagrant --machine-readable ssh-config test_vm').and_return([cmd_output, cmd_status]) 13 | end 14 | 15 | context 'vm not running' do 16 | let(:cmd_output) { 17 | '1402310908,,error-exit,Vagrant::Errors::SSHNotReady,The provider...' 18 | } 19 | 20 | it 'raises an error' do 21 | allow(cmd_status).to receive(:success?).and_return(false) 22 | 23 | expect {Vagrant.communicator_for(*args)}.to raise_error 'Vagrant::Errors::SSHNotReady,The provider...' 24 | end 25 | end 26 | 27 | context 'vm running' do 28 | let(:cmd_output) { 29 | <<-EOS 30 | Host test_vm 31 | HostName test.host.name 32 | User testuser 33 | Port 1234 34 | UserKnownHostsFile /dev/null 35 | StrictHostKeyChecking no 36 | PasswordAuthentication no 37 | IdentityFile /test/path/private_key 38 | IdentitiesOnly yes 39 | LogLevel FATAL 40 | EOS 41 | } 42 | 43 | include_context 'new_ssh_communicator', 'test.host.name', { 44 | 'user' => 'testuser', 45 | 'port' => 1234, 46 | 'keys' => '/test/path/private_key' 47 | } do 48 | before do 49 | allow(cmd_status).to receive(:success?).and_return(true) 50 | end 51 | 52 | it 'returns and ssh communicator initialized from the vagrant command output' do 53 | expect(Vagrant.communicator_for(*args)).to eq('ssh communicator') 54 | end 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | end -------------------------------------------------------------------------------- /spec/communication_adapters/winrm_communicator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/communication_adapters/winrm_communicator' 3 | 4 | module NodeSpec 5 | module CommunicationAdapters 6 | describe WinrmCommunicator do 7 | let(:configuration) {double('configuration')} 8 | 9 | shared_context 'creating new session' do |hostname, port, transport, options| 10 | before do 11 | expect(configuration).to receive(:unbind_ssh_session) 12 | allow(WinRM::WinRMWebService).to receive(:new).with("http://#{hostname}:#{port}/wsman", transport, options).and_return('session') 13 | allow(configuration).to receive(:bind_winrm_session_for) do |params, &block| 14 | expect(params[:host]).to eq hostname 15 | expect(params[:endpoint]).to eq "http://#{hostname}:#{port}/wsman" 16 | block.call 17 | end 18 | end 19 | end 20 | 21 | describe 'init the session' do 22 | context 'default port and transport' do 23 | subject {WinrmCommunicator.new('test.host.name', 'foo' => 'bar')} 24 | include_context 'creating new session', 'test.host.name', 5985, :plaintext, {foo: 'bar', disable_sspi: true} 25 | include_examples 'initializing communicator session' 26 | end 27 | 28 | context 'custom port and transport' do 29 | subject {WinrmCommunicator.new('test.host.name', 'port' => 1234, 'transport' => 'test_transport', 'foo' => 'bar')} 30 | include_context 'creating new session', 'test.host.name', 1234, :test_transport, {foo: 'bar'} 31 | include_examples 'initializing communicator session' 32 | end 33 | 34 | context 'same session' do 35 | subject {WinrmCommunicator.new('test.host.name', 'foo' => 'bar')} 36 | before do 37 | expect(configuration).to receive(:unbind_ssh_session) 38 | allow(configuration).to receive(:bind_winrm_session_for).with({host: 'test.host.name', endpoint: "http://test.host.name:5985/wsman"}).and_return('session') 39 | end 40 | 41 | include_examples 'initializing communicator session' 42 | end 43 | end 44 | 45 | describe 'providing backend proxy' do 46 | subject {WinrmCommunicator.new('test.host.name')} 47 | include_examples 'providing a backend', :winrm, BackendProxy::Winrm 48 | end 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /spec/communication_adapters/winrm_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/communication_adapters/winrm' 3 | 4 | module NodeSpec 5 | module CommunicationAdapters 6 | describe Winrm do 7 | include_examples 'new_communicator', Winrm, 'winrm' 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/communication_adapters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/communication_adapters' 2 | %w[vagrant ssh aws_ec2 winrm].each {|f| require "nodespec/communication_adapters/#{f}"} 3 | 4 | module NodeSpec 5 | describe CommunicationAdapters do 6 | { 7 | 'vagrant' => CommunicationAdapters::Vagrant, 8 | 'ssh' => CommunicationAdapters::Ssh, 9 | 'aws_ec2' => CommunicationAdapters::AwsEc2, 10 | 'winrm' => CommunicationAdapters::Winrm 11 | }.each do |adapter_name, adapter_class| 12 | it "retrieves a communicator from #{adapter_class}" do 13 | expect(adapter_class).to receive(:communicator_for).with('test_node', 'adapter_options' => 'test_options').and_return('communicator') 14 | 15 | communicator = CommunicationAdapters.get_communicator('test_node', adapter_name, 'adapter_options' => 'test_options') 16 | 17 | expect(communicator).to eq('communicator') 18 | end 19 | end 20 | 21 | it 'defaults to a NativeCommunicator' do 22 | expect(CommunicationAdapters::NativeCommunicator).to receive(:new).and_return('communicator') 23 | 24 | communicator = CommunicationAdapters.get_communicator('test_node') 25 | 26 | expect(communicator).to eq('communicator') 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /spec/configuration_binding_spec.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/configuration_binding' 2 | 3 | module NodeSpec 4 | describe ConfigurationBinding do 5 | subject {ConfigurationBinding.new(configuration)} 6 | 7 | let(:configuration) { double('configuration') } 8 | let(:existing_session) {double('existing session')} 9 | let(:new_session) {double('new session')} 10 | 11 | describe '#bind_ssh_session_for' do 12 | shared_examples 'bind ssh session' do 13 | it 'binds the session' do 14 | allow(new_session).to receive(:host).and_return('test.host.name') 15 | allow(new_session).to receive(:options).and_return({port: 1234}) 16 | expect(configuration).to receive(:ssh=).with(new_session) 17 | expect(configuration).to receive(:ssh_options=).with({port: 1234}) 18 | expect(configuration).to receive(:host=).with('test.host.name') 19 | 20 | session = subject.bind_ssh_session_for({host: 'test.host.name', port: 1234}) {new_session} 21 | expect(session).to be(new_session) 22 | end 23 | end 24 | 25 | context 'different or no session' do 26 | before do 27 | expect(configuration).to receive(:ssh=).with(nil) 28 | expect(configuration).to receive(:ssh_options=).with(nil) 29 | expect(configuration).to receive(:host=).with(nil) 30 | end 31 | 32 | context 'not existing session' do 33 | before do 34 | allow(configuration).to receive(:ssh).and_return(nil) 35 | end 36 | 37 | include_examples 'bind ssh session' 38 | end 39 | 40 | context 'different existing session' do 41 | before do 42 | allow(configuration).to receive(:ssh).and_return(existing_session) 43 | expect(existing_session).to receive(:close) 44 | end 45 | 46 | context 'different host' do 47 | before do 48 | allow(existing_session).to receive(:host).and_return('test.anotherhost.name') 49 | allow(existing_session).to receive(:options).and_return({port: 1234}) 50 | end 51 | 52 | include_examples 'bind ssh session' 53 | end 54 | 55 | context 'different port' do 56 | before do 57 | allow(existing_session).to receive(:host).and_return('test.host.name') 58 | allow(existing_session).to receive(:options).and_return({port: 5678}) 59 | end 60 | 61 | include_examples 'bind ssh session' 62 | end 63 | end 64 | end 65 | 66 | context 'same session' do 67 | before do 68 | allow(configuration).to receive(:ssh).and_return(existing_session) 69 | allow(existing_session).to receive(:host).and_return('test.host.name') 70 | allow(existing_session).to receive(:options).and_return({port: 1234}) 71 | expect(existing_session).not_to receive(:close) 72 | expect(configuration).not_to receive(:ssh=) 73 | expect(configuration).not_to receive(:ssh_options=) 74 | expect(configuration).not_to receive(:host=) 75 | end 76 | 77 | it 'does not change the exisintg session' do 78 | session = subject.bind_ssh_session_for({host: 'test.host.name', port: 1234}) {new_session} 79 | expect(session).to be(existing_session) 80 | end 81 | end 82 | end 83 | 84 | describe '#unbind_ssh_session' do 85 | before do 86 | allow(configuration).to receive(:ssh).and_return(existing_session) 87 | expect(existing_session).to receive(:host) 88 | expect(existing_session).to receive(:options).and_return({}) 89 | expect(existing_session).to receive(:close) 90 | expect(configuration).to receive(:ssh=).with(nil) 91 | expect(configuration).to receive(:ssh_options=).with(nil) 92 | expect(configuration).to receive(:host=).with(nil) 93 | end 94 | 95 | it 'removes ssh session from the configuration' do 96 | subject.unbind_ssh_session 97 | end 98 | end 99 | 100 | describe '#bind_winrm_session_for' do 101 | shared_examples 'bind winrm session' do 102 | it 'binds the session' do 103 | allow(new_session).to receive(:endpoint).and_return('test.endpoint') 104 | expect(configuration).to receive(:winrm=).with(new_session) 105 | expect(configuration).to receive(:host=).with('test.host.name') 106 | 107 | session = subject.bind_winrm_session_for({host: 'test.host.name', endpoint: 'test.endpoint'}) {new_session} 108 | expect(session).to be(new_session) 109 | end 110 | end 111 | 112 | context 'different or no session' do 113 | before do 114 | expect(configuration).to receive(:winrm=).with(nil) 115 | expect(configuration).to receive(:host=).with(nil) 116 | end 117 | 118 | context 'not existing session' do 119 | before do 120 | allow(configuration).to receive(:winrm).and_return(nil) 121 | end 122 | 123 | include_examples 'bind winrm session' 124 | end 125 | 126 | context 'different existing session' do 127 | before do 128 | allow(configuration).to receive(:winrm).and_return(existing_session) 129 | end 130 | 131 | context 'different host' do 132 | before do 133 | allow(existing_session).to receive(:endpoint).and_return('test.anotherendpoint') 134 | end 135 | 136 | include_examples 'bind winrm session' 137 | end 138 | end 139 | end 140 | 141 | context 'same session' do 142 | before do 143 | allow(configuration).to receive(:winrm).and_return(existing_session) 144 | allow(existing_session).to receive(:endpoint).and_return('test.endpoint') 145 | expect(configuration).not_to receive(:winrm=) 146 | expect(configuration).not_to receive(:host=) 147 | end 148 | 149 | it 'does not change the exisintg session' do 150 | session = subject.bind_winrm_session_for({host: 'test.host.name', endpoint: 'test.endpoint'}) {new_session} 151 | expect(session).to be(existing_session) 152 | end 153 | end 154 | end 155 | 156 | describe '#unbind_winrm_session' do 157 | before do 158 | expect(configuration).to receive(:winrm) 159 | expect(configuration).to receive(:winrm=).with(nil) 160 | expect(configuration).to receive(:host=).with(nil) 161 | end 162 | 163 | it 'removes winrm session from the configuration' do 164 | subject.unbind_winrm_session 165 | end 166 | end 167 | end 168 | end -------------------------------------------------------------------------------- /spec/local_command_runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/local_command_runner' 3 | 4 | module NodeSpec 5 | describe LocalCommandRunner do 6 | subject {Object.new.extend LocalCommandRunner} 7 | let(:cmd_status) { double('status') } 8 | 9 | before do 10 | allow(Open3).to receive(:capture2e).with('test command').and_return(['test output', cmd_status]) 11 | allow(subject).to receive(:execute_within_timeout).with('test command').and_yield 12 | end 13 | 14 | it 'returns true if the command succeeds' do 15 | allow(cmd_status).to receive(:success?).and_return(true) 16 | 17 | expect(subject.run_command('test command')).to be_truthy 18 | end 19 | 20 | it 'returns false if the command fails' do 21 | allow(cmd_status).to receive(:success?).and_return(false) 22 | 23 | expect(subject.run_command('test command')).to be_falsy 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /spec/node_configurations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/node_configurations' 2 | 3 | module NodeSpec 4 | describe NodeConfigurations do 5 | before do 6 | allow(File).to receive(:exists?).and_return(true) 7 | allow(YAML).to receive(:load_file).and_return({ 8 | 'test_node1' => { foo: 'bar' }, 9 | 'test_node2' => { foo: 'baz' }, 10 | 'test_node3' => { foo: 'qaz' } 11 | }) 12 | end 13 | 14 | describe 'current settings' do 15 | context 'anything but string or hash' do 16 | before do 17 | allow(Node).to receive(:new).with('node_name', {}).and_return('node') 18 | end 19 | [nil, true, 1, [], Object.new].each do |options| 20 | it 'returns a default Node' do 21 | expect(NodeConfigurations.instance.get('node_name')).to eq('node') 22 | end 23 | end 24 | end 25 | 26 | context 'configuration name' do 27 | it 'returns a Node for the given configuration name' do 28 | allow(Node).to receive(:new).with('node_name', {foo: 'baz'}).and_return('node') 29 | expect(NodeConfigurations.instance.get('node_name', 'test_node2')).to eq('node') 30 | end 31 | end 32 | 33 | context 'configuration hash' do 34 | it 'returns a Node with the given configuration' do 35 | allow(Node).to receive(:new).with('node_name', {foo: 'baz'}).and_return('node') 36 | expect(NodeConfigurations.instance.get('node_name', {foo: 'baz'})).to eq('node') 37 | end 38 | end 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /spec/node_spec.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/node' 2 | 3 | module NodeSpec 4 | describe Node do 5 | shared_examples 'node os' do |os| 6 | it "has the expected os" do 7 | expect(subject.os).to eq(os) 8 | end 9 | end 10 | 11 | shared_examples 'run commands' do 12 | it "runs a command through the backend proxy" do 13 | expect(backend_proxy).to receive(:execute).with('test command') 14 | 15 | subject.execute('test command') 16 | end 17 | 18 | it "creates a directory with a path relative to the temporary directory" do 19 | expect(backend_proxy).to receive(:temp_directory).ordered.and_return('/temp/dir') 20 | expect(backend_proxy).to receive(:create_directory).ordered.with('/temp/dir/test_dir') 21 | 22 | expect(subject.create_temp_directory('test_dir')).to eq('/temp/dir/test_dir') 23 | end 24 | 25 | it "creates a directory with a path relative to the node working directory" do 26 | expect(backend_proxy).to receive(:create_directory).ordered.with('.nodespec') 27 | expect(backend_proxy).to receive(:create_directory).ordered.with('.nodespec/test_dir') 28 | 29 | expect(subject.create_directory('test_dir')).to eq('.nodespec/test_dir') 30 | end 31 | 32 | it "writes to a file with a path relative to the node working directory" do 33 | expect(backend_proxy).to receive(:create_directory).ordered.with('.nodespec') 34 | expect(backend_proxy).to receive(:create_file).ordered.with('.nodespec/test/file', 'test content') 35 | 36 | expect(subject.create_file('test/file', 'test content')).to eq('.nodespec/test/file') 37 | end 38 | 39 | it "creates a directory with an absolute path" do 40 | expect(backend_proxy).to receive(:create_directory).with('/test/dir') 41 | 42 | expect(subject.create_directory('/test/dir')).to eq('/test/dir') 43 | end 44 | 45 | it "writes to a file with an absolute path" do 46 | expect(backend_proxy).to receive(:create_file).with('/test/file', 'test content') 47 | 48 | expect(subject.create_file('/test/file', 'test content')).to eq('/test/file') 49 | end 50 | end 51 | 52 | let(:communicator) {double('communicator')} 53 | let(:backend_proxy) {double('backend_proxy')} 54 | 55 | it 'does not change the original options' do 56 | Node.new('test_node', {'os' => 'test', 'foo' => 'bar'}.freeze) 57 | end 58 | 59 | describe 'node name' do 60 | it 'replaces spaces with hyphen' do 61 | subject = Node.new('test node description') 62 | expect(subject.name).to eq('test-node-description') 63 | end 64 | it 'trims spaces at the end' do 65 | subject = Node.new("test.node.description ") 66 | expect(subject.name).to eq('test.node.description') 67 | end 68 | it 'cannot contain punctuation characters' do 69 | "!@#$%^&*()+={}[]\\|:;\"'<>?,/".each_char do |invalid_char| 70 | expect {Node.new("test #{invalid_char} name")}.to raise_error 71 | end 72 | end 73 | it 'cannot start with space, underscore or hyphen characters' do 74 | " _-".each_char do |invalid_char| 75 | expect {Node.new("#{invalid_char} name")}.to raise_error 76 | end 77 | end 78 | end 79 | 80 | describe 'running commands through the communicator' do 81 | let(:adapter) {double('adapter')} 82 | before do 83 | allow(communicator).to receive(:session).and_return('remote session') 84 | allow(communicator).to receive(:backend_proxy).and_return(backend_proxy) 85 | end 86 | 87 | context 'no os given' do 88 | subject {Node.new('test_node', 'adapter' => 'test_adapter', 'foo' => 'bar')} 89 | 90 | before do 91 | allow(CommunicationAdapters).to receive(:get_communicator).with('test_node', 'test_adapter', 'foo' => 'bar').and_return(communicator) 92 | end 93 | 94 | include_examples 'node os', nil 95 | include_examples 'run commands' 96 | end 97 | 98 | context 'os given' do 99 | subject {Node.new('test_node', 'os' => 'test_os', 'adapter' => 'test_adapter', 'foo' => 'bar')} 100 | 101 | before do 102 | allow(CommunicationAdapters).to receive(:get_communicator).with('test_node', 'test_adapter', 'foo' => 'bar').and_return(communicator) 103 | end 104 | 105 | include_examples 'node os', 'test_os' 106 | include_examples 'run commands' 107 | end 108 | end 109 | end 110 | end -------------------------------------------------------------------------------- /spec/provisioning/ansible_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/provisioning/ansible' 3 | 4 | module NodeSpec 5 | module Provisioning 6 | describe Ansible, init_with_current_node: true do 7 | shared_context 'create temp file' do |prefix, path, content| 8 | let(:tmp_file) {double('temp file')} 9 | before do 10 | allow(Tempfile).to receive(:new).with(prefix).and_return(tmp_file) 11 | allow(tmp_file).to receive(:path).and_return(path) 12 | expect(tmp_file).to receive(:write).ordered.with(content) 13 | expect(tmp_file).to receive(:flush).ordered 14 | expect(tmp_file).to receive(:close!).ordered 15 | end 16 | end 17 | before do 18 | allow(current_node).to receive(:name).and_return('test_host') 19 | allow(current_node).to receive_message_chain(:communicator, :session, :options).and_return({ 20 | user: 'test_user', 21 | keys: 'path/to user/key' 22 | }) 23 | end 24 | 25 | describe 'executing an ansible playbook' do 26 | it_executes_the_local_command('ansible-playbook /path\ to/playbook -l test_host -u test_user --private-key=path/to\ user/key --sudo --opt1 --opt2') do 27 | ansible_playbook '/path to/playbook', %w[--opt1 --opt2] 28 | end 29 | 30 | describe 'setting playbook variables' do 31 | it_executes_the_local_command(/^ansible-playbook .* -e '\{"pacman":"mrs","ghosts":\["inky","pinky","clyde","sue"\]\}'$/) do 32 | set_extra_vars pacman: 'mrs', ghosts: %w[inky pinky clyde sue] 33 | ansible_playbook '/path to/playbook' 34 | end 35 | end 36 | end 37 | 38 | describe 'executing an ansible module' do 39 | before do 40 | allow(current_node).to receive_message_chain(:communicator, :session, :transport, :host).and_return('test.host') 41 | allow(current_node).to receive_message_chain(:communicator, :session, :transport, :port).and_return(1234) 42 | 43 | end 44 | 45 | it_executes_the_local_command('ansible test_host -m module -a module\ arguments -u test_user --private-key=path/to\ user/key --sudo --opt1 --opt2') do 46 | ansible_module 'module', 'module arguments', %w[--opt1 --opt2] 47 | end 48 | 49 | describe 'running ansible' do 50 | context 'multiples keys' do 51 | before do 52 | allow(current_node).to receive_message_chain(:communicator, :session, :options).and_return({ 53 | user: 'test_user', 54 | keys: ['path/to user/key1', 'path/to user/key2'] 55 | }) 56 | end 57 | it_executes_the_local_command('ansible test_host -m module -a module\ arguments -u test_user --private-key=path/to\ user/key1,path/to\ user/key2 --sudo --opt1 --opt2') do 58 | ansible_module 'module', 'module arguments', %w[--opt1 --opt2] 59 | end 60 | end 61 | 62 | context 'disable sudo' do 63 | it_executes_the_local_command('ansible test_host -m module -a module\ arguments -u test_user --private-key=path/to\ user/key --opt1 --opt2') do 64 | run_as_sudo(false) 65 | ansible_module 'module', 'module arguments', %w[--opt1 --opt2] 66 | end 67 | end 68 | 69 | context 'enable sudo' do 70 | it_executes_the_local_command('ansible test_host -m module -a module\ arguments -u test_user --private-key=path/to\ user/key --sudo --opt1 --opt2') do 71 | run_as_sudo 72 | ansible_module 'module', 'module arguments', %w[--opt1 --opt2] 73 | end 74 | end 75 | 76 | context 'runs as root without sudo' do 77 | before do 78 | allow(current_node).to receive_message_chain(:communicator, :session, :options).and_return({ 79 | user: 'root', 80 | keys: 'path/to user/key' 81 | }) 82 | end 83 | it_executes_the_local_command('ansible test_host -m module -a module\ arguments -u root --private-key=path/to\ user/key --opt1 --opt2') do 84 | ansible_module 'module', 'module arguments', %w[--opt1 --opt2] 85 | end 86 | end 87 | end 88 | 89 | describe 'setting a path to an inventory' do 90 | it_executes_the_local_command(/^ansible test_host .* -i path\/to\\ custom\/hosts .*/) do 91 | set_hostfile_path 'path/to custom/hosts' 92 | ansible_module 'module', 'module arguments', %w[--opt1 --opt2] 93 | end 94 | 95 | describe 'enabling inventory host auto detection' do 96 | context 'no groups specified' do 97 | include_context 'create temp file', 'nodespec_ansible_hosts', '/path/to/inventory', /test_host ansible_ssh_host=test.host ansible_ssh_port=1234/ 98 | 99 | it_executes_the_local_command(/^ansible test_host .* -i \/path\/to\/inventory .*/) do 100 | enable_host_auto_discovery 101 | ansible_module 'module', 'module arguments', %w[--opt1 --opt2] 102 | end 103 | end 104 | 105 | context 'group specified' do 106 | include_context 'create temp file', 'nodespec_ansible_hosts', '/path/to/inventory', <<-eos 107 | [test-group] 108 | test_host ansible_ssh_host=test.host ansible_ssh_port=1234 109 | eos 110 | 111 | it_executes_the_local_command(/^ansible test_host .* -i \/path\/to\/inventory .*/) do 112 | enable_host_auto_discovery('test-group') 113 | ansible_module 'module', 'module arguments', %w[--opt1 --opt2] 114 | end 115 | end 116 | end 117 | end 118 | 119 | describe 'configuring ansible' do 120 | it_executes_the_local_command(/^ANSIBLE_HOST_KEY_CHECKING=False ansible .*/) do 121 | set_host_key_checking(false) 122 | ansible_module 'module', 'module arguments' 123 | end 124 | 125 | it_executes_the_local_command(/^ANSIBLE_CONFIG=\/path\\ to\/config ANSIBLE_HOST_KEY_CHECKING=False ansible .*/) do 126 | set_config_path('/path to/config') 127 | set_host_key_checking(false) 128 | ansible_module 'module', 'module arguments' 129 | end 130 | 131 | describe 'inline custom configuration' do 132 | include_context 'create temp file', 'nodespec_ansible_cfg', '/path/to/cfg', 'test config' 133 | 134 | it_executes_the_local_command(/^ANSIBLE_CONFIG=\/path\/to\/cfg ansible .*/) do 135 | ansible_config('test config') 136 | ansible_module 'module', 'module arguments' 137 | end 138 | end 139 | end 140 | end 141 | end 142 | end 143 | end -------------------------------------------------------------------------------- /spec/provisioning/chef_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/provisioning/chef' 3 | 4 | module NodeSpec 5 | module Provisioning 6 | describe Chef, init_with_current_node: true do 7 | describe 'running chef-apply' do 8 | it_executes_the_node_command('chef-apply --opt1 --opt2 -e include_recipe\ \"main::cron\"') do 9 | chef_apply_execute 'include_recipe "main::cron"', %w[--opt1 --opt2] 10 | end 11 | 12 | it_executes_the_node_command('chef-apply /test\ path/to/recipe --opt1 --opt2') do 13 | chef_apply_recipe '/test path/to/recipe', %w[--opt1 --opt2] 14 | end 15 | end 16 | 17 | describe 'running chef-client' do 18 | context 'specified node path' do 19 | describe 'custom configuration' do 20 | before do 21 | expect(current_node).to receive(:create_file).with('chef_client.rb', "node_path '/path/to/nodes'").ordered.and_return('config/chef_client.rb') 22 | end 23 | 24 | it_executes_the_node_command( 25 | 'chef-client -z --opt1 --opt2 -c config/chef_client.rb -o recipe1,recipe2' 26 | ) do 27 | chef_client_config "node_path '/path/to/nodes'" 28 | chef_client_runlist 'recipe1', 'recipe2', %w[--opt1 --opt2] 29 | end 30 | end 31 | end 32 | 33 | context 'implicit node path' do 34 | before do 35 | expect(current_node).to receive(:create_temp_directory).with('chef_nodes').and_return('/tmp/dir/chef_nodes') 36 | allow(current_node).to receive(:create_file).with('chef_client.rb', /^node_path '\/tmp\/dir\/chef_nodes'$/).and_return('config/chef_client.rb') 37 | end 38 | 39 | it_executes_the_node_command('chef-client -z --opt1 --opt2 -c config/chef_client.rb -o recipe1,recipe2') do 40 | chef_client_runlist 'recipe1', 'recipe2', %w[--opt1 --opt2] 41 | end 42 | 43 | describe 'custom configuration' do 44 | before do 45 | expect(current_node).to receive(:create_file).with('chef_client.rb', /^test config$/).ordered.and_return('config/chef_client.rb') 46 | end 47 | 48 | it_executes_the_node_command( 49 | 'chef-client -z --opt1 --opt2 -c config/chef_client.rb -o recipe1,recipe2' 50 | ) do 51 | chef_client_config 'test config' 52 | chef_client_runlist 'recipe1', 'recipe2', %w[--opt1 --opt2] 53 | end 54 | end 55 | 56 | describe 'custom cookbook path' do 57 | before do 58 | expect(current_node).to receive(:create_file).with('chef_client.rb', /^cookbook_path \['\/var\/chef\/cookbooks','\/var\/chef\/site-cookbooks'\]$/).ordered.and_return('config/chef_client.rb') 59 | end 60 | 61 | it_executes_the_node_command( 62 | 'chef-client -z --opt1 --opt2 -c config/chef_client.rb -o recipe1,recipe2' 63 | ) do 64 | set_cookbook_paths '/var/chef/cookbooks', '/var/chef/site-cookbooks' 65 | 66 | chef_client_runlist 'recipe1', 'recipe2', %w[--opt1 --opt2] 67 | end 68 | end 69 | 70 | describe 'setting custom attributes' do 71 | before do 72 | expect(current_node).to receive(:create_file).with('chef_client_attributes.json', %q[{"test_attributes":{"attr1":"foo","attr2":"bar"}}]).ordered.and_return('config/chef_client_attributes.json') 73 | end 74 | 75 | it_executes_the_node_command( 76 | 'chef-client -z -c config/chef_client.rb -j config/chef_client_attributes.json -o recipe1,recipe2' 77 | ) do 78 | set_attributes test_attributes: {attr1: 'foo', attr2: 'bar'} 79 | 80 | chef_client_runlist 'recipe1', 'recipe2' 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end -------------------------------------------------------------------------------- /spec/provisioning/puppet_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper.rb' 2 | require 'nodespec/provisioning/puppet' 3 | 4 | module NodeSpec 5 | module Provisioning 6 | describe Puppet, init_with_current_node: true do 7 | describe 'executing a puppet inline code' do 8 | it_executes_the_node_command(/^puppet apply\s+ --opt1 --opt2 -e include\\ testmodule::testclass$/) do 9 | puppet_apply_execute 'include testmodule::testclass', %w[--opt1 --opt2] 10 | end 11 | end 12 | 13 | describe 'executing a puppet manifest file' do 14 | it_executes_the_node_command(/^puppet apply .* \/test\/path\\ to\/manifest$/) do 15 | puppet_apply_manifest '/test/path to/manifest' 16 | end 17 | end 18 | 19 | describe 'setting facts' do 20 | it_executes_the_node_command(/^FACTER_foo=bar\\ baz puppet apply .*$/) do 21 | set_facts 'foo' => 'bar baz' 22 | puppet_apply_manifest '/test/path/to/manifest' 23 | end 24 | end 25 | 26 | describe 'setting the module path' do 27 | it_executes_the_node_command(/puppet apply --modulepath \/test\\ path\/1:\/test\\ path\/2 .*/) do 28 | set_modulepaths '/test path/1', '/test path/2' 29 | puppet_apply_execute 'include testmodule::testclass' 30 | end 31 | end 32 | 33 | describe 'setting hiera data' do 34 | before do 35 | expect(current_node).to receive(:create_directory).with('puppet_hieradata').ordered.and_return('config/puppet_hieradata') 36 | expect(current_node).to receive(:create_file).with('puppet_hieradata/common.yaml', "---\ntest: hiera data\n").ordered.and_return('config/puppet_hieradata/common.yaml') 37 | expect(current_node).to receive(:create_file).with('puppet_hiera.yaml', ":backends:\n - yaml\n:yaml:\n :datadir: config/puppet_hieradata\n:hierarchy:\n - common\n").ordered.and_return('config/puppet_hiera.yaml') 38 | end 39 | 40 | it_executes_the_node_command( 41 | 'puppet apply --modulepath /test/module/path --hiera_config config/puppet_hiera.yaml --opts /test/path/to/manifest' 42 | ) do 43 | set_modulepaths '/test/module/path' 44 | set_hieradata('test' => 'hiera data') 45 | 46 | puppet_apply_manifest '/test/path/to/manifest', %w[--opts] 47 | end 48 | end 49 | 50 | describe 'setting hiera config' do 51 | end 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /spec/provisioning/shellscript_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nodespec/provisioning/shellscript' 3 | 4 | module NodeSpec 5 | module Provisioning 6 | describe Shellscript, init_with_current_node: true do 7 | # let() 8 | it_executes_the_node_command('/path/to/test/script.sh') do 9 | execute_file('/path/to/test/script.sh') 10 | end 11 | it_executes_the_node_command("sh -c mkdir\\ /tmp/test_dir'\n'cd\\ /tmp/test_dir'\n'touch\\ test.txt'\n'") do 12 | execute_script <<-EOS 13 | mkdir /tmp/test_dir 14 | cd /tmp/test_dir 15 | touch test.txt 16 | EOS 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /spec/provisioning_spec.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/provisioning' 2 | 3 | module NodeSpec 4 | describe Provisioning do 5 | available_provisioners = { 6 | 'puppet' => Provisioning::Puppet, 7 | 'chef' => Provisioning::Chef, 8 | 'ansible' => Provisioning::Ansible, 9 | 'shellscript' => Provisioning::Shellscript 10 | } 11 | 12 | subject {Object.new.extend Provisioning} 13 | let(:provisioning_block) {Proc.new {}} 14 | 15 | before do 16 | allow(NodeSpec).to receive(:current_node).and_return('current node') 17 | end 18 | 19 | context 'multiple invocations to same provisioner' do 20 | let(:provisioner_instance){double('provisioner instance')} 21 | available_provisioners.each do |name, clazz| 22 | it "instantiate the #{clazz} provisioner once only" do 23 | allow(clazz).to receive(:new).with('current node').once.and_return(provisioner_instance) 24 | expect(provisioner_instance).to receive(:instance_eval).twice 25 | 26 | subject.send("provision_node_with_#{name}", &provisioning_block) 27 | subject.send("provision_node_with_#{name}", &provisioning_block) 28 | end 29 | 30 | it "executes the #{clazz} command" do 31 | allow(clazz).to receive(:new).with('current node').and_return(provisioner_instance) 32 | expect(provisioner_instance).to receive(:instance_eval) do |&b| 33 | expect(b).to be provisioning_block 34 | end 35 | 36 | subject.send("provision_node_with_#{name}", &provisioning_block) 37 | end 38 | end 39 | end 40 | 41 | context "multiple provisioners" do 42 | it 'allows to run multiple provisioners on the same node' do 43 | available_provisioners.each do |name, clazz| 44 | provisioner_instance = double("#{name} provisioner instance") 45 | allow(clazz).to receive(:new).with('current node').once.and_return(provisioner_instance) 46 | expect(provisioner_instance).to receive(:instance_eval) 47 | subject.send("provision_node_with_#{name}", &provisioning_block) 48 | end 49 | end 50 | end 51 | end 52 | end -------------------------------------------------------------------------------- /spec/runtime_gem_loader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'nodespec/runtime_gem_loader' 2 | 3 | module NodeSpec 4 | describe RuntimeGemLoader do 5 | it 'requires the gem successfully and executes the block' do 6 | success = false 7 | RuntimeGemLoader.require_or_fail('rspec') do 8 | success = true 9 | end 10 | expect(success).to be_truthy 11 | end 12 | 13 | it 'fails to require the gem and prints a default error message' do 14 | success = false 15 | expect do 16 | RuntimeGemLoader.require_or_fail('a-gem-that/does-not-exist') do 17 | success = true 18 | end 19 | end.to raise_error(/Consider installing the missing gem\s+gem install 'a-gem-that\/does-not-exist'/) 20 | expect(success).to be_falsy 21 | end 22 | 23 | it 'fails to require the gem and prints a custom error message' do 24 | success = false 25 | expect do 26 | RuntimeGemLoader.require_or_fail('a-gem-that/does-not-exist', 'this is wrong') do 27 | success = true 28 | end 29 | end.to raise_error(/this is wrong\s+gem install 'a-gem-that\/does-not-exist'/) 30 | expect(success).to be_falsy 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | Dir[File.join(File.dirname(__FILE__), 'support/*.rb')].each {|f| require f} 2 | 3 | RSpec.configure do |config| 4 | config.extend NodeSpec::RSpecExtensions 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/backend.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'providing a backend' do |backend_identifier, backend_class| 2 | it "returns a #{backend_identifier} backend" do 3 | expect(subject.backend).to eq(backend_identifier) 4 | end 5 | 6 | it "returns #{backend_class.name} as backend proxy" do 7 | expect(subject.backend_proxy).to be_a(backend_class) 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /spec/support/communication_adapters.rb: -------------------------------------------------------------------------------- 1 | shared_context 'new_ssh_communicator' do |hostname, options| 2 | before do 3 | allow(NodeSpec::CommunicationAdapters::SshCommunicator).to receive(:new).with(hostname, options).and_return('ssh communicator') 4 | end 5 | end 6 | 7 | shared_context 'new_winrm_communicator' do |hostname, options| 8 | before do 9 | allow(NodeSpec::CommunicationAdapters::WinrmCommunicator).to receive(:new).with(hostname, options).and_return('winrm communicator') 10 | end 11 | end 12 | 13 | shared_examples 'new_communicator' do |adapter_clazz, connection| 14 | include_context "new_#{connection}_communicator", 'test.host.name', 'foo' => 'bar' 15 | 16 | it 'returns communicator with the host name from the node name' do 17 | expect(adapter_clazz.communicator_for('test.host.name', 'foo' => 'bar')).to eq("#{connection} communicator") 18 | end 19 | 20 | it 'returns communicator with the host name from the options' do 21 | expect(adapter_clazz.communicator_for('test_node', 'host' => 'test.host.name', 'foo' => 'bar')).to eq("#{connection} communicator") 22 | end 23 | end 24 | 25 | shared_examples 'initializing communicator session' do 26 | it 'returns a session' do 27 | subject.init_session(configuration) 28 | 29 | expect(subject.session).to eq('session') 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/init_with_current_node.rb: -------------------------------------------------------------------------------- 1 | shared_context 'initialize with current node', init_with_current_node: true do 2 | let(:current_node) {double('current node')} 3 | subject {described_class.new(current_node)} 4 | end -------------------------------------------------------------------------------- /spec/support/local_command.rb: -------------------------------------------------------------------------------- 1 | module NodeSpec 2 | module RSpecExtensions 3 | def it_executes_the_local_command(*command_matches, &instructions) 4 | it 'runs the commands' do 5 | command_matches.each do |command_match| 6 | expect(subject).to receive(:run_command).with(command_match) 7 | end 8 | subject.instance_eval(&instructions) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/node_command.rb: -------------------------------------------------------------------------------- 1 | module NodeSpec 2 | module RSpecExtensions 3 | def it_executes_the_node_command(*command_matches, &provisioning_instructions) 4 | it 'runs the commands' do 5 | command_matches.each do |command_match| 6 | expect(current_node).to receive(:execute).with(command_match) 7 | end 8 | subject.instance_eval(&provisioning_instructions) 9 | end 10 | end 11 | end 12 | end 13 | --------------------------------------------------------------------------------