lxd-common 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +28 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +34 -0
- data/Rakefile +9 -0
- data/Vagrantfile +15 -0
- data/bin/console +17 -0
- data/bin/setup +8 -0
- data/lib/nexussw/lxd.rb +32 -0
- data/lib/nexussw/lxd/driver.rb +89 -0
- data/lib/nexussw/lxd/driver/cli.rb +12 -0
- data/lib/nexussw/lxd/driver/mixins/cli.rb +182 -0
- data/lib/nexussw/lxd/driver/mixins/rest.rb +128 -0
- data/lib/nexussw/lxd/driver/rest.rb +12 -0
- data/lib/nexussw/lxd/transport.rb +73 -0
- data/lib/nexussw/lxd/transport/cli.rb +12 -0
- data/lib/nexussw/lxd/transport/local.rb +12 -0
- data/lib/nexussw/lxd/transport/mixins/cli.rb +98 -0
- data/lib/nexussw/lxd/transport/mixins/local.rb +71 -0
- data/lib/nexussw/lxd/transport/mixins/rest.rb +120 -0
- data/lib/nexussw/lxd/transport/rest.rb +12 -0
- data/lib/nexussw/lxd/version.rb +5 -0
- data/lxd-common.gemspec +35 -0
- metadata +152 -0
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'yaml'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module NexusSW
|
6
|
+
module LXD
|
7
|
+
class Driver
|
8
|
+
module Mixins
|
9
|
+
module CLI
|
10
|
+
def initialize(inner_transport, driver_options = {})
|
11
|
+
@inner_transport = inner_transport
|
12
|
+
@driver_options = driver_options || {}
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :inner_transport, :driver_options
|
16
|
+
|
17
|
+
def create_container(container_name, container_options = {})
|
18
|
+
if container_exists? container_name
|
19
|
+
start_container container_name # Start for Parity with the below logic (`lxc launch` auto starts)
|
20
|
+
return container_name
|
21
|
+
end
|
22
|
+
cline = "lxc launch #{image_alias(container_options)} #{container_name}"
|
23
|
+
profiles = container_options[:profiles] || []
|
24
|
+
profiles.each { |p| cline += " -p #{p}" }
|
25
|
+
configs = container_options[:config] || {}
|
26
|
+
configs.each { |k, v| cline += " -c #{k}=#{v}" }
|
27
|
+
inner_transport.execute(cline).error!
|
28
|
+
wait_for_status container_name, 'running'
|
29
|
+
container_name
|
30
|
+
end
|
31
|
+
|
32
|
+
def start_container(container_id)
|
33
|
+
return if container_status(container_id) == 'running'
|
34
|
+
inner_transport.execute("lxc start #{container_id}").error!
|
35
|
+
wait_for_status container_id, 'running'
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop_container(container_id, options = {})
|
39
|
+
options ||= {} # default behavior: no timeout or retries. These functions are up to the consumer's context and not really 'sane' defaults
|
40
|
+
return if container_status(container_id) == 'stopped'
|
41
|
+
return inner_transport.execute("lxc stop #{container_id} --force", capture: false).error! if options[:force]
|
42
|
+
LXD.with_timeout_and_retries(options) do
|
43
|
+
return if container_status(container_id) == 'stopped'
|
44
|
+
timeout = " --timeout=#{options[:retry_interval]}" if options[:retry_interval]
|
45
|
+
retval = inner_transport.execute("lxc stop #{container_id}#{timeout || ''}", capture: false)
|
46
|
+
begin
|
47
|
+
retval.error!
|
48
|
+
rescue => e
|
49
|
+
return if container_status(container_id) == 'stopped'
|
50
|
+
# can't distinguish between timeout, or other error.
|
51
|
+
# but if the status call is not popping a 404, and we're not stopped, then a retry is worth it
|
52
|
+
raise Timeout::Retry.new(e) if timeout # rubocop:disable Style/RaiseArgs
|
53
|
+
raise
|
54
|
+
end
|
55
|
+
end
|
56
|
+
wait_for_status container_id, 'stopped'
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete_container(container_id)
|
60
|
+
return unless container_exists? container_id
|
61
|
+
inner_transport.execute("lxc delete #{container_id} --force", capture: false).error!
|
62
|
+
end
|
63
|
+
|
64
|
+
def container_status(container_id)
|
65
|
+
STATUS_CODES[container(container_id)[:status_code].to_i]
|
66
|
+
end
|
67
|
+
|
68
|
+
def convert_keys(oldhash)
|
69
|
+
return oldhash unless oldhash.is_a?(Hash) || oldhash.is_a?(Array)
|
70
|
+
retval = {}
|
71
|
+
if oldhash.is_a? Array
|
72
|
+
retval = []
|
73
|
+
oldhash.each { |v| retval << convert_keys(v) }
|
74
|
+
else
|
75
|
+
oldhash.each do |k, v|
|
76
|
+
retval[k.to_sym] = convert_keys(v)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
retval
|
80
|
+
end
|
81
|
+
|
82
|
+
# YAML is not supported until somewhere in the feature branch
|
83
|
+
# the YAML return has :state and :container at the root level
|
84
|
+
# the JSON return has no :container (:container is root)
|
85
|
+
# and has :state underneath that
|
86
|
+
# (CLI Only) and :state is only available if the container is running
|
87
|
+
def container_state(container_id)
|
88
|
+
res = inner_transport.execute("lxc list #{container_id} --format=json")
|
89
|
+
res.error!
|
90
|
+
JSON.parse(res.stdout).each do |c|
|
91
|
+
return convert_keys(c['state']) if c['name'] == container_id
|
92
|
+
end
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def container(container_id)
|
97
|
+
res = inner_transport.execute("lxc list #{container_id} --format=json")
|
98
|
+
res.error!
|
99
|
+
JSON.parse(res.stdout).each do |c|
|
100
|
+
return convert_keys(c.except('state')) if c['name'] == container_id
|
101
|
+
end
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def container_exists?(container_id)
|
106
|
+
return true if container_status(container_id)
|
107
|
+
return false
|
108
|
+
rescue
|
109
|
+
false
|
110
|
+
end
|
111
|
+
|
112
|
+
include WaitMixin
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
def wait_for_status(container_id, newstatus)
|
117
|
+
loop do
|
118
|
+
status = container_status(container_id)
|
119
|
+
return if status == newstatus
|
120
|
+
NIO::WebSocket.logger.debug "#{container_id} status = '#{status}'. Waiting for '#{newstatus}'"
|
121
|
+
sleep 0.5
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def remote_for!(url, protocol = 'lxd')
|
128
|
+
raise 'Protocol is required' unless protocol # protect me from accidentally slipping in a nil
|
129
|
+
# normalize the url and 'require' protocol to protect against a scenario:
|
130
|
+
# 1) user only specifies https://someimageserver.org without specifying the protocol
|
131
|
+
# 2) the rest of this function would blindly add that without saying the protocol
|
132
|
+
# 3) 'lxc remote add' would add that remote, but defaults to the lxd protocol and appends ':8443' to the saved url
|
133
|
+
# 4) the next time this function is called we would not match that same entry due to the ':8443'
|
134
|
+
# 5) ultimately resulting in us adding a new remote EVERY time this function is called
|
135
|
+
port = url.split(':', 3)[2]
|
136
|
+
url += ':8443' unless port || protocol != 'lxd'
|
137
|
+
remotes = begin
|
138
|
+
YAML.load(inner_transport.read_file('~/.config/lxc/config.yml')) || {}
|
139
|
+
rescue
|
140
|
+
{}
|
141
|
+
end
|
142
|
+
# make sure these default entries are available to us even if config.yml isn't created yet
|
143
|
+
# and i've seen instances where these defaults don't live in the config.yml
|
144
|
+
remotes = { 'remotes' => {
|
145
|
+
'images' => { 'addr' => 'https://images.linuxcontainers.org' },
|
146
|
+
'ubuntu' => { 'addr' => 'https://cloud-images.ubuntu.com/releases' },
|
147
|
+
'ubuntu-daily' => { 'addr' => 'https://cloud-images.ubuntu.com/daily' },
|
148
|
+
} }.merge remotes
|
149
|
+
max = 0
|
150
|
+
remotes['remotes'].each do |remote, data|
|
151
|
+
return remote.to_s if data['addr'] == url
|
152
|
+
num = remote.to_s.split('-', 2)[1] if remote.to_s.start_with? 'images-'
|
153
|
+
max = num.to_i if num && num.to_i > max
|
154
|
+
end
|
155
|
+
remote = "images-#{max + 1}"
|
156
|
+
inner_transport.execute("lxc remote add #{remote} #{url} --accept-certificate --protocol=#{protocol}").error!
|
157
|
+
remote
|
158
|
+
end
|
159
|
+
|
160
|
+
def image(properties, remote = '')
|
161
|
+
return nil unless properties && properties.any?
|
162
|
+
cline = "lxc image list #{remote} --format=json"
|
163
|
+
properties.each { |k, v| cline += " #{k}=#{v}" }
|
164
|
+
res = inner_transport.execute cline
|
165
|
+
res.error!
|
166
|
+
res = JSON.parse(res.stdout)
|
167
|
+
return res[0]['fingerprint'] if res.any?
|
168
|
+
end
|
169
|
+
|
170
|
+
def image_alias(container_options)
|
171
|
+
remote = container_options[:server] ? remote_for!(container_options[:server], container_options[:protocol] || 'lxd') + ':' : ''
|
172
|
+
name = container_options[:alias]
|
173
|
+
name ||= container_options[:fingerprint]
|
174
|
+
name ||= image(container_options[:properties], remote)
|
175
|
+
raise 'No image parameters. One of alias, fingerprint, or properties must be specified (The CLI interface does not support empty containers)' unless name
|
176
|
+
"#{remote}#{name}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'hyperkit'
|
2
|
+
|
3
|
+
module NexusSW
|
4
|
+
module LXD
|
5
|
+
class Driver
|
6
|
+
module Mixins
|
7
|
+
module Rest
|
8
|
+
# PARITY note: CLI functions are on an indefinite timeout by default, yet we have a 2 minute socket read timeout
|
9
|
+
# Leaving it alone, for now, on calls that are quick in nature
|
10
|
+
# Adapting on known long running calls such as create, stop, execute
|
11
|
+
# REQUEST_TIMEOUT = 120 # upstream default: 120
|
12
|
+
def initialize(rest_endpoint, driver_options = {}, inner_driver = nil)
|
13
|
+
@rest_endpoint = rest_endpoint
|
14
|
+
@driver_options = driver_options
|
15
|
+
hkoptions = (driver_options || {}).merge(
|
16
|
+
api_endpoint: rest_endpoint,
|
17
|
+
auto_sync: true
|
18
|
+
)
|
19
|
+
@hk = inner_driver || Hyperkit::Client.new(hkoptions)
|
20
|
+
# HACK: can't otherwise get at the request timeout because sawyer is in the way
|
21
|
+
# Beware of unused function in hyperkit: reset_agent If that gets used it'll undo this timeout
|
22
|
+
# unneeded while default valued: @hk.agent.instance_variable_get(:@conn).options[:timeout] = REQUEST_TIMEOUT
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :hk, :rest_endpoint, :driver_options
|
26
|
+
|
27
|
+
def create_container(container_name, container_options = {})
|
28
|
+
if container_exists?(container_name)
|
29
|
+
start_container container_name # Start the container for Parity with the CLI
|
30
|
+
return container_name
|
31
|
+
end
|
32
|
+
# we'll break this apart and time it out for those with slow net (and this was my 3 minute stress test case with good net)
|
33
|
+
# parity note: CLI will run indefinitely rather than timeout hence the 0 timeout
|
34
|
+
retry_forever do
|
35
|
+
@hk.create_container(container_name, container_options.merge(sync: false))
|
36
|
+
end
|
37
|
+
start_container container_name
|
38
|
+
container_name
|
39
|
+
end
|
40
|
+
|
41
|
+
def start_container(container_id)
|
42
|
+
return if container_status(container_id) == 'running'
|
43
|
+
retry_forever do
|
44
|
+
@hk.start_container(container_id, sync: false)
|
45
|
+
end
|
46
|
+
wait_for_status container_id, 'running'
|
47
|
+
end
|
48
|
+
|
49
|
+
def stop_container(container_id, options = {})
|
50
|
+
return if container_status(container_id) == 'stopped'
|
51
|
+
return @hk.stop_container(container_id, force: true) if options[:force]
|
52
|
+
last_id = nil
|
53
|
+
use_last = false
|
54
|
+
LXD.with_timeout_and_retries({ timeout: 0 }.merge(options)) do # timeout: 0 to enable retry functionality
|
55
|
+
return if container_status(container_id) == 'stopped'
|
56
|
+
begin
|
57
|
+
unless use_last
|
58
|
+
# Keep resubmitting until the server complains (Stops will be ignored/hang if init is not yet listening for SIGPWR i.e. recently started)
|
59
|
+
begin
|
60
|
+
last_id = @hk.stop_container(container_id, sync: false)[:id] # TODO: this 'could' hang for 2 minutes? and then we'd never get a last_id if that's where the hang happens, and then it 'could' error on retry
|
61
|
+
rescue Hyperkit::BadRequest # Happens if a stop command has previously been accepted as well as other reasons. handle that on next line
|
62
|
+
raise unless last_id # if we have a last_id then a prior stop command has successfully initiated so we'll just wait on that one
|
63
|
+
use_last = true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
@hk.wait_for_operation last_id # , options[:retry_interval]
|
67
|
+
rescue Faraday::TimeoutError => e
|
68
|
+
return if container_status(container_id) == 'stopped'
|
69
|
+
raise Timeout::Retry.new e # if options[:retry_interval] # rubocop:disable Style/RaiseArgs
|
70
|
+
end
|
71
|
+
end
|
72
|
+
wait_for_status container_id, 'stopped'
|
73
|
+
end
|
74
|
+
|
75
|
+
def delete_container(container_id)
|
76
|
+
return unless container_exists? container_id
|
77
|
+
stop_container container_id, force: true
|
78
|
+
@hk.delete_container(container_id)
|
79
|
+
end
|
80
|
+
|
81
|
+
def container_status(container_id)
|
82
|
+
STATUS_CODES[container(container_id)[:status_code].to_i]
|
83
|
+
end
|
84
|
+
|
85
|
+
def container_state(container_id)
|
86
|
+
return nil unless container_status(container_id) == 'running' # Parity with CLI
|
87
|
+
@hk.container_state(container_id)
|
88
|
+
end
|
89
|
+
|
90
|
+
def container(container_id)
|
91
|
+
@hk.container container_id
|
92
|
+
end
|
93
|
+
|
94
|
+
def container_exists?(container_id)
|
95
|
+
return true if container_status(container_id)
|
96
|
+
return false
|
97
|
+
rescue
|
98
|
+
false
|
99
|
+
end
|
100
|
+
|
101
|
+
include WaitMixin
|
102
|
+
|
103
|
+
protected
|
104
|
+
|
105
|
+
def wait_for_status(container_id, newstatus)
|
106
|
+
loop do
|
107
|
+
return if container_status(container_id) == newstatus
|
108
|
+
sleep 0.5
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def retry_forever
|
115
|
+
retval = yield
|
116
|
+
LXD.with_timeout_and_retries timeout: 0 do
|
117
|
+
begin
|
118
|
+
@hk.wait_for_operation retval[:id]
|
119
|
+
rescue Faraday::TimeoutError => e
|
120
|
+
raise Timeout::Retry.new e # rubocop:disable Style/RaiseArgs
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'nexussw/lxd'
|
2
|
+
|
3
|
+
module NexusSW
|
4
|
+
module LXD
|
5
|
+
class Transport
|
6
|
+
class LXDExecuteResult
|
7
|
+
def initialize(command, options, exitstatus)
|
8
|
+
@command = command
|
9
|
+
@options = options || {}
|
10
|
+
@exitstatus = exitstatus
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :exitstatus, :options, :command
|
14
|
+
|
15
|
+
def stdout
|
16
|
+
options[:capture_options][:stdout] if options.key? :capture_options
|
17
|
+
end
|
18
|
+
|
19
|
+
def stderr
|
20
|
+
options[:capture_options][:stderr] if options.key? :capture_options
|
21
|
+
end
|
22
|
+
|
23
|
+
def error!
|
24
|
+
return self if exitstatus == 0
|
25
|
+
msg = "Error: '#{command}' failed with exit code #{exitstatus}.\n"
|
26
|
+
msg += "STDOUT: #{stdout}" if stdout && !stdout.empty?
|
27
|
+
msg += "STDERR: #{stderr}" if stderr && !stderr.empty?
|
28
|
+
raise msg
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module ExecuteMixin
|
33
|
+
def execute(command, options = {}, &block)
|
34
|
+
options ||= {}
|
35
|
+
return execute_chunked(command, options) if options[:capture] == false && !block_given?
|
36
|
+
|
37
|
+
capture_options = { stdout: '', stderr: '' }
|
38
|
+
capture_options[:capture] = block if block_given?
|
39
|
+
capture_options[:capture] ||= options[:capture] if options[:capture].respond_to? :call
|
40
|
+
# capture_options[:capture] ||= options[:stream] if options[:stream].respond_to? :call
|
41
|
+
capture_options[:capture] ||= proc do |stdout_chunk, stderr_chunk|
|
42
|
+
capture_options[:stdout] += stdout_chunk if stdout_chunk
|
43
|
+
capture_options[:stderr] += stderr_chunk if stderr_chunk
|
44
|
+
end
|
45
|
+
|
46
|
+
execute_chunked(command, options.merge(capture_options: capture_options), &capture_options[:capture])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def execute(_command, _options = {})
|
51
|
+
raise 'NexusSW::LXD::Transport.execute not implemented'
|
52
|
+
end
|
53
|
+
|
54
|
+
def read_file(_path)
|
55
|
+
raise 'NexusSW::LXD::Transport.read_file not implemented'
|
56
|
+
end
|
57
|
+
|
58
|
+
def write_file(_path, _content)
|
59
|
+
raise 'NexusSW::LXD::Transport.write_file not implemented'
|
60
|
+
end
|
61
|
+
|
62
|
+
def download_file(_path, _local_path)
|
63
|
+
raise 'NexusSW::LXD::Transport.download_file not implemented'
|
64
|
+
end
|
65
|
+
|
66
|
+
def upload_file(_local_path, _path)
|
67
|
+
raise 'NexusSW::LXD::Transport.upload_file not implemented'
|
68
|
+
end
|
69
|
+
# protected
|
70
|
+
# def execute_chunked(_command, _options = {})
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'nexussw/lxd/transport/mixins/local'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
module NexusSW
|
6
|
+
module LXD
|
7
|
+
class Transport
|
8
|
+
module Mixins
|
9
|
+
module CLI
|
10
|
+
def initialize(remote_transport, container_name, config = {})
|
11
|
+
@container_name = container_name
|
12
|
+
@config = config
|
13
|
+
@inner_transport = remote_transport
|
14
|
+
@punt = !inner_transport.is_a?(::NexusSW::LXD::Transport::Mixins::Local)
|
15
|
+
end
|
16
|
+
attr_reader :inner_transport, :punt, :container_name, :config
|
17
|
+
|
18
|
+
def execute(command, options = {})
|
19
|
+
mycommand = command.is_a?(Array) ? command.join(' ') : command
|
20
|
+
subcommand = options[:subcommand] || "exec #{container_name} --"
|
21
|
+
mycommand = "lxc #{subcommand} #{mycommand}"
|
22
|
+
options = options.except :subcommand if options.key? :subcommand
|
23
|
+
# We would have competing timeout logic depending on who the inner transport is
|
24
|
+
# I'll just let rest & local do the timeouts, and if inner is a chef sourced transport, they have timeout logic of their own
|
25
|
+
# with_timeout_and_retries(options) do
|
26
|
+
inner_transport.execute mycommand, options
|
27
|
+
end
|
28
|
+
|
29
|
+
def read_file(path)
|
30
|
+
tfile = inner_mktmp
|
31
|
+
retval = execute("#{@container_name}#{path} #{tfile}", subcommand: 'file pull', capture: false)
|
32
|
+
return '' if retval.exitstatus == 1
|
33
|
+
retval.error!
|
34
|
+
return inner_transport.read_file tfile
|
35
|
+
ensure
|
36
|
+
inner_transport.execute("rm -rf #{tfile}", capture: false) if tfile
|
37
|
+
end
|
38
|
+
|
39
|
+
def write_file(path, content)
|
40
|
+
tfile = inner_mktmp
|
41
|
+
inner_transport.write_file tfile, content
|
42
|
+
execute("#{tfile} #{container_name}#{path}", subcommand: 'file push', capture: false).error!
|
43
|
+
ensure
|
44
|
+
inner_transport.execute("rm -rf #{tfile}", capture: false) if tfile
|
45
|
+
end
|
46
|
+
|
47
|
+
def download_file(path, local_path)
|
48
|
+
tfile = inner_mktmp if punt
|
49
|
+
localname = tfile || local_path
|
50
|
+
execute("#{container_name}#{path} #{localname}", subcommand: 'file pull', capture: false).error!
|
51
|
+
inner_transport.download_file tfile, local_path if tfile
|
52
|
+
ensure
|
53
|
+
inner_transport.execute("rm -rf #{tfile}", capture: false) if tfile
|
54
|
+
end
|
55
|
+
|
56
|
+
def upload_file(local_path, path)
|
57
|
+
tfile = inner_mktmp if punt
|
58
|
+
localname = tfile || local_path
|
59
|
+
inner_transport.upload_file local_path, tfile if tfile
|
60
|
+
execute("#{localname} #{container_name}#{path}", subcommand: 'file push', capture: false).error!
|
61
|
+
ensure
|
62
|
+
inner_transport.execute("rm -rf #{tfile}", capture: false) if tfile
|
63
|
+
end
|
64
|
+
|
65
|
+
def add_remote(host_name)
|
66
|
+
execute("add #{host_name} --accept-certificate", subcommand: 'remote').error! unless remote? host_name
|
67
|
+
end
|
68
|
+
|
69
|
+
def linked_transport(host_name)
|
70
|
+
linked = inner_transport.linked_transport(host_name) if inner_transport.is_a?(::NexusSW::LXD::Transport::CLI)
|
71
|
+
return linked if linked
|
72
|
+
return nil unless remote?(host_name)
|
73
|
+
new(driver, inner_transport, "#{host_name}:#{container_name}", config)
|
74
|
+
end
|
75
|
+
|
76
|
+
def remote?(host_name)
|
77
|
+
result = execute 'list', subcommand: 'remote'
|
78
|
+
result.error!
|
79
|
+
result.stdout.each_line do |line|
|
80
|
+
return true if line.start_with? "| #{host_name} "
|
81
|
+
end
|
82
|
+
false
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# kludge for windows environment
|
88
|
+
def inner_mktmp
|
89
|
+
tfile = Tempfile.new(container_name)
|
90
|
+
"/tmp/#{File.basename tfile.path}"
|
91
|
+
ensure
|
92
|
+
tfile.unlink
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|