mspectator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in serverspec.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ mspectator
2
+ ==========
3
+
4
+ Use RSpec and MCollective to test your fleet
5
+
6
+ # Goal
7
+
8
+ This project provides a way to test your fleet of servers using RSpec and MCollective.
9
+
10
+ It is similar to the [serverspec](http://serverspec.org) project, but it uses MCollective instead of SSH as a network facility, and it allows to test more than a host at a time.
11
+
12
+ ## Example
13
+
14
+ The matchers allow to test hosts based on filters, using classes and facts. Below is an example:
15
+
16
+ require 'mspectator'
17
+
18
+ describe "apache::server" do
19
+ it { should find_nodes(10).or_less.with_agent('spec') }
20
+ it { should have_certificate.signed }
21
+ it { should pass_puppet_spec }
22
+
23
+ context "when on Debian", :facts => [:operatingsystem => "Debian"] do
24
+ it { should find_nodes(5).or_more }
25
+ it { should have_service('apache2').with(
26
+ :ensure => 'running',
27
+ :enable => 'true'
28
+ )
29
+ }
30
+ it { should have_package('apache2') }
31
+ it { should have_user('www-data') }
32
+ end
33
+ end
34
+
35
+ # Architecture
36
+
37
+ ## Network architecture and libraries
38
+
39
+ The general architecture of the solution is the following:
40
+
41
+ +-------------------------------------+ +-------------------------------------+
42
+ | Client | | Server |
43
+ |-------------------------------------| |-------------------------------------|
44
+ | | | |
45
+ | rspec | | |
46
+ | + | | |
47
+ | | (check_action, *args) | | |
48
+ | v | | |
49
+ | MCollective::RPC#rpcclient | | Serverspec::Backend::Puppet |
50
+ | | | | ^ |
51
+ | + | | | (check_action, *args) |
52
+ | | | | + |
53
+ | +-------------------------------------------> MCollective::RPC::Agent |
54
+ | action, *args | | |
55
+ | | | |
56
+ +-------------------------------------+ +-------------------------------------+
57
+
58
+
59
+ The components required for this architecture are:
60
+
61
+ * [RSpec](http://rspec.info), on the client side;
62
+ * The [serverspec](http://serverspec.org) backends and matchers, on the server side;
63
+ * The [spec MCollective agent](https://github.com/camptocamp/puppet-spec/tree/master/files/mcollective/agent) on the server side;
64
+ * A series of matchers on the client side to describe the hosts being tested.
65
+
66
+
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'apache' do
4
+ it { should find_nodes(100).or_less }
5
+ it { should pass_puppet_spec }
6
+ it { should have_certificate.signed }
7
+
8
+ context 'when on Debian',
9
+ :facts => { :operatingsystem => 'Debian' } do
10
+
11
+ it { should find_nodes(5).with_agent('spec') }
12
+ it { should have_package('apache2.2-common') }
13
+ it { should_not have_package('httpd') }
14
+ it { should have_service('apache2').with(
15
+ :ensure => 'running'
16
+ ) }
17
+ it { should have_file('/etc/apache2/apache2.conf') }
18
+ it { should have_directory('/etc/apache2/conf.d') }
19
+ it { should have_user('www-data') }
20
+ end
21
+
22
+ context 'when using SSL', :classes => ['apache::ssl'] do
23
+ it { should find_nodes(50).or_more }
24
+ it { should have_package('ca-certificates') }
25
+ end
26
+ end
data/lib/mspectator.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'mspectator/matchers'
2
+ require 'mspectator/example'
3
+
4
+ RSpec.configure do |c|
5
+ c.include(MSpectator::ExampleGroup)
6
+ end
@@ -0,0 +1,79 @@
1
+ require 'mcollective'
2
+
3
+ module MSpectator
4
+ module ExampleGroup
5
+ include MCollective::RPC
6
+
7
+ def subject
8
+ @subject ||= self.class.top_level_description
9
+ end
10
+
11
+ def self.included(group)
12
+ group.before(:all) do
13
+ @mc_clients ||= {}
14
+ end
15
+ end
16
+
17
+ def mc_clients
18
+ @mc_clients ||= {}
19
+ end
20
+
21
+ def mc_client(agent)
22
+ unless mc_clients.has_key? agent
23
+ # Can't pass :progress_bar to rpcclient()?
24
+ new = rpcclient(agent)
25
+ new.progress = false
26
+ mc_clients[agent] = new
27
+ end
28
+ mc_clients[agent]
29
+ end
30
+
31
+ def filtered_mc(agent, example)
32
+ mc = mc_client(agent)
33
+ # There is no need to reset only before :group currently
34
+ # so we need to reset before applying filters
35
+ mc.reset
36
+ apply_filters(mc, example)
37
+ end
38
+
39
+ def apply_filters (mc, example)
40
+ classes = example.metadata[:classes] || []
41
+ facts = example.metadata[:facts] || {}
42
+ mc.compound_filter example.example_group.top_level_description
43
+ unless classes.empty? and facts.empty?
44
+ classes.each do |c|
45
+ mc.class_filter c
46
+ end
47
+ facts.each do |f, v|
48
+ mc.fact_filter f, v
49
+ end
50
+ end
51
+ mc
52
+ end
53
+
54
+ def check_spec (example, action, values)
55
+ spec_mc = filtered_mc('spec', example)
56
+ passed = []
57
+ failed = []
58
+ spec_mc.check(:action => action, :values => values).each do |resp|
59
+ if resp[:data][:passed]
60
+ passed << resp[:sender]
61
+ else
62
+ failed << resp[:sender]
63
+ end
64
+ end
65
+ return passed, failed
66
+ end
67
+
68
+ def get_fqdn (example)
69
+ util_mc = self.mc_client('rpcutil')
70
+ util_mc.progress = false
71
+ util_mc = apply_filters util_mc, example
72
+ fqdn = []
73
+ util_mc.get_fact(:fact => 'fqdn').each do |resp|
74
+ fqdn << resp[:data][:value]
75
+ end
76
+ fqdn
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,8 @@
1
+ require 'mspectator/matchers/find_nodes'
2
+ require 'mspectator/matchers/have_certificate'
3
+ require 'mspectator/matchers/have_directory'
4
+ require 'mspectator/matchers/have_file'
5
+ require 'mspectator/matchers/have_package'
6
+ require 'mspectator/matchers/have_service'
7
+ require 'mspectator/matchers/have_user'
8
+ require 'mspectator/matchers/pass_puppet_spec'
@@ -0,0 +1,36 @@
1
+ RSpec::Matchers.define :find_nodes do |number|
2
+ match do
3
+ @agent ||= 'rpcutil'
4
+ mc = filtered_mc(@agent, example)
5
+ @size = mc.discover.size
6
+ if @compare == :or_less
7
+ @size <= number
8
+ elsif @compare == :or_more
9
+ @size >= number
10
+ else
11
+ @size == number
12
+ end
13
+ end
14
+
15
+ chain :or_less do
16
+ @compare = :or_less
17
+ @compare_msg = ' or less'
18
+ end
19
+
20
+ chain :or_more do
21
+ @compare = :or_more
22
+ @compare_msg = ' or more'
23
+ end
24
+
25
+ chain :with_agent do |agent|
26
+ @agent = agent
27
+ end
28
+
29
+ failure_message_for_should do |actual|
30
+ "expected to find #{expected} nodes#{@compare_msg} using agent '#{@agent}', but found #{@size} instead."
31
+ end
32
+
33
+ failure_message_for_should_not do |actual|
34
+ "expected not to find #{expected} nodes#{@compare_msg} using agent '#{@agent}', but found #{@size} instead."
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ RSpec::Matchers.define :have_certificate do
2
+ match do
3
+ discovered = get_fqdn(example)
4
+ puppetca_mc = mc_client('puppetca')
5
+ # TODO: use hash for requests and signed
6
+ # so we can tell where a certificate was found
7
+ requests = []
8
+ signed = []
9
+ puppetca_mc.list.each do |resp|
10
+ requests << resp[:data][:requests]
11
+ signed << resp[:data][:signed]
12
+ end
13
+ requests.flatten!
14
+ signed.flatten!
15
+ @passed = []
16
+ @failed = []
17
+ discovered.each do |c|
18
+ if !@signed and requests.include? c
19
+ @passed << c
20
+ elsif signed.include? c
21
+ @passed << c
22
+ else
23
+ @failed << c
24
+ end
25
+ end
26
+ @failed.empty?
27
+ end
28
+
29
+ failure_message_for_should do |actual|
30
+ "expected that all hosts would have a valid #{@signed_msg}certificate, but found hosts without one: #{@failed.join(', ')}"
31
+ end
32
+
33
+ failure_message_for_should_not do |actual|
34
+ "expected that no hosts would have a valid #{@signed_msg}certificate, but found hosts with one: #{@passed.join(', ')}"
35
+ end
36
+
37
+ chain :signed do
38
+ @signed = true
39
+ @signed_msg = 'signed '
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ RSpec::Matchers.define :have_directory do |directory|
2
+ match do
3
+ @passed, @failed = check_spec(example, 'directory', directory)
4
+ @failed.empty?
5
+ end
6
+
7
+ failure_message_for_should do |actual|
8
+ "expected that all hosts would have the #{expected} directory, but found hosts without it: #{@failed.join(', ')}"
9
+ end
10
+
11
+ failure_message_for_should_not do |actual|
12
+ "expected that no hosts would have the #{expected} directory, but found hosts with it: #{@passed.join(', ')}"
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ RSpec::Matchers.define :have_file do |file|
2
+ match do
3
+ @passed, @failed = check_spec(example, 'file', file)
4
+ @failed.empty?
5
+ end
6
+
7
+ failure_message_for_should do |actual|
8
+ "expected that all hosts would have the #{expected} file, but found hosts without it: #{@failed.join(', ')}"
9
+ end
10
+
11
+ failure_message_for_should_not do |actual|
12
+ "expected that no hosts would have the #{expected} file, but found hosts with it: #{@passed.join(', ')}"
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ RSpec::Matchers.define :have_package do |package|
2
+ match do
3
+ @passed, @failed = check_spec(example, 'installed', package)
4
+ @failed.empty?
5
+ end
6
+
7
+ failure_message_for_should do |actual|
8
+ "expected that all hosts would have the #{expected} package, but found hosts without it: #{@failed.join(', ')}"
9
+ end
10
+
11
+ failure_message_for_should_not do |actual|
12
+ "expected that no hosts would have the #{expected} package, but found hosts with it: #{@passed.join(', ')}"
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ RSpec::Matchers.define :have_service do |service|
2
+ match do
3
+ @filters ||= {}
4
+ # Eventually, stop using serverspec
5
+ # and use a dedicated Puppet library on the server side
6
+ # which will allow to pass @filters simply
7
+ if @filters.fetch(:enable, false) == true
8
+ @action = 'enabled'
9
+ elsif @filters.fetch(:ensure, false) == 'running'
10
+ @action = 'running'
11
+ else
12
+ @action = 'running'
13
+ end
14
+ @passed, @failed = check_spec(example, @action, service)
15
+ @failed.empty?
16
+ end
17
+
18
+ failure_message_for_should do |actual|
19
+ "expected that all hosts would have the #{expected} service #{@action}, but found hosts without it: #{@failed.join(', ')}"
20
+ end
21
+
22
+ failure_message_for_should_not do |actual|
23
+ "expected that no hosts would have the #{expected} service #{@action}, but found hosts with it: #{@passed.join(', ')}"
24
+ end
25
+
26
+ chain :with do |filters|
27
+ @filters = filters
28
+ end
29
+ end
30
+
@@ -0,0 +1,15 @@
1
+ RSpec::Matchers.define :have_user do |user|
2
+ match do
3
+ @passed, @failed = check_spec(example, 'user', user)
4
+ @failed.empty?
5
+ end
6
+
7
+ failure_message_for_should do |actual|
8
+ "expected that all hosts would have the #{expected} user, but found hosts without it: #{@failed.join(', ')}"
9
+ end
10
+
11
+ failure_message_for_should_not do |actual|
12
+ "expected that no hosts would have the #{expected} user, but found hosts with it: #{@passed.join(', ')}"
13
+ end
14
+ end
15
+
@@ -0,0 +1,27 @@
1
+ RSpec::Matchers.define :pass_puppet_spec do
2
+ match do
3
+ spec_mc = filtered_mc('spec', example)
4
+ @passed = []
5
+ @failed = {}
6
+ spec_mc.run.each do |resp|
7
+ if resp[:data][:passed]
8
+ @passed << resp[:sender]
9
+ else
10
+ @failed[resp[:sender]] = resp[:data][:output]
11
+ end
12
+ end
13
+ @failed.empty?
14
+ end
15
+
16
+ failure_message_for_should do |actual|
17
+ text = "expected that all hosts would pass tests, the following didn't:\n"
18
+ @failed.each do |h, o|
19
+ text += "#{h}:\n #{o}"
20
+ end
21
+ text
22
+ end
23
+
24
+ failure_message_for_should_not do |actual|
25
+ "expected that no hosts would pass tests, the following did: #{@passed.join(', ')}"
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module MSpectator
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mspectator/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mspectator"
8
+ spec.version = MSpectator::VERSION
9
+ spec.authors = ["Raphaël Pinson"]
10
+ spec.email = ["raphink@gmail.com"]
11
+ spec.description = %q{Use RSpec and MCollective to test your fleet}
12
+ spec.summary = %q{Use RSpec and MCollective to test your fleet}
13
+ spec.homepage = "https://github.com/raphink/mspectator"
14
+ spec.license = "GPLv3"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "mcollective"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mspectator
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - "Rapha\xC3\xABl Pinson"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2013-04-15 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: mcollective
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: bundler
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ hash: 9
43
+ segments:
44
+ - 1
45
+ - 3
46
+ version: "1.3"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rake
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ description: Use RSpec and MCollective to test your fleet
64
+ email:
65
+ - raphink@gmail.com
66
+ executables: []
67
+
68
+ extensions: []
69
+
70
+ extra_rdoc_files: []
71
+
72
+ files:
73
+ - Gemfile
74
+ - README.md
75
+ - examples/apache_spec.rb
76
+ - lib/mspectator.rb
77
+ - lib/mspectator/example.rb
78
+ - lib/mspectator/matchers.rb
79
+ - lib/mspectator/matchers/find_nodes.rb
80
+ - lib/mspectator/matchers/have_certificate.rb
81
+ - lib/mspectator/matchers/have_directory.rb
82
+ - lib/mspectator/matchers/have_file.rb
83
+ - lib/mspectator/matchers/have_package.rb
84
+ - lib/mspectator/matchers/have_service.rb
85
+ - lib/mspectator/matchers/have_user.rb
86
+ - lib/mspectator/matchers/pass_puppet_spec.rb
87
+ - lib/mspectator/version.rb
88
+ - mspectator.gemspec
89
+ homepage: https://github.com/raphink/mspectator
90
+ licenses:
91
+ - GPLv3
92
+ post_install_message:
93
+ rdoc_options: []
94
+
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ hash: 3
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ hash: 3
112
+ segments:
113
+ - 0
114
+ version: "0"
115
+ requirements: []
116
+
117
+ rubyforge_project:
118
+ rubygems_version: 1.8.24
119
+ signing_key:
120
+ specification_version: 3
121
+ summary: Use RSpec and MCollective to test your fleet
122
+ test_files: []
123
+