serverspec 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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'