class Cyclid::API::Plugins::Digitalocean

Digitalocean builder. Uses the Digitalocean API to obtain a build host instance.

Public Class Methods

new() click to toggle source
# File lib/cyclid/plugins/builder/digitalocean.rb, line 34
def initialize
  @config = load_digitalocean_config(Cyclid.config.plugins)
  @client = DropletKit::Client.new(access_token: @config[:access_token])
end

Public Instance Methods

get(args = {}) click to toggle source

Create & return a build host

# File lib/cyclid/plugins/builder/digitalocean.rb, line 40
def get(args = {})
  args.symbolize_keys!

  Cyclid.logger.debug "digitalocean: args=#{args}"

  # If there is one, split the 'os' into a 'distro' and 'release'
  if args.key? :os
    match = args[:os].match(/\A(\w*)_(.*)\Z/)
    distro = match[1] if match
    release = match[2] if match
  else
    # No OS was specified; use the default
    # XXX Defaults should be configurable
    distro = 'ubuntu'
    release = 'trusty'
  end

  # Convert Debian & Ubuntu release codenames
  release_version = if distro == 'ubuntu' or distro == 'debian'
                      codename_to_version(release)
                    else
                      release
                    end

  begin
    # Find, or create, the SSH key Cyclid can use
    build_key = build_ssh_key

    # Create the Droplet
    droplet = DropletKit::Droplet.new(name: create_name,
                                      region: @config[:region],
                                      image: "#{distro}-#{release_version}-x64",
                                      size: @config[:size],
                                      ssh_keys: [build_key.id])
    created = @client.droplets.create droplet

    # Wait for the droplet to become 'active'
    created = wait_for_droplet(created)

    # Create a buildhost from the active Droplet details
    buildhost = DigitaloceanHost.new(name: created.name,
                                     host: created.networks.v4.first.ip_address,
                                     id: created.id,
                                     username: 'root',
                                     workspace: '/root',
                                     key: @config[:ssh_private_key],
                                     distro: distro,
                                     release: release)
  rescue StandardError => ex
    Cyclid.logger.error "couldn't get a build host from Digitalocean: #{ex}"
    raise "digitalocean failed: #{ex}"
  end

  Cyclid.logger.debug "digitalocean buildhost=#{buildhost.inspect}"
  return buildhost
end
release(_transport, buildhost) click to toggle source

Destroy the build host

# File lib/cyclid/plugins/builder/digitalocean.rb, line 98
def release(_transport, buildhost)
  @client.droplets.delete(id: buildhost[:id])
rescue StandardError => ex
  Cyclid.logger.error "Digitalcoean destroy timed out: #{ex}"
end

Private Instance Methods

build_ssh_key() click to toggle source
# File lib/cyclid/plugins/builder/digitalocean.rb, line 154
def build_ssh_key
  # Find the build key, if it exists
  build_key = nil
  all_keys = @client.ssh_keys.all
  all_keys.each do |key|
    build_key = key if key.name == @config[:ssh_key_name]
  end
  Cyclid.logger.debug "build_key=#{build_key.inspect}"

  # If the key doesn't exist, create it
  if build_key.nil?
    pubkey = File.read(@config[:ssh_public_key])
    key = DropletKit::SSHKey.new(name: @config[:ssh_key_name],
                                 public_key: pubkey)
    build_key = @client.ssh_keys.create(key)
    Cyclid.logger.debug "build_key=#{build_key.inspect}"
  end

  return build_key
end
codename_to_version(release) click to toggle source

Convert Ubuntu & Debian release codenames to Digitialocean approved release versions

# File lib/cyclid/plugins/builder/digitalocean.rb, line 137
def codename_to_version(release)
  versions = { 'precise' => '12-04',
               'trusty' => '14-04',
               'xenial' => '16-04',
               'yakkety' => '16-10',
               'wheezy' => '7',
               'jessie' => '8' }

  # No need to convert if it isn't a codename
  return release if release =~ /\A\d*[-\d*]/

  raise "don't know what version Ubuntu/Debian #{release} is" \
    unless versions.key? release

  return versions[release]
end
create_name() click to toggle source
# File lib/cyclid/plugins/builder/digitalocean.rb, line 197
def create_name
  base = @config[:instance_name]
  "#{base}-#{SecureRandom.hex(16)}"
end
load_digitalocean_config(config) click to toggle source

Load the config for the Digitalocean plugin and set defaults if they're not in the config

# File lib/cyclid/plugins/builder/digitalocean.rb, line 111
def load_digitalocean_config(config)
  config.symbolize_keys!

  do_config = config[:digitalocean] || {}
  do_config.symbolize_keys!
  Cyclid.logger.debug "config=#{do_config}"

  raise 'the Digitalocean API access token must be provided' \
    unless do_config.key? :access_token

  # Set defaults
  do_config[:region] = 'nyc1' unless do_config.key? :region
  do_config[:size] = '512mb' unless do_config.key? :size
  do_config[:ssh_private_key] = File.join(%w(/ etc cyclid id_rsa_build)) \
    unless do_config.key? :ssh_private_key
  do_config[:ssh_public_key] = File.join(%w(/ etc cyclid id_rsa_build.pub)) \
    unless do_config.key? :ssh_public_key
  do_config[:ssh_key_name] = 'cyclid-build' \
    unless do_config.key? :ssh_key_name
  do_config[:instance_name] = 'cyclid-build' \
    unless do_config.key? :instance_name

  return do_config
end
wait_for_droplet(created) click to toggle source
# File lib/cyclid/plugins/builder/digitalocean.rb, line 175
def wait_for_droplet(created)
  # Wait for the droplet to become active; wait a maximum of 1 minute,
  # polling every 2 seconds.
  Cyclid.logger.debug "Waiting for instance #{created.id} to become 'active'..."
  29.times do
    created = @client.droplets.find(id: created.id.to_s)
    break if created.status == 'active'

    sleep 2
  end
  Cyclid.logger.debug "created=#{created.inspect}"

  unless created.status == 'active'
    @client.droplets.delete(id: created.id)

    raise 'failed to create build host: did not become active within 1 minute. ' \
          "Status is #{created.status}" \
  end

  return created
end