class Kitchen::Driver::Lxd

Constants

UBUNTU_RELEASES

only bothering with the releases on linuxcontainers.org leaving this mutable so that end-users can append new releases to it Usage Note: If a future release is not in the below table, just specify the full image name in the kitchen yml instead of using ubuntu-<version>

Public Class Methods

new(config = {}) click to toggle source
Calls superclass method
# File lib/kitchen/driver/lxd.rb, line 22
def initialize(config = {})
  # pp 'Config:', config
  super
end

Public Instance Methods

create(state) click to toggle source
# File lib/kitchen/driver/lxd.rb, line 35
def create(state)
  state[:config] = config.select { |k, _| [:server, :port, :rest_options, :image_server].include? k }
  info "Utilizing REST interface at " + host_address if respond_to?(:info) && can_rest?

  state[:username] = config[:username] if config.key? :username
  state[:container_name] = new_container_name unless state[:container_name]

  # TODO: convergent behavior on container_options change? (:profiles :config)
  state[:container_options] = container_options

  info "Container name: #{state[:container_name]}"
  driver.create_container(state[:container_name], state[:container_options])

  # Allow SSH transport on known images with sshd enabled
  # This will only work if the container is routable.  LXD does not do port forwarding (yet)
  # Which also means that you might need to do 'ssh_login: false' in the config if you're using a cloud-image and aren't routable
  # think ahead for default behaviour once LXD can port forward
  # FUTURE: If I get time I'll look into faking a port forward with something under /dev/ until then
  if use_ssh?
    # Normalize [:ssh_login]
    config[:ssh_login] = { username: config[:ssh_login] } if config[:ssh_login].is_a? String
    config[:ssh_login] ||= {} # if config[:ssh_login] && !config.to_hash[:ssh_login].is_a?(Hash)

    state[:username] = config[:ssh_login][:username] if config[:ssh_login].key? :username
    state[:username] ||= "root"
  end

  if (state[:username] && (state[:username] != "root")) && cloud_image?
    info "Waiting for cloud-init..."
    driver.wait_for state[:container_name], :cloud_init
  else
    info "Waiting for an IP address..."
  end
  state[:ip_address] = state[:hostname] = container_ip(state)

  if use_ssh?
    setup_ssh(state[:username], config[:ssh_login][:public_key] || "#{ENV['HOME']}/.ssh/id_rsa.pub", state)
    info "SSH access enabled on #{state[:ip_address]}"
  else
    # TODO: this section is only for the base images on linuxcontainers.org... (and I still need to account for yum)
    # they need patched because they don't have wget, or anything else with which to download the chef client
    # Custom images should account for this, so I won't run this patch for them (in the name of testing speed)
    unless cloud_image?
      transport = nx_transport(state)
      transport.reset_user
      # only centos/7 and various ubuntu versions have been tested here
      #   - ubuntu non-cloud has no download utilities in order to dl the chef package so we must adapt that
      #   - centos/7 needs sudo installed, or you need to use sudo:false on the provisioner...  leaving it explicit for the user to fix
      unless transport.execute("test -d /etc/apt").error?
        info "Installing additional dependencies..."
        transport.execute("apt install openssl curl ca-certificates -y").error!
      end
    end
  end
end
destroy(state) click to toggle source
# File lib/kitchen/driver/lxd.rb, line 97
def destroy(state)
  info "Destroying container #{state[:container_name]}"
  driver.delete_container state[:container_name]
end
finalize_config!(instance) click to toggle source
Calls superclass method
# File lib/kitchen/driver/lxd.rb, line 91
def finalize_config!(instance)
  super.tap do
    instance.transport = Kitchen::Transport::Lxd.new config unless lxd_transport_selected? || use_ssh?
  end
end

Private Instance Methods

cloud_image?() click to toggle source
# File lib/kitchen/driver/lxd.rb, line 120
def cloud_image?
  server = image_server
  return false unless server && server[:server]
  server[:server].downcase.start_with? "https://cloud-images.ubuntu.com"
end
container_ip(state) click to toggle source
# File lib/kitchen/driver/lxd.rb, line 231
def container_ip(state)
  # TODO: make timeout configurable
  driver.wait_for state[:container_name], :ip # , 60 # default timeout=60
end
container_options() click to toggle source
# File lib/kitchen/driver/lxd.rb, line 188
def container_options
  options = image_server
  # 0:
  found = false
  %w{:alias :fingerprint :properties}.each do |k|
    if config.key? k
      options[k] = config[k]
      found = true
    end
  end
  options[:alias] = image_name(options[:server]) unless found
  options.merge(config.select { |k, _| [:profiles, :config, :devices].include? k })
end
image_name(server) click to toggle source

Special cases: using example `ubuntu-16.04`

0: if alias, fingerprint, or properties are specified, use instead of the below: (handled by caller)
1: if server.start_with? 'https://cloud-images.ubuntu.com'
    - trim the leading `ubuntu-` (optionally specified)
2: if server.start_with? 'https://images.linuxcontainers.org'
    - replace `-` with `/` (in all cases?)
    - replace version with codename, if dist == 'ubuntu'
# File lib/kitchen/driver/lxd.rb, line 152
def image_name(server)
  name = instance.platform.name
  return name unless server

  # 1:
  if server.downcase.start_with? "https://cloud-images.ubuntu.com"
    info "Using cloud-image '#{name}'"
    return name.downcase.sub(/^ubuntu-/, "")
  end
  # 2:
  if server.downcase.start_with? "https://images.linuxcontainers.org"
    name = name.downcase.split("-")
    # 'core' parses out in this method as the 'version' so just use 'ubuntu-core' in the kitchen.yml
    if UBUNTU_RELEASES.key?(name[1]) && name[0] == "ubuntu"
      name[1] = UBUNTU_RELEASES[name[1]]
      name[0] = "ubuntu-core" if name[1] == "16" # Logic patch for the edge case.  We'll do something different if this gets complicated
    end
    name = name.join("/")
    info "Using standard image #{name}"
  end
  name
end
image_server() click to toggle source

Normalize into a hash with the correct protocol We'll take a given hash verbatim But we'll allow a simple string to default to the simplestreams protocol if no port is specified (otherwise 'lxd' is default, but counterintuitive given that if we specify neither a port nor a protocol, 8443 will be appended for lxd's default) Side effect: differing behavior (protocol) depending on whether a port is specified on a simple string

which is (ok?)  If you want to specify an odd port, you should probably also specify which protocol
# File lib/kitchen/driver/lxd.rb, line 136
def image_server
  server = config[:image_server]
  if server.is_a? String
    server = { server: server }
    server[:protocol] = "simplestreams" if server[:server].split(":", 3)[2].nil?
  end
  server
end
lxd_transport_selected?() click to toggle source

if selected by user during startup (finalize_config!) later, if selected by the user, or if deemed necessary by the driver

# File lib/kitchen/driver/lxd.rb, line 116
def lxd_transport_selected?
  instance.transport.is_a? Kitchen::Transport::Lxd
end
new_container_name() click to toggle source
# File lib/kitchen/driver/lxd.rb, line 126
def new_container_name
  instance.name + "-" + SecureRandom.hex(8)
end
setup_ssh(username, pubkey, state) click to toggle source
# File lib/kitchen/driver/lxd.rb, line 202
def setup_ssh(username, pubkey, state)
  # DEFERRED: should I create the ssh user if it doesn't exist? (I've seen that in other drivers)
  # not for now...  that is an edge case within an edge case, and the default case just shells in as 'root' without concept of 'users'
  # submit a feature request if you need me to create a user
  # and that is if it is unfeasible for you to create a custom image with that user included
  return if state[:ssh_enabled]
  raise ActionFailed, "Public Key File does not exist (#{pubkey})" unless File.exist? pubkey

  transport = nx_transport(state)
  transport.reset_user
  remote_file = "/tmp/#{state[:container_name]}-publickey"
  transport.upload_file pubkey, remote_file
  begin
    sshdir = transport.execute("bash -c \"grep '^#{username}:' /etc/passwd | cut -d':' -f 6\"").error!.stdout.strip
  rescue => e
    fatal "Transport Error querying SSH User: #{sshdir}\n#{e.message}"
    raise
  ensure
    raise ActionFailed, "User (#{username}), or their home directory, were not found within container (#{state[:container_name]})" unless sshdir && !sshdir.empty?
    sshdir += "/.ssh"
  end
  ak_file = sshdir + "/authorized_keys"

  info "Inserting public key for container user '#{username}'"
  transport.execute("bash -c 'mkdir -p #{sshdir} 2> /dev/null; cat #{remote_file} >> #{ak_file} \
    && rm -rf #{remote_file} && chown -R #{username}:#{username} #{sshdir}'").error! # , capture: false
  state[:ssh_enabled] = true
end
use_ssh?() click to toggle source

ssh is kitchen's default unless lxd is selected by the user otherwise, only use ssh when the user supplies info and automatically, only on capable cloud images (requires kitchen 1.19 to perform the automatic override)

# File lib/kitchen/driver/lxd.rb, line 107
def use_ssh?
  return false if lxd_transport_selected?
  return true if config[:ssh_login]
  return false if config[:ssh_login] == false # allow forced disable for cloud-images... or (TODO: should I not default enable for them?)
  cloud_image?
end