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 +4 -0
- data/README.md +66 -0
- data/examples/apache_spec.rb +26 -0
- data/lib/mspectator.rb +6 -0
- data/lib/mspectator/example.rb +79 -0
- data/lib/mspectator/matchers.rb +8 -0
- data/lib/mspectator/matchers/find_nodes.rb +36 -0
- data/lib/mspectator/matchers/have_certificate.rb +41 -0
- data/lib/mspectator/matchers/have_directory.rb +14 -0
- data/lib/mspectator/matchers/have_file.rb +14 -0
- data/lib/mspectator/matchers/have_package.rb +14 -0
- data/lib/mspectator/matchers/have_service.rb +30 -0
- data/lib/mspectator/matchers/have_user.rb +15 -0
- data/lib/mspectator/matchers/pass_puppet_spec.rb +27 -0
- data/lib/mspectator/version.rb +3 -0
- data/mspectator.gemspec +24 -0
- metadata +123 -0
data/Gemfile
ADDED
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,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
|
data/mspectator.gemspec
ADDED
@@ -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
|
+
|