lib/nexussw/lxd/driver/mixins/cli.rb in lxd-common-0.9.8 vs lib/nexussw/lxd/driver/mixins/cli.rb in lxd-common-0.9.9
- old
+ new
@@ -1,10 +1,10 @@
-require 'nexussw/lxd/driver/mixins/helpers/wait'
-require 'nexussw/lxd/transport/cli'
-require 'tempfile'
-require 'yaml'
-require 'json'
+require "nexussw/lxd/driver/mixins/helpers/wait"
+require "nexussw/lxd/transport/cli"
+require "tempfile"
+require "yaml"
+require "json"
module NexusSW
module LXD
class Driver
module Mixins
@@ -15,53 +15,119 @@
end
attr_reader :inner_transport, :driver_options
def transport_for(container_name)
- Transport::CLI.new inner_transport, container_name, info: YAML.load(inner_transport.execute('lxc info').error!.stdout)
+ Transport::CLI.new inner_transport, container_name, info: YAML.load(inner_transport.execute("lxc info").error!.stdout)
end
def create_container(container_name, container_options = {})
+ autostart = (container_options.delete(:autostart) != false)
if container_exists? container_name
- start_container container_name # Start for Parity with the below logic (`lxc launch` auto starts)
+ start_container(container_name) if autostart
return container_name
end
cline = "lxc launch #{image_alias(container_options)} #{container_name}"
profiles = container_options[:profiles] || []
profiles.each { |p| cline += " -p #{p}" }
configs = container_options[:config] || {}
configs.each { |k, v| cline += " -c #{k}=#{v}" }
+ if !autostart || container_options[:devices] # append to the cline to avoid potential lag between create & stop
+ cline += " && lxc stop -f #{container_name}"
+ cline = ["sh", "-c", cline] # There's no guarantee that inner_transport is running a shell for the && operator
+ end
inner_transport.execute(cline).error!
- wait_for_status container_name, 'running'
+ if container_options[:devices]
+ update_container(container_name, devices: container_options[:devices])
+ start_container(container_name) if autostart
+ else
+ wait_for_status container_name, "running" if autostart
+ end
container_name
end
+ def update_container(container_name, container_options)
+ raise NexusSW::LXD::RestAPI::Error::NotFound, "Container (#{container_name}) does not exist" unless container_exists? container_name
+ configs = container_options[:config]
+ devices = container_options[:devices]
+ profiles = container_options[:profiles]
+ existing = container(container_name)
+
+ if configs
+ configs.each do |k, v|
+ if v.nil?
+ next unless existing[:config][k]
+ inner_transport.execute("lxc config unset #{container_name} #{k}").error!
+ else
+ next if existing[:config][k] == v
+ inner_transport.execute("lxc config set #{container_name} #{k} #{v}").error!
+ end
+ end
+ end
+
+ if devices
+ devices.each do |name, device|
+ cmd = "add"
+ if device.nil?
+ next unless existing[:devices].include? name
+ inner_transport.execute("lxc config device remove #{container_name} #{name}").error!
+ next
+ elsif existing[:devices].include?(name)
+ cmd = "set"
+ if existing[:devices][name][:type] != device[:type]
+ inner_transport.execute("lxc config device remove #{container_name} #{name}").error!
+ cmd = "add"
+ end
+ end
+ if cmd == "add"
+ cline = "lxc config device add #{container_name} #{name} #{device[:type]}"
+ device.each do |k, v|
+ cline << " #{k}=#{v}"
+ end
+ inner_transport.execute(cline).error!
+ else
+ device.each do |k, v|
+ next if k == :type
+ next if v == existing[:devices][name][k]
+ inner_transport.execute("lxc config device set #{container_name} #{name} #{k} #{v}").error!
+ end
+ end
+ end
+ end
+
+ if profiles
+ inner_transport.execute("lxc profile assign #{container_name} #{profiles.join(",")}").error! unless profiles == existing[:profiles]
+ end
+
+ container container_name
+ end
+
def start_container(container_id)
- return if container_status(container_id) == 'running'
+ return if container_status(container_id) == "running"
inner_transport.execute("lxc start #{container_id}").error!
- wait_for_status container_id, 'running'
+ wait_for_status container_id, "running"
end
def stop_container(container_id, options = {})
options ||= {} # default behavior: no timeout or retries. These functions are up to the consumer's context and not really 'sane' defaults
- return if container_status(container_id) == 'stopped'
+ return if container_status(container_id) == "stopped"
return inner_transport.execute("lxc stop #{container_id} --force", capture: false).error! if options[:force]
LXD.with_timeout_and_retries(options) do
- return if container_status(container_id) == 'stopped'
+ return if container_status(container_id) == "stopped"
timeout = " --timeout=#{options[:retry_interval]}" if options[:retry_interval]
retval = inner_transport.execute("lxc stop #{container_id}#{timeout || ''}", capture: false)
begin
retval.error!
rescue => e
- return if container_status(container_id) == 'stopped'
+ return if container_status(container_id) == "stopped"
# can't distinguish between timeout, or other error.
# but if the status call is not popping a 404, and we're not stopped, then a retry is worth it
raise Timeout::Retry.new(e) if timeout # rubocop:disable Style/RaiseArgs
raise
end
end
- wait_for_status container_id, 'stopped'
+ wait_for_status container_id, "stopped"
end
def delete_container(container_id)
return unless container_exists? container_id
inner_transport.execute("lxc delete #{container_id} --force", capture: false).error!
@@ -69,50 +135,36 @@
def container_status(container_id)
STATUS_CODES[container(container_id)[:status_code].to_i]
end
- def convert_keys(oldhash)
- return oldhash unless oldhash.is_a?(Hash) || oldhash.is_a?(Array)
- retval = {}
- if oldhash.is_a? Array
- retval = []
- oldhash.each { |v| retval << convert_keys(v) }
- else
- oldhash.each do |k, v|
- retval[k.to_sym] = convert_keys(v)
- end
- end
- retval
- end
-
# YAML is not supported until somewhere in the feature branch
# the YAML return has :state and :container at the root level
# the JSON return has no :container (:container is root)
# and has :state underneath that
# (CLI Only) and :state is only available if the container is running
def container_state(container_id)
res = inner_transport.execute("lxc list #{container_id} --format=json")
res.error!
JSON.parse(res.stdout).each do |c|
- return convert_keys(c['state']) if c['name'] == container_id
+ return LXD.symbolize_keys(c["state"]) if c["name"] == container_id
end
nil
end
def container(container_id)
res = inner_transport.execute("lxc list #{container_id} --format=json")
res.error!
JSON.parse(res.stdout).each do |c|
- return convert_keys(c.reject { |k, _| k == 'state' }) if c['name'] == container_id
+ return Driver.convert_bools(LXD.symbolize_keys(c.reject { |k, _| k == "state" })) if c["name"] == container_id
end
nil
end
def container_exists?(container_id)
return true if container_status(container_id)
- return false
+ false
rescue
false
end
include Helpers::WaitMixin
@@ -128,58 +180,58 @@
end
end
private
- def remote_for!(url, protocol = 'lxd')
- raise 'Protocol is required' unless protocol # protect me from accidentally slipping in a nil
+ def remote_for!(url, protocol = "lxd")
+ raise "Protocol is required" unless protocol # protect me from accidentally slipping in a nil
# normalize the url and 'require' protocol to protect against a scenario:
# 1) user only specifies https://someimageserver.org without specifying the protocol
# 2) the rest of this function would blindly add that without saying the protocol
# 3) 'lxc remote add' would add that remote, but defaults to the lxd protocol and appends ':8443' to the saved url
# 4) the next time this function is called we would not match that same entry due to the ':8443'
# 5) ultimately resulting in us adding a new remote EVERY time this function is called
- port = url.split(':', 3)[2]
- url += ':8443' unless port || protocol != 'lxd'
+ port = url.split(":", 3)[2]
+ url += ":8443" unless port || protocol != "lxd"
remotes = begin
- YAML.load(inner_transport.read_file('~/.config/lxc/config.yml')) || {}
+ YAML.load(inner_transport.read_file("~/.config/lxc/config.yml")) || {}
rescue
{}
end
# make sure these default entries are available to us even if config.yml isn't created yet
# and i've seen instances where these defaults don't live in the config.yml
- remotes = { 'remotes' => {
- 'images' => { 'addr' => 'https://images.linuxcontainers.org' },
- 'ubuntu' => { 'addr' => 'https://cloud-images.ubuntu.com/releases' },
- 'ubuntu-daily' => { 'addr' => 'https://cloud-images.ubuntu.com/daily' },
+ remotes = { "remotes" => {
+ "images" => { "addr" => "https://images.linuxcontainers.org" },
+ "ubuntu" => { "addr" => "https://cloud-images.ubuntu.com/releases" },
+ "ubuntu-daily" => { "addr" => "https://cloud-images.ubuntu.com/daily" },
} }.merge remotes
max = 0
- remotes['remotes'].each do |remote, data|
- return remote.to_s if data['addr'] == url
- num = remote.to_s.split('-', 2)[1] if remote.to_s.start_with? 'images-'
+ remotes["remotes"].each do |remote, data|
+ return remote.to_s if data["addr"] == url
+ num = remote.to_s.split("-", 2)[1] if remote.to_s.start_with? "images-"
max = num.to_i if num && num.to_i > max
end
remote = "images-#{max + 1}"
inner_transport.execute("lxc remote add #{remote} #{url} --accept-certificate --protocol=#{protocol}").error!
remote
end
- def image(properties, remote = '')
+ def image(properties, remote = "")
return nil unless properties && properties.any?
cline = "lxc image list #{remote} --format=json"
properties.each { |k, v| cline += " #{k}=#{v}" }
res = inner_transport.execute cline
res.error!
res = JSON.parse(res.stdout)
- return res[0]['fingerprint'] if res.any?
+ return res[0]["fingerprint"] if res.any?
end
def image_alias(container_options)
- remote = container_options[:server] ? remote_for!(container_options[:server], container_options[:protocol] || 'lxd') + ':' : ''
+ remote = container_options[:server] ? remote_for!(container_options[:server], container_options[:protocol] || "lxd") + ":" : ""
name = container_options[:alias]
name ||= container_options[:fingerprint]
name ||= image(container_options[:properties], remote)
- raise 'No image parameters. One of alias, fingerprint, or properties must be specified (The CLI interface does not support empty containers)' unless name
+ raise "No image parameters. One of alias, fingerprint, or properties must be specified (The CLI interface does not support empty containers)" unless name
"#{remote}#{name}"
end
end
end
end