lxd-common 0.4.1
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 +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
|