serverspec 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/Rakefile +6 -8
  4. data/WindowsSupport.md +88 -0
  5. data/bin/serverspec-init +75 -0
  6. data/lib/serverspec/backend/base.rb +31 -0
  7. data/lib/serverspec/backend/cmd.rb +35 -0
  8. data/lib/serverspec/backend/exec.rb +1 -24
  9. data/lib/serverspec/backend/powershell/command.rb +36 -0
  10. data/lib/serverspec/backend/powershell/script_helper.rb +69 -0
  11. data/lib/serverspec/backend/powershell/support/check_file_access_rules.ps1 +12 -0
  12. data/lib/serverspec/backend/powershell/support/crop_text.ps1 +11 -0
  13. data/lib/serverspec/backend/powershell/support/find_group.ps1 +8 -0
  14. data/lib/serverspec/backend/powershell/support/find_installed_application.ps1 +7 -0
  15. data/lib/serverspec/backend/powershell/support/find_service.ps1 +5 -0
  16. data/lib/serverspec/backend/powershell/support/find_user.ps1 +8 -0
  17. data/lib/serverspec/backend/powershell/support/find_usergroup.ps1 +9 -0
  18. data/lib/serverspec/backend/powershell/support/is_port_listening.ps1 +13 -0
  19. data/lib/serverspec/backend/winrm.rb +26 -0
  20. data/lib/serverspec/backend.rb +5 -0
  21. data/lib/serverspec/commands/windows.rb +211 -0
  22. data/lib/serverspec/helper/cmd.rb +15 -0
  23. data/lib/serverspec/helper/type.rb +1 -1
  24. data/lib/serverspec/helper/windows.rb +9 -0
  25. data/lib/serverspec/helper/winrm.rb +15 -0
  26. data/lib/serverspec/helper.rb +3 -0
  27. data/lib/serverspec/setup.rb +59 -83
  28. data/lib/serverspec/type/windows_registry_key.rb +21 -0
  29. data/lib/serverspec/version.rb +1 -1
  30. data/lib/serverspec.rb +3 -0
  31. data/spec/backend/cmd/configuration_spec.rb +9 -0
  32. data/spec/backend/powershell/script_helper_spec.rb +77 -0
  33. data/spec/backend/winrm/configuration_spec.rb +9 -0
  34. data/spec/spec_helper.rb +18 -26
  35. data/spec/support/powershell_command_runner.rb +52 -0
  36. data/spec/windows/file_spec.rb +161 -0
  37. data/spec/windows/group_spec.rb +29 -0
  38. data/spec/windows/port_spec.rb +31 -0
  39. data/spec/windows/user_spec.rb +44 -0
  40. metadata +37 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ee32cc231c6c0c9b020ecea8e4d14b0001c94725
4
- data.tar.gz: 57eda3e3a98b88e1b73bb58eb4e88fe672a6cec1
3
+ metadata.gz: af5097bb467c14c4f8d0f22747b6775ecf5d598a
4
+ data.tar.gz: fee035ec0e675c79cf74ef7367da9d2e028a7dbb
5
5
  SHA512:
6
- metadata.gz: f5544d360701ab7a9ab655512291c77e6aed57d7ac5f1921433fe3d2d6408b942107d69a17484e6d5c1d8799c807a47a2b676edfcc4d4e2afb5ee443680b6c1a
7
- data.tar.gz: 3cbbdbb771612ac3bca87a6c4f78d0396151553118ebaef9faa17fb5c1cd571828200e0cf063110543283dd5297d2e1be3c99c168d1ec3d8c996515bc58508cb
6
+ metadata.gz: ba213503e568d2c6bcb5a6f814f436508880546bf90bd43c39e5a944b7d6ada8ce9adbb36889e3024f3dafacb43106a382d7d89cb97eba9217b67694578c8295
7
+ data.tar.gz: c68841e54c3aa39773d997e330e612ddd74b619a9a93328461a362c326b26e1efea60e3f6d2582c52ab84f8a4ad97931941355c412a3a2baa82b5f51403025d4
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  *.swp
4
4
  .bundle
5
5
  .rvmrc
6
+ .versions.conf
6
7
  .config
7
8
  .yardoc
8
9
  .rspec
@@ -21,3 +22,4 @@ test/version_tmp
21
22
  tmp
22
23
  Vagrantfile
23
24
  vendor/
25
+ .DS_Store
data/Rakefile CHANGED
@@ -4,9 +4,9 @@ require 'rspec/core/rake_task'
4
4
  task :spec => 'spec:all'
5
5
 
6
6
  namespace :spec do
7
- oses = %w( darwin debian gentoo redhat solaris solaris10 solaris11 smartos )
7
+ oses = %w( darwin debian gentoo redhat solaris solaris10 solaris11 smartos windows)
8
8
 
9
- task :all => [ oses.map {|os| "spec:#{os}" }, :helpers, :exec, :ssh ].flatten
9
+ task :all => [ oses.map {|os| "spec:#{os}" }, :helpers, :exec, :ssh, :cmd, :winrm, :powershell ].flatten
10
10
 
11
11
  oses.each do |os|
12
12
  RSpec::Core::RakeTask.new(os.to_sym) do |t|
@@ -18,11 +18,9 @@ namespace :spec do
18
18
  t.pattern = "spec/helpers/*_spec.rb"
19
19
  end
20
20
 
21
- RSpec::Core::RakeTask.new(:exec) do |t|
22
- t.pattern = "spec/backend/exec/*_spec.rb"
23
- end
24
-
25
- RSpec::Core::RakeTask.new(:ssh) do |t|
26
- t.pattern = "spec/backend/ssh/*_spec.rb"
21
+ [:exec, :ssh, :cmd, :winrm, :powershell].each do |backend|
22
+ RSpec::Core::RakeTask.new(backend) do |t|
23
+ t.pattern = "spec/backend/#{backend.to_s}/*_spec.rb"
24
+ end
27
25
  end
28
26
  end
data/WindowsSupport.md ADDED
@@ -0,0 +1,88 @@
1
+ ## Windows support
2
+
3
+ Serverspec is now providing a limited support for Microsoft Windows.
4
+
5
+ If you want to test Windows based machines you need to set the target host's OS explicitly in your `spec/spec_helper.rb`
6
+
7
+ For local testing (equivalent to the Exec option in Linux/Unix systems) simply do:
8
+
9
+ ```ruby
10
+ require 'serverspec'
11
+
12
+ include Serverspec::Helper::Cmd
13
+ include Serverspec::Helper::Windows
14
+
15
+ ```
16
+
17
+ For remote testing you have to configure Windows Remote Management in order to communicate to the target host:
18
+
19
+ ```ruby
20
+ require 'serverspec'
21
+ require 'winrm'
22
+
23
+ include Serverspec::Helper::WinRM
24
+ include Serverspec::Helper::Windows
25
+
26
+ RSpec.configure do |c|
27
+ user = <username>
28
+ pass = <password>
29
+ endpoint = "http://<hostname>:5985/wsman"
30
+
31
+ c.winrm = ::WinRM::WinRMWebService.new(endpoint, :ssl, :user => user, :pass => pass, :basic_auth_only => true)
32
+ c.winrm.set_timeout 300 # 5 minutes max timeout for any operation
33
+ end
34
+ ```
35
+
36
+ For different authentication mechanisms check the Microsoft WinRM documentation and verify the ones that are supported by [WinRb/WinRM](https://github.com/WinRb/WinRM)
37
+
38
+
39
+ ###RSpec Examples for windows target hosts
40
+ ```ruby
41
+ describe file('c:/windows') do
42
+ it { should be_directory }
43
+ it { should be_readable }
44
+ it { should_not be_writable.by('Everyone') }
45
+ end
46
+
47
+ describe file('c:/temp/test.txt') do
48
+ it { should be_file }
49
+ it { should contain "some text" }
50
+ end
51
+
52
+ describe package('Adobe AIR') do
53
+ it { should be_installed}
54
+ end
55
+
56
+ describe service('DNS Client') do
57
+ it { should be_enabled }
58
+ it { should be_running }
59
+ end
60
+
61
+ describe port(139) do
62
+ it { should be_listening }
63
+ end
64
+
65
+ describe user('some.admin') do
66
+ it { should exist }
67
+ it { should belong_to_group('Administrators')}
68
+ end
69
+
70
+ describe group('Guests') do
71
+ it { should exist }
72
+ end
73
+
74
+ describe group('MYDOMAIN\Domain Users') do
75
+ it { should exist }
76
+ end
77
+
78
+ describe windows_registry_key('HKEY_USERS\S-1-5-21-1319311448-2088773778-316617838-32407\Test MyKey') do
79
+ it { should exist }
80
+ it { should have_property('string value') }
81
+ it { should have_property('binary value', :type_binary) }
82
+ it { should have_property('dword value', :type_dword) }
83
+ it { should have_value('test default data') }
84
+ it { should have_property_value('multistring value', :type_multistring, "test\nmulti\nstring\ndata") }
85
+ it { should have_property_value('qword value', :type_qword, 'adff32') }
86
+ it { should have_property_value('binary value', :type_binary, 'dfa0f066') }
87
+ end
88
+ ```
data/bin/serverspec-init CHANGED
@@ -5,3 +5,78 @@ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
5
5
  require 'serverspec'
6
6
 
7
7
  Serverspec::Setup.run
8
+
9
+ __END__
10
+ require 'serverspec'
11
+ <% if @os_type == 'UN*X' -%>
12
+ require 'pathname'
13
+ <% end -%>
14
+ <% if @backend_type == 'Ssh' -%>
15
+ require 'net/ssh'
16
+ <% end -%>
17
+ <% if @backend_type == 'WinRM' -%>
18
+ require 'winrm'
19
+ <% end -%>
20
+
21
+ include Serverspec::Helper::<%= @backend_type %>
22
+ <% if @os_type == 'UN*X' -%>
23
+ include Serverspec::Helper::DetectOS
24
+ <% else -%>
25
+ include Serverspec::Helper::Windows
26
+ <% end -%>
27
+
28
+ <% if @os_type == 'UN*X' -%>
29
+ RSpec.configure do |c|
30
+ if ENV['ASK_SUDO_PASSWORD']
31
+ require 'highline/import'
32
+ c.sudo_password = ask("Enter sudo password: ") { |q| q.echo = false }
33
+ else
34
+ c.sudo_password = ENV['SUDO_PASSWORD']
35
+ end
36
+ <%- if @backend_type == 'Ssh' -%>
37
+ c.before :all do
38
+ block = self.class.metadata[:example_group_block]
39
+ if RUBY_VERSION.start_with?('1.8')
40
+ file = block.to_s.match(/.*@(.*):[0-9]+>/)[1]
41
+ else
42
+ file = block.source_location.first
43
+ end
44
+ host = File.basename(Pathname.new(file).dirname)
45
+ if c.host != host
46
+ c.ssh.close if c.ssh
47
+ c.host = host
48
+ options = Net::SSH::Config.for(c.host)
49
+ user = options[:user] || Etc.getlogin
50
+ <%- if @vagrant -%>
51
+ vagrant_up = `vagrant up #{@hostname}`
52
+ config = `vagrant ssh-config #{@hostname}`
53
+ if config != ''
54
+ config.each_line do |line|
55
+ if match = /HostName (.*)/.match(line)
56
+ c.host = match[1]
57
+ elsif match = /User (.*)/.match(line)
58
+ user = match[1]
59
+ elsif match = /IdentityFile (.*)/.match(line)
60
+ options[:keys] = [match[1].gsub(/\"/,'')]
61
+ elsif match = /Port (.*)/.match(line)
62
+ options[:port] = match[1]
63
+ end
64
+ end
65
+ end
66
+ <%- end -%>
67
+ c.ssh = Net::SSH.start(c.host, user, options)
68
+ end
69
+ end
70
+ <%- end -%>
71
+ end
72
+ <% end -%>
73
+ <% if @backend_type == 'WinRM'-%>
74
+ RSpec.configure do |c|
75
+ user = <username>
76
+ pass = <password>
77
+ endpoint = "http://<hostname>:5985/wsman"
78
+
79
+ c.winrm = ::WinRM::WinRMWebService.new(endpoint, :ssl, :user => user, :pass => pass, :basic_auth_only => true)
80
+ c.winrm.set_timeout 300 # 5 minutes max timeout for any operation
81
+ end
82
+ <% end -%>
@@ -0,0 +1,31 @@
1
+ require 'singleton'
2
+
3
+ module Serverspec
4
+ module Backend
5
+ class Base
6
+ include Singleton
7
+
8
+ def set_commands(c)
9
+ @commands = c
10
+ end
11
+
12
+ def set_example(e)
13
+ @example = e
14
+ end
15
+
16
+ def commands
17
+ @commands
18
+ end
19
+
20
+ def check_zero(cmd, *args)
21
+ ret = run_command(commands.send(cmd, *args))
22
+ ret[:exit_status] == 0
23
+ end
24
+
25
+ # Default action is to call check_zero with args
26
+ def method_missing(meth, *args, &block)
27
+ check_zero(meth, *args)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ require 'open3'
2
+
3
+ module Serverspec
4
+ module Backend
5
+ class Cmd < Base
6
+ include PowerShell::ScriptHelper
7
+
8
+ def run_command(cmd, opts={})
9
+ script = create_script(cmd)
10
+ result = execute_script script
11
+
12
+ if @example
13
+ @example.metadata[:command] = script
14
+ @example.metadata[:stdout] = result[:stdout] + result[:stderr]
15
+ end
16
+ { :stdout => result[:stdout], :stderr => result[:stderr],
17
+ :exit_status => result[:status], :exit_signal => nil }
18
+ end
19
+
20
+ def execute_script script
21
+ ps_script = %Q{powershell -encodedCommand #{encode_script(script)}}
22
+ if Open3.respond_to? :capture3
23
+ stdout, stderr, status = Open3.capture3(ps_script)
24
+ # powershell still exits with 0 even if there are syntax errors, although it spits the error out into stderr
25
+ # so we have to resort to return an error exit code if there is anything in the standard error
26
+ status = 1 if status == 0 and !stderr.empty?
27
+ { :stdout => stdout, :stderr => stderr, :status => status }
28
+ else
29
+ stdout = `#{ps_script} 2>&1`
30
+ { :stdout => stdout, :stderr => nil, :status => $? }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -2,20 +2,7 @@ require 'singleton'
2
2
 
3
3
  module Serverspec
4
4
  module Backend
5
- class Exec
6
- include Singleton
7
-
8
- def set_commands(c)
9
- @commands = c
10
- end
11
-
12
- def set_example(e)
13
- @example = e
14
- end
15
-
16
- def commands
17
- @commands
18
- end
5
+ class Exec < Base
19
6
 
20
7
  def run_command(cmd, opts={})
21
8
  cmd = build_command(cmd)
@@ -52,16 +39,6 @@ module Serverspec
52
39
  cmd
53
40
  end
54
41
 
55
- def check_zero(cmd, *args)
56
- ret = run_command(commands.send(cmd, *args))
57
- ret[:exit_status] == 0
58
- end
59
-
60
- # Default action is to call check_zero with args
61
- def method_missing(meth, *args, &block)
62
- check_zero(meth, *args)
63
- end
64
-
65
42
  def check_running(process)
66
43
  ret = run_command(commands.check_running(process))
67
44
  if ret[:exit_status] == 1 || ret[:stdout] =~ /stopped/
@@ -0,0 +1,36 @@
1
+ module Serverspec
2
+ module Backend
3
+ module PowerShell
4
+ class Command
5
+ attr_reader :import_functions, :script
6
+ def initialize &block
7
+ @import_functions = []
8
+ @script = ""
9
+ instance_eval &block if block_given?
10
+ end
11
+
12
+ def using *functions
13
+ functions.each { |f| import_functions << f }
14
+ end
15
+
16
+ def exec code
17
+ @script = code
18
+ end
19
+
20
+ def convert_regexp(target)
21
+ case target
22
+ when Regexp
23
+ target.source
24
+ else
25
+ target.to_s.gsub '/', ''
26
+ end
27
+ end
28
+
29
+ def get_identity id
30
+ raise "You must provide a specific Windows user/group" if id =~ /(owner|group|others)/
31
+ identity = id || 'Everyone'
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,69 @@
1
+ require 'base64'
2
+
3
+ module Serverspec
4
+ module Backend
5
+ module PowerShell
6
+ module ScriptHelper
7
+ def build_command(cmd)
8
+ path = Serverspec.configuration.path || RSpec.configuration.path
9
+ if path
10
+ cmd.strip!
11
+ cmd =
12
+ <<-EOF
13
+ $env:path = "#{path};$env:path"
14
+ #{cmd}
15
+ EOF
16
+ end
17
+ cmd
18
+ end
19
+
20
+ def add_pre_command(cmd)
21
+ path = Serverspec.configuration.path || RSpec.configuration.path
22
+ if Serverspec.configuration.pre_command
23
+ cmd.strip!
24
+ cmd =
25
+ <<-EOF
26
+ if (#{Serverspec.configuration.pre_command})
27
+ {
28
+ #{cmd}
29
+ }
30
+ EOF
31
+ cmd = "$env:path = \"#{path};$env:path\"\n#{cmd}" if path
32
+ end
33
+ cmd
34
+ end
35
+
36
+ def encode_script script
37
+ script_text = script.chars.to_a.join("\x00").chomp
38
+ script_text << "\x00" unless script_text[-1].eql? "\x00"
39
+ if script_text.respond_to?(:encode)
40
+ script_text = script_text.encode('ASCII-8BIT')
41
+ end
42
+ if Base64.respond_to?(:strict_encode64)
43
+ Base64.strict_encode64(script_text)
44
+ else
45
+ [ script_text ].pack("m").strip
46
+ end
47
+ end
48
+
49
+ def create_script command
50
+ script = build_command(command.script)
51
+ script = add_pre_command(script)
52
+ ps_functions = command.import_functions.map { |f| File.read(File.join(File.dirname(__FILE__), 'support', f)) }
53
+ <<-EOF
54
+ $exitCode = 1
55
+ try {
56
+ #{ps_functions.join("\n")}
57
+ $success = (#{script})
58
+ if ($success -is [Boolean] -and $success) { $exitCode = 0 }
59
+ } catch {
60
+ Write-Output $_.Exception.Message
61
+ }
62
+ Write-Output "Exiting with code: $exitCode"
63
+ exit $exitCode
64
+ EOF
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,12 @@
1
+ function CheckFileAccessRules
2
+ {
3
+ param($path, $identity, $rules)
4
+
5
+ $result = $false
6
+ $accessRules = (Get-Acl $path).access | Where-Object {$_.AccessControlType -eq 'Allow' -and $_.IdentityReference -eq $identity }
7
+ if ($accessRules) {
8
+ $match = $accessRules.FileSystemRights.ToString() -Split (', ') | ?{$rules -contains $_}
9
+ $result = $match -ne $null -or $match.length -gt 0
10
+ }
11
+ $result
12
+ }
@@ -0,0 +1,11 @@
1
+ function CropText
2
+ {
3
+ param($text, $fromPattern, $toPattern)
4
+
5
+ $from, $to = ([regex]::matches($text, $fromPattern)), ([regex]::matches($text, $toPattern))
6
+ if ($from.count -gt 0 -and $to.count -gt 0) {
7
+ $text.substring($from[0].index, $to[0].index + $to[0].length - $from[0].index)
8
+ } else {
9
+ ""
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ function FindGroup
2
+ {
3
+ param($groupName, $domain)
4
+ if ($domain -eq $null) {$selectionCriteria = " and LocalAccount = true"}
5
+ else {$selectionCriteria = " and Domain = '$domain'"}
6
+
7
+ Get-WmiObject Win32_Group -filter "Name = '$groupName' $selectionCriteria"
8
+ }
@@ -0,0 +1,7 @@
1
+ function FindInstalledApplication
2
+ {
3
+ param($appName, $appVersion)
4
+ $selectionCriteria = "(Name like '$appName' or PackageName like '$appName') and InstallState = 5"
5
+ if ($appVersion -ne $null) { $selectionCriteria += " and version = '$appVersion'"}
6
+ Get-WmiObject Win32_Product -filter $selectionCriteria
7
+ }
@@ -0,0 +1,5 @@
1
+ function FindService
2
+ {
3
+ param($name)
4
+ Get-WmiObject Win32_Service | Where-Object {$_.serviceName -eq $name -or $_.displayName -eq $name}
5
+ }
@@ -0,0 +1,8 @@
1
+ function FindUser
2
+ {
3
+ param($userName, $domain)
4
+ if ($domain -eq $null) {$selectionCriteria = " and LocalAccount = true"}
5
+ else {$selectionCriteria = " and Domain = '$domain'"}
6
+
7
+ Get-WmiObject Win32_UserAccount -filter "Name = '$userName' $selectionCriteria"
8
+ }
@@ -0,0 +1,9 @@
1
+ function FindUserGroup
2
+ {
3
+ param($userName, $groupName, $userDomain, $groupDomain)
4
+ $user = FindUser -userName $userName -domain $userDomain
5
+ $group = FindGroup -groupName $groupName -domain $groupDomain
6
+ if ($user -and $group) {
7
+ Get-WmiObject Win32_GroupUser -filter ("GroupComponent = 'Win32_Group.Domain=`"" + $group.domain + "`",Name=`"" + $group.name + "`"' and PartComponent = 'Win32_UserAccount.Domain=`"" + $user.domain + "`",Name=`"" + $user.name + "`"'")
8
+ }
9
+ }
@@ -0,0 +1,13 @@
1
+ function IsPortListening
2
+ {
3
+ param($portNumber, $protocol)
4
+ $netstatOutput = netstat -an | Out-String
5
+ $networkIPs = (Get-WmiObject Win32_NetworkAdapterConfiguration | ? {$_.IPEnabled}) | %{ $_.IPAddress[0] }
6
+ foreach ($ipaddress in $networkIPs)
7
+ {
8
+ $matchExpression = ("$ipaddress" + ":" + $portNumber)
9
+ if ($protocol) { $matchExpression = ($protocol.toUpper() + "\s+$matchExpression") }
10
+ if ($netstatOutput -match $matchExpression) { return $true }
11
+ }
12
+ $false
13
+ }
@@ -0,0 +1,26 @@
1
+ module Serverspec
2
+ module Backend
3
+ class WinRM < Base
4
+ include PowerShell::ScriptHelper
5
+
6
+ def run_command(cmd, opts={})
7
+ script = create_script(cmd)
8
+ winrm = RSpec.configuration.winrm
9
+
10
+ result = winrm.powershell(script)
11
+ stdout, stderr = [:stdout, :stderr].map do |s|
12
+ result[:data].select {|item| item.key? s}.map {|item| item[s]}.join
13
+ end
14
+ result[:exitcode] = 1 if result[:exitcode] == 0 and !stderr.empty?
15
+
16
+ if @example
17
+ @example.metadata[:command] = script
18
+ @example.metadata[:stdout] = stdout + stderr
19
+ end
20
+
21
+ { :stdout => stdout, :stderr => stderr,
22
+ :exit_status => result[:exitcode], :exit_signal => nil }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,2 +1,7 @@
1
+ require 'serverspec/backend/base'
1
2
  require 'serverspec/backend/ssh'
2
3
  require 'serverspec/backend/exec'
4
+ require 'serverspec/backend/powershell/script_helper'
5
+ require 'serverspec/backend/powershell/command'
6
+ require 'serverspec/backend/cmd'
7
+ require 'serverspec/backend/winrm'