chake 0.21 → 0.81.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ackrc +2 -0
- data/.gitignore +22 -0
- data/.gitlab-ci.yml +24 -0
- data/.manifest +65 -0
- data/.rubocop.yml +55 -0
- data/.rubocop_todo.yml +40 -0
- data/ChangeLog.md +37 -0
- data/README.chef.md +70 -0
- data/README.itamae.md +58 -0
- data/README.md +118 -85
- data/README.shell.md +30 -0
- data/Rakefile +36 -10
- data/bin/chake +2 -2
- data/chake.gemspec +16 -16
- data/examples/test/.ssh_config +4 -0
- data/examples/test/Rakefile +1 -1
- data/examples/test/Vagrantfile +6 -0
- data/examples/test/config.rb +4 -4
- data/examples/test/cookbooks/basics/recipes/default.rb +1 -0
- data/examples/test/cookbooks/example/files/default/test +1 -0
- data/examples/test/cookbooks/example/files/{host-homer → host-lemur}/test.asc +0 -0
- data/lib/chake.rb +111 -153
- data/lib/chake/bootstrap/chef/01_installed.sh +4 -0
- data/lib/chake/bootstrap/{01_debian.sh → chef/02_debian.sh} +0 -0
- data/lib/chake/bootstrap/{99_unsupported.sh → chef/99_unsupported.sh} +0 -0
- data/lib/chake/config.rb +2 -7
- data/lib/chake/config_manager.rb +89 -0
- data/lib/chake/config_manager/chef.rb +35 -0
- data/lib/chake/config_manager/itamae.rb +57 -0
- data/lib/chake/config_manager/shell.rb +34 -0
- data/lib/chake/config_manager/skel/chef/Rakefile +1 -0
- data/lib/chake/config_manager/skel/chef/config.rb +4 -0
- data/lib/chake/config_manager/skel/chef/cookbooks/basics/recipes/default.rb +1 -0
- data/lib/chake/config_manager/skel/chef/nodes.yaml +3 -0
- data/lib/chake/config_manager/skel/itamae/Rakefile +1 -0
- data/lib/chake/config_manager/skel/itamae/cookbooks/basics/default.rb +1 -0
- data/lib/chake/config_manager/skel/itamae/nodes.yaml +3 -0
- data/lib/chake/config_manager/skel/itamae/roles/basic.rb +1 -0
- data/lib/chake/config_manager/skel/shell/Rakefile +1 -0
- data/lib/chake/config_manager/skel/shell/nodes.yaml +3 -0
- data/lib/chake/connection.rb +83 -0
- data/lib/chake/{backend → connection}/local.rb +2 -8
- data/lib/chake/{backend → connection}/ssh.rb +6 -14
- data/lib/chake/node.rb +49 -29
- data/lib/chake/readline.rb +6 -10
- data/lib/chake/version.rb +1 -1
- data/lib/chake/wipe.rb +18 -0
- data/man/.gitignore +2 -0
- data/man/Rakefile +28 -14
- data/man/readme2man.sed +5 -5
- data/spec/chake/backend/local_spec.rb +5 -6
- data/spec/chake/backend/ssh_spec.rb +8 -10
- data/spec/chake/backend_spec.rb +1 -2
- data/spec/chake/config_manager/chef_spec.rb +38 -0
- data/spec/chake/config_manager/itamae_spec.rb +87 -0
- data/spec/chake/config_manager/shell_spec.rb +54 -0
- data/spec/chake/config_manager_spec.rb +23 -0
- data/spec/chake/node_spec.rb +38 -15
- data/spec/spec_helper.rb +37 -17
- metadata +65 -39
- data/coverage/assets/0.11.0/application.css +0 -809
- data/coverage/assets/0.11.0/application.js +0 -43679
- data/coverage/assets/0.11.0/colorbox/border.png +0 -0
- data/coverage/assets/0.11.0/colorbox/controls.png +0 -0
- data/coverage/assets/0.11.0/colorbox/loading.gif +0 -0
- data/coverage/assets/0.11.0/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.11.0/favicon_green.png +0 -0
- data/coverage/assets/0.11.0/favicon_red.png +0 -0
- data/coverage/assets/0.11.0/favicon_yellow.png +0 -0
- data/coverage/assets/0.11.0/loading.gif +0 -0
- data/coverage/assets/0.11.0/magnify.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.11.0/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/index.html +0 -4158
- data/lib/chake/backend.rb +0 -80
- data/tags +0 -72
data/lib/chake/node.rb
CHANGED
@@ -2,58 +2,78 @@ require 'uri'
|
|
2
2
|
require 'etc'
|
3
3
|
require 'forwardable'
|
4
4
|
|
5
|
-
require 'chake/
|
5
|
+
require 'chake/connection'
|
6
|
+
require 'chake/config_manager'
|
6
7
|
|
7
8
|
module Chake
|
8
|
-
|
9
9
|
class Node
|
10
|
-
|
11
10
|
extend Forwardable
|
12
11
|
|
13
|
-
attr_reader :hostname
|
14
|
-
|
15
|
-
|
16
|
-
attr_reader :remote_username
|
17
|
-
attr_reader :path
|
18
|
-
attr_reader :data
|
12
|
+
attr_reader :hostname, :port, :username, :remote_username, :data
|
13
|
+
|
14
|
+
attr_accessor :silent
|
19
15
|
|
20
16
|
def self.max_node_name_length
|
21
17
|
@max_node_name_length ||= 0
|
22
18
|
end
|
23
|
-
|
24
|
-
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_writer :max_node_name_length
|
25
22
|
end
|
26
23
|
|
27
24
|
def initialize(hostname, data = {})
|
28
|
-
uri =
|
29
|
-
|
30
|
-
uri = URI.parse("ssh://#{hostname}")
|
31
|
-
end
|
32
|
-
if uri.path && uri.path.empty?
|
33
|
-
uri.path = nil
|
34
|
-
end
|
35
|
-
|
36
|
-
@backend_name = uri.scheme
|
37
|
-
|
25
|
+
uri = parse_uri(hostname)
|
26
|
+
@connection_name = uri.scheme
|
38
27
|
@hostname = uri.host
|
39
28
|
@port = uri.port
|
40
29
|
@username = uri.user || Etc.getpwuid.name
|
41
30
|
@remote_username = uri.user
|
42
|
-
@path = uri.path
|
31
|
+
@path = uri.path
|
43
32
|
@data = data
|
33
|
+
set_max_node_length
|
34
|
+
end
|
35
|
+
|
36
|
+
def connection
|
37
|
+
@connection ||= Chake::Connection.get(@connection_name).new(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
def_delegators :connection, :run, :run_as_root, :run_shell, :rsync, :rsync_dest, :scp, :scp_dest, :skip?
|
44
41
|
|
45
|
-
|
46
|
-
|
42
|
+
def config_manager
|
43
|
+
@config_manager ||= Chake::ConfigManager.get(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
def_delegators :config_manager, :converge, :apply, :path, :bootstrap_steps, :needs_upload?
|
47
|
+
|
48
|
+
def path
|
49
|
+
@path ||= config_manager.path
|
50
|
+
end
|
51
|
+
|
52
|
+
def log(msg)
|
53
|
+
return if silent
|
54
|
+
|
55
|
+
puts("%#{Node.max_node_name_length}<host>s: %<msg>s\n" % { host: hostname, msg: msg })
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def parse_uri(hostname)
|
61
|
+
uri = URI.parse(hostname)
|
62
|
+
if incomplete_uri(uri)
|
63
|
+
uri = URI.parse("ssh://#{hostname}")
|
47
64
|
end
|
65
|
+
uri.path = nil if uri.path.empty?
|
66
|
+
uri
|
48
67
|
end
|
49
68
|
|
50
|
-
def
|
51
|
-
|
69
|
+
def incomplete_uri(uri)
|
70
|
+
!uri.host && ((!uri.scheme && uri.path) || (uri.scheme && uri.opaque))
|
52
71
|
end
|
53
72
|
|
54
|
-
|
73
|
+
def set_max_node_length
|
74
|
+
return if @hostname.length <= self.class.max_node_name_length
|
55
75
|
|
76
|
+
self.class.max_node_name_length = @hostname.length
|
77
|
+
end
|
56
78
|
end
|
57
|
-
|
58
79
|
end
|
59
|
-
|
data/lib/chake/readline.rb
CHANGED
@@ -4,11 +4,8 @@ require 'readline'
|
|
4
4
|
require 'chake/tmpdir'
|
5
5
|
|
6
6
|
module Chake
|
7
|
-
|
8
7
|
class Readline
|
9
|
-
|
10
8
|
class << self
|
11
|
-
|
12
9
|
def history_file
|
13
10
|
raise NotImplementedError
|
14
11
|
end
|
@@ -22,12 +19,14 @@ module Chake
|
|
22
19
|
end
|
23
20
|
|
24
21
|
def init
|
25
|
-
return
|
22
|
+
return unless File.exist?(history_file)
|
23
|
+
|
26
24
|
@history = File.readlines(history_file).map(&:strip)
|
27
25
|
end
|
28
26
|
|
29
27
|
def finish
|
30
28
|
return if !File.writable?(File.dirname(history_file)) || history.empty?
|
29
|
+
|
31
30
|
File.open(history_file, 'w') do |f|
|
32
31
|
history.last(500).each do |line|
|
33
32
|
f.puts(line)
|
@@ -41,18 +40,16 @@ module Chake
|
|
41
40
|
::Readline::HISTORY.push(cmd)
|
42
41
|
end
|
43
42
|
input = ::Readline.readline(prompt)
|
44
|
-
if input && input.strip != '' && input != @last
|
45
|
-
history.push(input)
|
46
|
-
end
|
43
|
+
history.push(input) if input && input.strip != '' && input != @last
|
47
44
|
input
|
48
45
|
end
|
49
|
-
|
50
46
|
end
|
51
47
|
|
52
48
|
class Commands < Readline
|
53
49
|
def self.history_file
|
54
50
|
File.join(Chake.tmpdir, '.commands_history')
|
55
51
|
end
|
52
|
+
|
56
53
|
def self.prompt
|
57
54
|
'$ '
|
58
55
|
end
|
@@ -62,13 +59,12 @@ module Chake
|
|
62
59
|
def self.history_file
|
63
60
|
File.join(Chake.tmpdir, '.recipes_history')
|
64
61
|
end
|
62
|
+
|
65
63
|
def self.prompt
|
66
64
|
'> '
|
67
65
|
end
|
68
66
|
end
|
69
|
-
|
70
67
|
end
|
71
|
-
|
72
68
|
end
|
73
69
|
|
74
70
|
Chake::Readline.constants.each do |subclass|
|
data/lib/chake/version.rb
CHANGED
data/lib/chake/wipe.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Chake
|
4
|
+
class Wipe
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
if system('which', 'wipe', out: '/dev/null', err: :out)
|
8
|
+
def wipe(file)
|
9
|
+
system('wipe', '-rfs', file)
|
10
|
+
end
|
11
|
+
else
|
12
|
+
warn 'W: please install "wipe" program for secure deletion, falling back to unlink(2)'
|
13
|
+
def wipe(file)
|
14
|
+
File.unlink(file)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/man/.gitignore
ADDED
data/man/Rakefile
CHANGED
@@ -1,22 +1,36 @@
|
|
1
|
-
|
1
|
+
MAIN_MANPAGE = 'man/chake.1'.freeze
|
2
|
+
OTHER_MANPAGES = %w[
|
3
|
+
man/chake-chef.7
|
4
|
+
man/chake-itamae.7
|
5
|
+
man/chake-shell.7
|
6
|
+
].freeze
|
7
|
+
MANPAGES = [MAIN_MANPAGE] + OTHER_MANPAGES
|
2
8
|
|
3
|
-
|
4
|
-
|
5
|
-
end
|
9
|
+
task default: :man
|
10
|
+
task man: MANPAGES
|
6
11
|
|
7
|
-
|
8
|
-
|
12
|
+
MANPAGES.each do |man|
|
13
|
+
source = "README#{man.pathmap('%n').sub(/^chake/, '').sub('-', '.')}.md"
|
14
|
+
file man => [source, 'man/readme2man.sed'] do
|
15
|
+
sh "sed -f man/readme2man.sed #{source} > #{man}.ronn || (rm -f #{man}.ronn; false)"
|
16
|
+
sh "ronn --roff #{man}.ronn"
|
17
|
+
sh "rm -f #{man}.ronn"
|
18
|
+
sh 'sed', '-i', '-e', 's/\\\\\'/\'/g', man
|
19
|
+
end
|
9
20
|
end
|
10
21
|
|
11
|
-
task :
|
12
|
-
prefix = ENV['PREFIX'] || File.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
sh 'install', '-
|
22
|
+
task install: MANPAGES do
|
23
|
+
prefix = ENV['PREFIX'] || File.exist?('debian/rules') && '/usr' || '/usr/local'
|
24
|
+
man1 = File.join(*[ENV['DESTDIR'], prefix, 'share/man/man1'].compact)
|
25
|
+
man7 = File.join(*[ENV['DESTDIR'], prefix, 'share/man/man7'].compact)
|
26
|
+
target = { '.1' => man1, '.7' => man7 }
|
27
|
+
sh 'install', '-d', '-m', '0755', man1
|
28
|
+
sh 'install', '-d', '-m', '0755', man7
|
29
|
+
MANPAGES.each do |m|
|
30
|
+
sh 'install', '-m', '0644', m, target[m.pathmap('%x')]
|
31
|
+
end
|
17
32
|
end
|
18
33
|
|
19
34
|
task :clean do
|
20
|
-
rm_f
|
21
|
-
rm_f 'man/chake.adoc'
|
35
|
+
rm_f MANPAGES
|
22
36
|
end
|
data/man/readme2man.sed
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Chake::
|
3
|
+
describe Chake::Connection::Local do
|
4
|
+
include_examples 'Chake::Connection', Chake::Connection::Local
|
4
5
|
|
5
|
-
|
6
|
+
let(:node) { Chake::Node.new('local://myusername@myhost/srv/chake') }
|
6
7
|
|
7
|
-
|
8
|
+
it('runs commands with sh -c') { expect(connection.command_runner).to eq(['sh', '-c']) }
|
8
9
|
|
9
|
-
it('
|
10
|
-
|
11
|
-
it('rsyncs locally') { expect(backend.rsync_dest).to eq('/srv/chef/') }
|
10
|
+
it('rsyncs locally') { expect(connection.rsync_dest).to eq('/srv/chake/') }
|
12
11
|
|
13
12
|
it('skips if hostname is not the local hostname') do
|
14
13
|
allow(Socket).to receive(:gethostname).and_return('otherhost')
|
@@ -1,14 +1,13 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Chake::
|
3
|
+
describe Chake::Connection::Ssh do
|
4
|
+
include_examples 'Chake::Connection', Chake::Connection::Ssh
|
4
5
|
|
5
|
-
|
6
|
+
let(:node) { Chake::Node.new('ssh://myuser@myhost/srv/chake') }
|
6
7
|
|
7
|
-
|
8
|
+
it('runs commands with ssh') { expect(connection.command_runner).to eq(['ssh', 'myuser@myhost']) }
|
8
9
|
|
9
|
-
it('
|
10
|
-
|
11
|
-
it('rsyncs over ssh') { expect(backend.rsync_dest).to eq('myuser@myhost:/srv/chef/') }
|
10
|
+
it('rsyncs over ssh') { expect(connection.rsync_dest).to eq('myuser@myhost:/srv/chake/') }
|
12
11
|
|
13
12
|
it 'uses no remote username if none was passed' do
|
14
13
|
node = Chake::Node.new('theserver')
|
@@ -24,14 +23,13 @@ describe Chake::Backend::Ssh do
|
|
24
23
|
context 'with a custom port' do
|
25
24
|
let(:node) { Chake::Node.new('ssh://myhost:2222') }
|
26
25
|
it 'uses port with ssh' do
|
27
|
-
expect(
|
26
|
+
expect(connection.command_runner).to eq(['ssh', '-p', '2222', 'myhost'])
|
28
27
|
end
|
29
28
|
it 'uses port with scp' do
|
30
|
-
expect(
|
29
|
+
expect(connection.scp).to eq(['scp', '-P', '2222'])
|
31
30
|
end
|
32
31
|
it 'uses port with rsync' do
|
33
|
-
expect(
|
32
|
+
expect(connection.send(:rsync_ssh)).to eq(['-e', 'ssh -p 2222'])
|
34
33
|
end
|
35
34
|
end
|
36
|
-
|
37
35
|
end
|
data/spec/chake/backend_spec.rb
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
require 'chake/
|
2
|
-
|
1
|
+
require 'chake/connection'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'chake/node'
|
2
|
+
require 'chake/config_manager/chef'
|
3
|
+
|
4
|
+
describe Chake::ConfigManager::Chef do
|
5
|
+
let(:node) do
|
6
|
+
Chake::Node.new('foobar')
|
7
|
+
end
|
8
|
+
|
9
|
+
subject do
|
10
|
+
Chake::ConfigManager::Chef.new(node)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'provides a name' do
|
14
|
+
expect(subject.name).to eq('chef')
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'calls chef-solo on converge' do
|
18
|
+
expect(subject).to receive(:logging).and_return('-l debug')
|
19
|
+
expect(node).to receive(:run_as_root).with(%r{chef-solo -c #{node.path}/config.rb -l debug -j #{node.path}/#{Chake.tmpdir}/foobar.json})
|
20
|
+
subject.converge
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'calls chef-solo on apply' do
|
24
|
+
expect(subject).to receive(:logging).and_return('-l debug')
|
25
|
+
expect(node).to receive(:run_as_root).with(%r{chef-solo -c #{node.path}/config.rb -l debug -j #{node.path}/#{Chake.tmpdir}/foobar.json --override-runlist recipe\[myrecipe\]})
|
26
|
+
subject.apply('myrecipe')
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'logging' do
|
30
|
+
it 'logs when requested' do
|
31
|
+
expect(subject.send(:logging)).to eq('')
|
32
|
+
end
|
33
|
+
it 'only show fatal errrrs when requested' do
|
34
|
+
node.silent = true
|
35
|
+
expect(subject.send(:logging)).to eq('-l fatal')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'chake/node'
|
3
|
+
require 'chake/config_manager/itamae'
|
4
|
+
|
5
|
+
describe Chake::ConfigManager::Itamae do
|
6
|
+
let(:hostname) { 'foobar' }
|
7
|
+
let(:node) do
|
8
|
+
Chake::Node.new(hostname).tap do |n|
|
9
|
+
n.silent = true
|
10
|
+
n.data['itamae'] = ['foo.rb', 'bar.rb']
|
11
|
+
end
|
12
|
+
end
|
13
|
+
let(:cfg) { Chake::ConfigManager::Itamae.new(node) }
|
14
|
+
let(:output) { StringIO.new("line1\nline2\n") }
|
15
|
+
|
16
|
+
it 'does not require uploading' do
|
17
|
+
expect(cfg.needs_upload?).to eq(false)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'calls itamae when converging' do
|
21
|
+
expect(IO).to receive(:popen).with(
|
22
|
+
array_including('itamae', 'foo.rb', 'bar.rb'),
|
23
|
+
'r',
|
24
|
+
err: %i[child out]
|
25
|
+
).and_return(output)
|
26
|
+
cfg.converge
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'calls itamae when applying' do
|
30
|
+
expect(IO).to receive(:popen).with(
|
31
|
+
array_including('itamae', 'foobarbaz.rb'),
|
32
|
+
'r',
|
33
|
+
err: %i[child out]
|
34
|
+
).and_return(output)
|
35
|
+
cfg.apply('foobarbaz.rb')
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'for ssh hosts' do
|
39
|
+
let(:hostname) { 'ssh://theusernanme@thehostname' }
|
40
|
+
it 'calls itamae ssh subcommand' do
|
41
|
+
expect(IO).to receive(:popen).with(
|
42
|
+
array_including('itamae', 'ssh', '--host=thehostname', '--user=theusernanme'),
|
43
|
+
anything,
|
44
|
+
err: anything
|
45
|
+
).and_return(output)
|
46
|
+
cfg.converge
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'for local hosts' do
|
51
|
+
let(:hostname) { 'local://localhostname' }
|
52
|
+
it 'calls itamae with local subcommand' do
|
53
|
+
expect(IO).to receive(:popen).with(
|
54
|
+
array_including('itamae', 'local', /--node-json=.*/, 'foo.rb', 'bar.rb'),
|
55
|
+
anything,
|
56
|
+
err: anything
|
57
|
+
).and_return(output)
|
58
|
+
cfg.converge
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'throws an error for unsupported connection' do
|
63
|
+
allow(node).to receive(:connection).and_return(Object.new)
|
64
|
+
expect(-> { cfg.converge }).to raise_error(NotImplementedError)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'handles silent mode' do
|
68
|
+
expect(IO).to receive(:popen).with(
|
69
|
+
array_including('--log-level=warn'),
|
70
|
+
anything,
|
71
|
+
err: anything
|
72
|
+
).and_return(output)
|
73
|
+
cfg.converge
|
74
|
+
end
|
75
|
+
|
76
|
+
RSpec::Matchers.define_negated_matcher :array_excluding, :include
|
77
|
+
|
78
|
+
it 'handles non-silent mode' do
|
79
|
+
node.silent = false
|
80
|
+
expect(IO).to receive(:popen).with(
|
81
|
+
array_excluding('--log-level=warn'),
|
82
|
+
anything,
|
83
|
+
err: anything
|
84
|
+
).and_return(output)
|
85
|
+
silence($stdout) { cfg.converge }
|
86
|
+
end
|
87
|
+
end
|