lxd-common 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ require 'nexussw/lxd/driver'
2
+ require 'nexussw/lxd/driver/mixins/cli'
3
+
4
+ module NexusSW
5
+ module LXD
6
+ class Driver
7
+ class CLI < Driver
8
+ include Mixins::CLI
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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,12 @@
1
+ require 'nexussw/lxd/driver'
2
+ require 'nexussw/lxd/driver/mixins/rest'
3
+
4
+ module NexusSW
5
+ module LXD
6
+ class Driver
7
+ class Rest < Driver
8
+ include Mixins::Rest
9
+ end
10
+ end
11
+ end
12
+ 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,12 @@
1
+ require 'nexussw/lxd/transport'
2
+ require 'nexussw/lxd/transport/mixins/cli'
3
+
4
+ module NexusSW
5
+ module LXD
6
+ class Transport
7
+ class CLI < Transport
8
+ include Mixins::CLI
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ require 'nexussw/lxd/transport'
2
+ require 'nexussw/lxd/transport/mixins/local'
3
+
4
+ module NexusSW
5
+ module LXD
6
+ class Transport
7
+ class Local < Transport
8
+ include Mixins::Local
9
+ end
10
+ end
11
+ end
12
+ 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