Skip to content

Commit

Permalink
Merge pull request #244 from serverspec/windows
Browse files Browse the repository at this point in the history
[WIP]Support Windows
  • Loading branch information
mizzy committed Sep 24, 2013
2 parents 27babae + 619deac commit e0079bd
Show file tree
Hide file tree
Showing 37 changed files with 1,160 additions and 72 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.swp
.bundle
.rvmrc
.versions.conf
.config
.yardoc
.rspec
Expand All @@ -21,3 +22,4 @@ test/version_tmp
tmp
Vagrantfile
vendor/
.DS_Store
14 changes: 6 additions & 8 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ require 'rspec/core/rake_task'
task :spec => 'spec:all'

namespace :spec do
oses = %w( darwin debian gentoo redhat solaris solaris10 solaris11 smartos )
oses = %w( darwin debian gentoo redhat solaris solaris10 solaris11 smartos windows)

task :all => [ oses.map {|os| "spec:#{os}" }, :helpers, :exec, :ssh ].flatten
task :all => [ oses.map {|os| "spec:#{os}" }, :helpers, :exec, :ssh, :cmd, :winrm, :powershell ].flatten

oses.each do |os|
RSpec::Core::RakeTask.new(os.to_sym) do |t|
Expand All @@ -18,11 +18,9 @@ namespace :spec do
t.pattern = "spec/helpers/*_spec.rb"
end

RSpec::Core::RakeTask.new(:exec) do |t|
t.pattern = "spec/backend/exec/*_spec.rb"
end

RSpec::Core::RakeTask.new(:ssh) do |t|
t.pattern = "spec/backend/ssh/*_spec.rb"
[:exec, :ssh, :cmd, :winrm, :powershell].each do |backend|
RSpec::Core::RakeTask.new(backend) do |t|
t.pattern = "spec/backend/#{backend.to_s}/*_spec.rb"
end
end
end
88 changes: 88 additions & 0 deletions WindowsSupport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
## Windows support

Serverspec is now providing a limited support for Microsoft Windows.

If you want to test Windows based machines you need to set the target host's OS explicitly in your `spec/spec_helper.rb`

For local testing (equivalent to the Exec option in Linux/Unix systems) simply do:

```ruby
require 'serverspec'

include Serverspec::Helper::Cmd
include Serverspec::Helper::Windows

```

For remote testing you have to configure Windows Remote Management in order to communicate to the target host:

```ruby
require 'serverspec'
require 'winrm'

include Serverspec::Helper::WinRM
include Serverspec::Helper::Windows

RSpec.configure do |c|
user = <username>
pass = <password>
endpoint = "http://<hostname>:5985/wsman"

c.winrm = ::WinRM::WinRMWebService.new(endpoint, :ssl, :user => user, :pass => pass, :basic_auth_only => true)
c.winrm.set_timeout 300 # 5 minutes max timeout for any operation
end
```

For different authentication mechanisms check the Microsoft WinRM documentation and verify the ones that are supported by [WinRb/WinRM](https://github.com/WinRb/WinRM)


###RSpec Examples for windows target hosts
```ruby
describe file('c:/windows') do
it { should be_directory }
it { should be_readable }
it { should_not be_writable.by('Everyone') }
end

describe file('c:/temp/test.txt') do
it { should be_file }
it { should contain "some text" }
end

describe package('Adobe AIR') do
it { should be_installed}
end

describe service('DNS Client') do
it { should be_enabled }
it { should be_running }
end

describe port(139) do
it { should be_listening }
end

describe user('some.admin') do
it { should exist }
it { should belong_to_group('Administrators')}
end

describe group('Guests') do
it { should exist }
end

describe group('MYDOMAIN\Domain Users') do
it { should exist }
end

describe windows_registry_key('HKEY_USERS\S-1-5-21-1319311448-2088773778-316617838-32407\Test MyKey') do
it { should exist }
it { should have_property('string value') }
it { should have_property('binary value', :type_binary) }
it { should have_property('dword value', :type_dword) }
it { should have_value('test default data') }
it { should have_property_value('multistring value', :type_multistring, "test\nmulti\nstring\ndata") }
it { should have_property_value('qword value', :type_qword, 'adff32') }
it { should have_property_value('binary value', :type_binary, 'dfa0f066') }
end
```
27 changes: 24 additions & 3 deletions bin/serverspec-init
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,32 @@ Serverspec::Setup.run

__END__
require 'serverspec'
<% if @os_type == 'UN*X' -%>
require 'pathname'
<% end -%>
<% if @backend_type == 'Ssh' -%>
require 'net/ssh'
<% end -%>
<% if @backend_type == 'WinRM' -%>
require 'winrm'
<% end -%>

include Serverspec::Helper::<%= @backend_type %>
<% if @os_type == 'UN*X' -%>
include Serverspec::Helper::DetectOS
<% else -%>
include Serverspec::Helper::Windows
<% end -%>

<% if @os_type == 'UN*X' -%>
RSpec.configure do |c|
if ENV['ASK_SUDO_PASSWORD']
require 'highline/import'
c.sudo_password = ask("Enter sudo password: ") { |q| q.echo = false }
else
c.sudo_password = ENV['SUDO_PASSWORD']
end
<% if @backend_type == 'Ssh' -%>
<%- if @backend_type == 'Ssh' -%>
c.before :all do
block = self.class.metadata[:example_group_block]
if RUBY_VERSION.start_with?('1.8')
Expand All @@ -37,7 +47,7 @@ RSpec.configure do |c|
c.host = host
options = Net::SSH::Config.for(c.host)
user = options[:user] || Etc.getlogin
<% if @vagrant -%>
<%- if @vagrant -%>
vagrant_up = `vagrant up #{@hostname}`
config = `vagrant ssh-config #{@hostname}`
if config != ''
Expand All @@ -53,9 +63,20 @@ RSpec.configure do |c|
end
end
end
<% end -%>
<%- end -%>
c.ssh = Net::SSH.start(c.host, user, options)
end
end
<%- end -%>
end
<% end -%>
<% if @backend_type == 'WinRM'-%>
RSpec.configure do |c|
user = <username>
pass = <password>
endpoint = "http://<hostname>:5985/wsman"

c.winrm = ::WinRM::WinRMWebService.new(endpoint, :ssl, :user => user, :pass => pass, :basic_auth_only => true)
c.winrm.set_timeout 300 # 5 minutes max timeout for any operation
end
<% end -%>
3 changes: 3 additions & 0 deletions lib/serverspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require 'serverspec/commands/solaris11'
require 'serverspec/commands/smartos'
require 'serverspec/commands/darwin'
require 'serverspec/commands/windows'
require 'serverspec/configuration'
require 'rspec/core/formatters/base_formatter'

Expand All @@ -39,10 +40,12 @@ def configuration
c.include(Serverspec::Helper::Solaris11, :os => :solaris11)
c.include(Serverspec::Helper::SmartOS, :os => :smartos)
c.include(Serverspec::Helper::Darwin, :os => :darwin)
c.include(Serverspec::Helper::Windows, :os => :windows)
c.add_setting :os, :default => nil
c.add_setting :host, :default => nil
c.add_setting :ssh, :default => nil
c.add_setting :sudo_password, :default => nil
c.add_setting :winrm, :default => nil
Serverspec.configuration.defaults.each { |k, v| c.add_setting k, :default => v }
c.before :each do
backend.set_example(example)
Expand Down
5 changes: 5 additions & 0 deletions lib/serverspec/backend.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
require 'serverspec/backend/base'
require 'serverspec/backend/ssh'
require 'serverspec/backend/exec'
require 'serverspec/backend/powershell/script_helper'
require 'serverspec/backend/powershell/command'
require 'serverspec/backend/cmd'
require 'serverspec/backend/winrm'
31 changes: 31 additions & 0 deletions lib/serverspec/backend/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'singleton'

module Serverspec
module Backend
class Base
include Singleton

def set_commands(c)
@commands = c
end

def set_example(e)
@example = e
end

def commands
@commands
end

def check_zero(cmd, *args)
ret = run_command(commands.send(cmd, *args))
ret[:exit_status] == 0
end

# Default action is to call check_zero with args
def method_missing(meth, *args, &block)
check_zero(meth, *args)
end
end
end
end
35 changes: 35 additions & 0 deletions lib/serverspec/backend/cmd.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'open3'

module Serverspec
module Backend
class Cmd < Base
include PowerShell::ScriptHelper

def run_command(cmd, opts={})
script = create_script(cmd)
result = execute_script script

if @example
@example.metadata[:command] = script
@example.metadata[:stdout] = result[:stdout] + result[:stderr]
end
{ :stdout => result[:stdout], :stderr => result[:stderr],
:exit_status => result[:status], :exit_signal => nil }
end

def execute_script script
ps_script = %Q{powershell -encodedCommand #{encode_script(script)}}
if Open3.respond_to? :capture3
stdout, stderr, status = Open3.capture3(ps_script)
# powershell still exits with 0 even if there are syntax errors, although it spits the error out into stderr
# so we have to resort to return an error exit code if there is anything in the standard error
status = 1 if status == 0 and !stderr.empty?
{ :stdout => stdout, :stderr => stderr, :status => status }
else
stdout = `#{ps_script} 2>&1`
{ :stdout => stdout, :stderr => nil, :status => $? }
end
end
end
end
end
25 changes: 1 addition & 24 deletions lib/serverspec/backend/exec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,7 @@

module Serverspec
module Backend
class Exec
include Singleton

def set_commands(c)
@commands = c
end

def set_example(e)
@example = e
end

def commands
@commands
end
class Exec < Base

def run_command(cmd, opts={})
cmd = build_command(cmd)
Expand Down Expand Up @@ -52,16 +39,6 @@ def add_pre_command(cmd)
cmd
end

def check_zero(cmd, *args)
ret = run_command(commands.send(cmd, *args))
ret[:exit_status] == 0
end

# Default action is to call check_zero with args
def method_missing(meth, *args, &block)
check_zero(meth, *args)
end

def check_running(process)
ret = run_command(commands.check_running(process))
if ret[:exit_status] == 1 || ret[:stdout] =~ /stopped/
Expand Down
36 changes: 36 additions & 0 deletions lib/serverspec/backend/powershell/command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Serverspec
module Backend
module PowerShell
class Command
attr_reader :import_functions, :script
def initialize &block
@import_functions = []
@script = ""
instance_eval &block if block_given?
end

def using *functions
functions.each { |f| import_functions << f }
end

def exec code
@script = code
end

def convert_regexp(target)
case target
when Regexp
target.source
else
target.to_s.gsub '/', ''
end
end

def get_identity id
raise "You must provide a specific Windows user/group" if id =~ /(owner|group|others)/
identity = id || 'Everyone'
end
end
end
end
end
Loading

0 comments on commit e0079bd

Please sign in to comment.