beaker 0.0.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.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.simplecov +14 -0
- data/DOCUMENTING.md +167 -0
- data/Gemfile +3 -0
- data/LICENSE +17 -0
- data/README.md +332 -0
- data/Rakefile +121 -0
- data/beaker.gemspec +42 -0
- data/beaker.rb +10 -0
- data/bin/beaker +9 -0
- data/lib/beaker.rb +36 -0
- data/lib/beaker/answers.rb +29 -0
- data/lib/beaker/answers/version28.rb +104 -0
- data/lib/beaker/answers/version30.rb +194 -0
- data/lib/beaker/cli.rb +113 -0
- data/lib/beaker/command.rb +241 -0
- data/lib/beaker/command_factory.rb +21 -0
- data/lib/beaker/dsl.rb +85 -0
- data/lib/beaker/dsl/assertions.rb +87 -0
- data/lib/beaker/dsl/helpers.rb +625 -0
- data/lib/beaker/dsl/install_utils.rb +299 -0
- data/lib/beaker/dsl/outcomes.rb +99 -0
- data/lib/beaker/dsl/roles.rb +97 -0
- data/lib/beaker/dsl/structure.rb +63 -0
- data/lib/beaker/dsl/wrappers.rb +100 -0
- data/lib/beaker/host.rb +193 -0
- data/lib/beaker/host/aix.rb +15 -0
- data/lib/beaker/host/aix/file.rb +16 -0
- data/lib/beaker/host/aix/group.rb +35 -0
- data/lib/beaker/host/aix/user.rb +32 -0
- data/lib/beaker/host/unix.rb +54 -0
- data/lib/beaker/host/unix/exec.rb +15 -0
- data/lib/beaker/host/unix/file.rb +16 -0
- data/lib/beaker/host/unix/group.rb +40 -0
- data/lib/beaker/host/unix/pkg.rb +22 -0
- data/lib/beaker/host/unix/user.rb +32 -0
- data/lib/beaker/host/windows.rb +44 -0
- data/lib/beaker/host/windows/exec.rb +18 -0
- data/lib/beaker/host/windows/file.rb +15 -0
- data/lib/beaker/host/windows/group.rb +36 -0
- data/lib/beaker/host/windows/pkg.rb +26 -0
- data/lib/beaker/host/windows/user.rb +32 -0
- data/lib/beaker/hypervisor.rb +37 -0
- data/lib/beaker/hypervisor/aixer.rb +52 -0
- data/lib/beaker/hypervisor/blimper.rb +123 -0
- data/lib/beaker/hypervisor/fusion.rb +56 -0
- data/lib/beaker/hypervisor/solaris.rb +65 -0
- data/lib/beaker/hypervisor/vagrant.rb +118 -0
- data/lib/beaker/hypervisor/vcloud.rb +175 -0
- data/lib/beaker/hypervisor/vsphere.rb +80 -0
- data/lib/beaker/hypervisor/vsphere_helper.rb +200 -0
- data/lib/beaker/logger.rb +167 -0
- data/lib/beaker/network_manager.rb +73 -0
- data/lib/beaker/options_parsing.rb +323 -0
- data/lib/beaker/result.rb +55 -0
- data/lib/beaker/shared.rb +15 -0
- data/lib/beaker/shared/error_handler.rb +17 -0
- data/lib/beaker/shared/host_handler.rb +46 -0
- data/lib/beaker/shared/repetition.rb +28 -0
- data/lib/beaker/ssh_connection.rb +198 -0
- data/lib/beaker/test_case.rb +225 -0
- data/lib/beaker/test_config.rb +148 -0
- data/lib/beaker/test_suite.rb +288 -0
- data/lib/beaker/utils.rb +7 -0
- data/lib/beaker/utils/ntp_control.rb +42 -0
- data/lib/beaker/utils/repo_control.rb +92 -0
- data/lib/beaker/utils/setup_helper.rb +77 -0
- data/lib/beaker/utils/validator.rb +27 -0
- data/spec/beaker/command_spec.rb +94 -0
- data/spec/beaker/dsl/assertions_spec.rb +104 -0
- data/spec/beaker/dsl/helpers_spec.rb +230 -0
- data/spec/beaker/dsl/install_utils_spec.rb +70 -0
- data/spec/beaker/dsl/outcomes_spec.rb +43 -0
- data/spec/beaker/dsl/roles_spec.rb +86 -0
- data/spec/beaker/dsl/structure_spec.rb +60 -0
- data/spec/beaker/dsl/wrappers_spec.rb +52 -0
- data/spec/beaker/host_spec.rb +95 -0
- data/spec/beaker/logger_spec.rb +117 -0
- data/spec/beaker/options_parsing_spec.rb +37 -0
- data/spec/beaker/puppet_command_spec.rb +128 -0
- data/spec/beaker/ssh_connection_spec.rb +39 -0
- data/spec/beaker/test_case_spec.rb +6 -0
- data/spec/beaker/test_suite_spec.rb +44 -0
- data/spec/mocks_and_helpers.rb +34 -0
- data/spec/spec_helper.rb +15 -0
- metadata +359 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module Beaker
|
2
|
+
module Shared
|
3
|
+
module Repetition
|
4
|
+
|
5
|
+
def repeat_for seconds, &block
|
6
|
+
timeout = Time.now + seconds
|
7
|
+
done = false
|
8
|
+
until done or timeout < Time.now do
|
9
|
+
done = block.call
|
10
|
+
end
|
11
|
+
return done
|
12
|
+
end
|
13
|
+
|
14
|
+
def repeat_fibonacci_style_for attempts, &block
|
15
|
+
done = false
|
16
|
+
attempt = 1
|
17
|
+
last_wait, wait = 0, 1
|
18
|
+
while not done and attempt <= attempts do
|
19
|
+
done = block.call
|
20
|
+
sleep wait
|
21
|
+
last_wait, wait = wait, last_wait + wait
|
22
|
+
end
|
23
|
+
return done
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'timeout'
|
3
|
+
require 'net/scp'
|
4
|
+
|
5
|
+
module Beaker
|
6
|
+
class SshConnection
|
7
|
+
|
8
|
+
RETRYABLE_EXCEPTIONS = [
|
9
|
+
SocketError,
|
10
|
+
Timeout::Error,
|
11
|
+
Errno::ETIMEDOUT,
|
12
|
+
Errno::EHOSTDOWN,
|
13
|
+
Errno::EHOSTUNREACH,
|
14
|
+
Errno::ECONNREFUSED,
|
15
|
+
Errno::ECONNRESET,
|
16
|
+
Errno::ENETUNREACH,
|
17
|
+
Net::SSH::Disconnect
|
18
|
+
]
|
19
|
+
|
20
|
+
def initialize hostname, user = nil, options = {}
|
21
|
+
@hostname = hostname
|
22
|
+
@user = user
|
23
|
+
@options = options
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.connect hostname, user = 'root', options = {}
|
27
|
+
connection = new hostname, user, options
|
28
|
+
connection.connect
|
29
|
+
connection
|
30
|
+
end
|
31
|
+
|
32
|
+
def connect
|
33
|
+
try = 1
|
34
|
+
last_wait = 0
|
35
|
+
wait = 1
|
36
|
+
@ssh ||= begin
|
37
|
+
Net::SSH.start(@hostname, @user, @options)
|
38
|
+
rescue *RETRYABLE_EXCEPTIONS => e
|
39
|
+
if try <= 11
|
40
|
+
puts "Try #{try} -- Host #{@hostname} unreachable: #{e.message}"
|
41
|
+
puts "Trying again in #{wait} seconds"
|
42
|
+
sleep wait
|
43
|
+
(last_wait, wait) = wait, last_wait + wait
|
44
|
+
try += 1
|
45
|
+
retry
|
46
|
+
else
|
47
|
+
# why is the logger not passed into this class?
|
48
|
+
puts "Failed to connect to #{@hostname}"
|
49
|
+
raise
|
50
|
+
end
|
51
|
+
rescue Net::SSH::AuthenticationFailed => e
|
52
|
+
raise "Unable to authenticate to #{@hostname} with user #{e.message.inspect}"
|
53
|
+
end
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def close
|
58
|
+
begin
|
59
|
+
@ssh.close if @ssh
|
60
|
+
rescue
|
61
|
+
@ssh.shutdown!
|
62
|
+
end
|
63
|
+
@ssh = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def try_to_execute command, options = {}, stdout_callback = nil,
|
67
|
+
stderr_callback = stdout_callback
|
68
|
+
|
69
|
+
result = Result.new(@hostname, command)
|
70
|
+
# why are we getting to this point on a dry run anyways?
|
71
|
+
# also... the host creates connections through the class method,
|
72
|
+
# which automatically connects, so you can't do a dry run unless you also
|
73
|
+
# can connect to your hosts?
|
74
|
+
return result if options[:dry_run]
|
75
|
+
|
76
|
+
@ssh.open_channel do |channel|
|
77
|
+
request_terminal_for( channel, command ) if options[:pty]
|
78
|
+
|
79
|
+
channel.exec(command) do |terminal, success|
|
80
|
+
abort "FAILED: to execute command on a new channel on #{@hostname}" unless success
|
81
|
+
register_stdout_for terminal, result, stdout_callback
|
82
|
+
register_stderr_for terminal, result, stderr_callback
|
83
|
+
register_exit_code_for terminal, result
|
84
|
+
|
85
|
+
process_stdin_for( terminal, options[:stdin] ) if options[:stdin]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Process SSH activity until we stop doing that - which is when our
|
90
|
+
# channel is finished with...
|
91
|
+
@ssh.loop
|
92
|
+
|
93
|
+
result.finalize!
|
94
|
+
result
|
95
|
+
end
|
96
|
+
|
97
|
+
def execute command, options = {}, stdout_callback = nil,
|
98
|
+
stderr_callback = stdout_callback
|
99
|
+
attempt = true
|
100
|
+
begin
|
101
|
+
result = try_to_execute(command, options, stdout_callback, stderr_callback)
|
102
|
+
rescue *RETRYABLE_EXCEPTIONS => e
|
103
|
+
if attempt
|
104
|
+
attempt = false
|
105
|
+
puts "Command execution failed, attempting to reconnect to #{@hostname}"
|
106
|
+
close
|
107
|
+
connect
|
108
|
+
retry
|
109
|
+
else
|
110
|
+
raise
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
result
|
115
|
+
end
|
116
|
+
|
117
|
+
def request_terminal_for channel, command
|
118
|
+
channel.request_pty do |ch, success|
|
119
|
+
if success
|
120
|
+
puts "Allocated a PTY on #{@hostname} for #{command.inspect}"
|
121
|
+
else
|
122
|
+
abort "FAILED: could not allocate a pty when requested on " +
|
123
|
+
"#{@hostname} for #{command.inspect}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def register_stdout_for channel, output, callback = nil
|
129
|
+
channel.on_data do |ch, data|
|
130
|
+
callback[data] if callback
|
131
|
+
output.stdout << data
|
132
|
+
output.output << data
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def register_stderr_for channel, output, callback = nil
|
137
|
+
channel.on_extended_data do |ch, type, data|
|
138
|
+
if type == 1
|
139
|
+
callback[data] if callback
|
140
|
+
output.stderr << data
|
141
|
+
output.output << data
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def register_exit_code_for channel, output
|
147
|
+
channel.on_request("exit-status") do |ch, data|
|
148
|
+
output.exit_code = data.read_long
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def process_stdin_for channel, stdin
|
153
|
+
# queue stdin data, force it to packets, and signal eof: this
|
154
|
+
# triggers action in many remote commands, notably including
|
155
|
+
# 'puppet apply'. It must be sent at some point before the rest
|
156
|
+
# of the action.
|
157
|
+
channel.send_data stdin.to_s
|
158
|
+
channel.process
|
159
|
+
channel.eof!
|
160
|
+
end
|
161
|
+
|
162
|
+
def scp_to source, target, options = {}, dry_run = false
|
163
|
+
return if dry_run
|
164
|
+
|
165
|
+
options[:recursive]=File.directory?(source) if options[:recursive].nil?
|
166
|
+
|
167
|
+
@ssh.scp.upload! source, target, options
|
168
|
+
|
169
|
+
result = Result.new(@hostname, [source, target])
|
170
|
+
|
171
|
+
# Setting these values allows reporting via result.log(test_name)
|
172
|
+
result.stdout = "SCP'ed file #{source} to #{@hostname}:#{target}"
|
173
|
+
|
174
|
+
# Net::Scp always returns 0, so just set the return code to 0.
|
175
|
+
result.exit_code = 0
|
176
|
+
|
177
|
+
return result
|
178
|
+
end
|
179
|
+
|
180
|
+
def scp_from source, target, options = {}, dry_run = false
|
181
|
+
return if dry_run
|
182
|
+
|
183
|
+
options[:recursive] = true if options[:recursive].nil?
|
184
|
+
|
185
|
+
@ssh.scp.download! source, target, options
|
186
|
+
|
187
|
+
result = Result.new(@hostname, [source, target])
|
188
|
+
|
189
|
+
# Setting these values allows reporting via result.log(test_name)
|
190
|
+
result.stdout = "SCP'ed file #{@hostname}:#{source} to #{target}"
|
191
|
+
|
192
|
+
# Net::Scp always returns 0, so just set the return code to 0.
|
193
|
+
result.exit_code = 0
|
194
|
+
|
195
|
+
result
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
%w( host answers dsl ).each do |lib|
|
2
|
+
begin
|
3
|
+
require "beaker/#{lib}"
|
4
|
+
rescue LoadError
|
5
|
+
require File.expand_path(File.join(File.dirname(__FILE__), lib))
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'tempfile'
|
10
|
+
require 'benchmark'
|
11
|
+
require 'stringio'
|
12
|
+
require 'rbconfig'
|
13
|
+
require 'test/unit'
|
14
|
+
|
15
|
+
module Beaker
|
16
|
+
# This class represents a single test case. A test case is necessarily
|
17
|
+
# contained all in one file though may have multiple dependent examples.
|
18
|
+
# They are executed in order (save for any teardown procs registered
|
19
|
+
# through {Beaker::DSL::Structure#teardown}) and once completed
|
20
|
+
# the status of the TestCase is saved. Instance readers/accessors provide
|
21
|
+
# the test case access to various details of the environment and suite
|
22
|
+
# the test case is running within.
|
23
|
+
#
|
24
|
+
# See {Beaker::DSL} for more information about writing tests
|
25
|
+
# using the DSL.
|
26
|
+
class TestCase
|
27
|
+
include Beaker::DSL
|
28
|
+
|
29
|
+
rb_config_class = defined?(RbConfig) ? RbConfig : Config
|
30
|
+
if rb_config_class::CONFIG['MAJOR'].to_i == 1 &&
|
31
|
+
rb_config_class::CONFIG['MINOR'].to_i == 8 then
|
32
|
+
Test::Unit.run = true
|
33
|
+
# The Exception raised by Ruby's STDLIB's test framework (Ruby 1.8)
|
34
|
+
TEST_EXCEPTION_CLASS = Test::Unit::AssertionFailedError
|
35
|
+
else
|
36
|
+
# The Exception raised by Ruby's STDLIB's test framework (Ruby 1.9)
|
37
|
+
TEST_EXCEPTION_CLASS = ::MiniTest::Assertion
|
38
|
+
end
|
39
|
+
|
40
|
+
# Necessary for implementing {Beaker::DSL::Helpers#confine}.
|
41
|
+
# Assumed to be an array of valid {Beaker::Host} objects for
|
42
|
+
# this test case.
|
43
|
+
attr_accessor :hosts
|
44
|
+
|
45
|
+
# Necessary for many methods in {Beaker::DSL}. Assumed to be
|
46
|
+
# an instance of {Beaker::Logger}.
|
47
|
+
attr_accessor :logger
|
48
|
+
|
49
|
+
# A Hash of 'product name' => 'version installed', only set when
|
50
|
+
# products are installed via git or PE install steps. See the 'git' or
|
51
|
+
# 'pe' directories within 'ROOT/setup' for examples.
|
52
|
+
attr_reader :version
|
53
|
+
|
54
|
+
# A Hash of values taken from host config file.
|
55
|
+
attr_reader :config
|
56
|
+
|
57
|
+
# Parsed command line options.
|
58
|
+
attr_reader :options
|
59
|
+
|
60
|
+
# The path to the file which contains this test case.
|
61
|
+
attr_reader :path
|
62
|
+
|
63
|
+
# I don't know why this is here
|
64
|
+
attr_reader :fail_flag
|
65
|
+
|
66
|
+
# The user that is running this tests home directory, needed by 'net/ssh'.
|
67
|
+
attr_reader :usr_home
|
68
|
+
|
69
|
+
# A Symbol denoting the status of this test (:fail, :pending,
|
70
|
+
# :skipped, :pass).
|
71
|
+
attr_reader :test_status
|
72
|
+
|
73
|
+
# The exception that may have stopped this test's execution.
|
74
|
+
attr_reader :exception
|
75
|
+
|
76
|
+
# @deprecated
|
77
|
+
# The amount of time taken to execute the test. Unused, probably soon
|
78
|
+
# to be removed or refactored.
|
79
|
+
attr_reader :runtime
|
80
|
+
|
81
|
+
# An Array of Procs to be called after test execution has stopped
|
82
|
+
# (whether by exception or not).
|
83
|
+
attr_reader :teardown_procs
|
84
|
+
|
85
|
+
# @deprecated
|
86
|
+
# Legacy accessor from when test files would only contain one remote
|
87
|
+
# action. Contains the Result of the last call to utilize
|
88
|
+
# {Beaker::DSL::Helpers#on}. Do not use as it is not safe
|
89
|
+
# in test files that use multiple calls to
|
90
|
+
# {Beaker::DSL::Helpers#on}.
|
91
|
+
attr_accessor :result
|
92
|
+
|
93
|
+
# @param [Hosts,Array<Host>] these_hosts The hosts to execute this test
|
94
|
+
# against/on.
|
95
|
+
# @param [Logger] logger A logger that implements
|
96
|
+
# {Beaker::Logger}'s interface.
|
97
|
+
# @param [Hash{String=>String}] config Clusterfck of various config opts.
|
98
|
+
# @param [Hash{Symbol=>String}] options Parsed command line options.
|
99
|
+
# @param [String] path The local path to a test file to be executed.
|
100
|
+
def initialize(these_hosts, logger, config, options={}, path=nil)
|
101
|
+
@config = config['CONFIG']
|
102
|
+
@hosts = these_hosts
|
103
|
+
@logger = logger
|
104
|
+
@options = options
|
105
|
+
@path = path
|
106
|
+
@usr_home = ENV['HOME']
|
107
|
+
@test_status = :pass
|
108
|
+
@exception = nil
|
109
|
+
@runtime = nil
|
110
|
+
@teardown_procs = []
|
111
|
+
|
112
|
+
|
113
|
+
#
|
114
|
+
# We put this on each wrapper (rather than the class) so that methods
|
115
|
+
# defined in the tests don't leak out to other tests.
|
116
|
+
class << self
|
117
|
+
def run_test
|
118
|
+
@runtime = Benchmark.realtime do
|
119
|
+
begin
|
120
|
+
test = File.read(path)
|
121
|
+
eval test,nil,path,1
|
122
|
+
rescue FailTest, TEST_EXCEPTION_CLASS => e
|
123
|
+
@test_status = :fail
|
124
|
+
@exception = e
|
125
|
+
rescue PendingTest
|
126
|
+
@test_status = :pending
|
127
|
+
rescue SkipTest
|
128
|
+
@test_status = :skip
|
129
|
+
rescue StandardError, ScriptError => e
|
130
|
+
log_and_fail_test(e)
|
131
|
+
ensure
|
132
|
+
@teardown_procs.each do |teardown|
|
133
|
+
begin
|
134
|
+
teardown.call
|
135
|
+
rescue StandardError => e
|
136
|
+
log_and_fail_test(e)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
return self
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# Log an error and mark the test as failed, passing through an
|
147
|
+
# exception so it can be displayed at the end of the total run.
|
148
|
+
#
|
149
|
+
# We break out the complete exception backtrace and log each line
|
150
|
+
# individually as well.
|
151
|
+
#
|
152
|
+
# @param exception [Exception] exception to fail with
|
153
|
+
def log_and_fail_test(exception)
|
154
|
+
logger.error(exception.inspect)
|
155
|
+
bt = exception.backtrace
|
156
|
+
logger.pretty_backtrace(bt).each_line do |line|
|
157
|
+
logger.error(line)
|
158
|
+
end
|
159
|
+
@test_status = :error
|
160
|
+
@exception = exception
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# The TestCase as a hash
|
166
|
+
# @api public
|
167
|
+
# @note The visibility and semantics of this method are valid, but the
|
168
|
+
# structure of the Hash it returns may change without notice
|
169
|
+
#
|
170
|
+
# @return [Hash] A Hash representation of this test.
|
171
|
+
def to_hash
|
172
|
+
hash = {}
|
173
|
+
hash['HOSTS'] = {}
|
174
|
+
hash['CONFIG'] = @config
|
175
|
+
@hosts.each do |host|
|
176
|
+
hash['HOSTS'][host.name] = host.overrides
|
177
|
+
end
|
178
|
+
hash
|
179
|
+
end
|
180
|
+
|
181
|
+
# @deprecated
|
182
|
+
# An proxy for the last {Beaker::Result#stdout} returned by
|
183
|
+
# a method that makes remote calls. Use the {Beaker::Result}
|
184
|
+
# object returned by the method directly instead. For Usage see
|
185
|
+
# {Beaker::Result}.
|
186
|
+
def stdout
|
187
|
+
return nil if @result.nil?
|
188
|
+
@result.stdout
|
189
|
+
end
|
190
|
+
|
191
|
+
# @deprecated
|
192
|
+
# An proxy for the last {Beaker::Result#stderr} returned by
|
193
|
+
# a method that makes remote calls. Use the {Beaker::Result}
|
194
|
+
# object returned by the method directly instead. For Usage see
|
195
|
+
# {Beaker::Result}.
|
196
|
+
def stderr
|
197
|
+
return nil if @result.nil?
|
198
|
+
@result.stderr
|
199
|
+
end
|
200
|
+
|
201
|
+
# @deprecated
|
202
|
+
# An proxy for the last {Beaker::Result#exit_code} returned by
|
203
|
+
# a method that makes remote calls. Use the {Beaker::Result}
|
204
|
+
# object returned by the method directly instead. For Usage see
|
205
|
+
# {Beaker::Result}.
|
206
|
+
def exit_code
|
207
|
+
return nil if @result.nil?
|
208
|
+
@result.exit_code
|
209
|
+
end
|
210
|
+
|
211
|
+
# This method retrieves the forge hostname from either:
|
212
|
+
# * The environment variable 'forge_host'
|
213
|
+
# * The parameter 'forge_host' from the CONFIG hash in a node definition
|
214
|
+
#
|
215
|
+
# If none of these are available, it falls back to the static
|
216
|
+
# 'vulcan-acceptance.delivery.puppetlabs.net' hostname
|
217
|
+
#
|
218
|
+
# @return [String] hostname of test forge
|
219
|
+
def forge
|
220
|
+
ENV['forge_host'] ||
|
221
|
+
@config['forge_host'] ||
|
222
|
+
'vulcan-acceptance.delivery.puppetlabs.net'
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|