mspectator 0.1.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.
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
+