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