hyperkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,123 @@
1
+ # -*- mode: ruby -*-
2
+ # vi: set ft=ruby :
3
+
4
+ # All Vagrant configuration is done below. The "2" in Vagrant.configure
5
+ # configures the configuration version (we support older styles for
6
+ # backwards compatibility). Please don't change it unless you know what
7
+ # you're doing.
8
+ Vagrant.configure(2) do |config|
9
+ # The most common configuration options are documented and commented below.
10
+ # For a complete reference, please see the online documentation at
11
+ # https://docs.vagrantup.com.
12
+
13
+ # Every Vagrant development environment requires a box. You can search for
14
+ # boxes at https://atlas.hashicorp.com/search.
15
+ config.vm.box = "ubuntu/trusty64"
16
+
17
+ config.vm.define "lxd1" do |lxd1|
18
+ lxd1.vm.box = "ubuntu/trusty64"
19
+ lxd1.vm.network "forwarded_port", guest: 8443, host: 8443
20
+ lxd1.vm.network "private_network", ip: "192.168.103.101"
21
+
22
+ lxd1.vm.provider "virtualbox" do |vb|
23
+ vb.memory = "2048"
24
+ disk_file = File.expand_path("../.vagrant/disks/lxd1-disk1.vdi", __FILE__)
25
+
26
+ if ! File.exist?(disk_file)
27
+ vb.customize ['createhd', '--filename', disk_file, '--size', 1024 * 1024]
28
+ vb.customize ['storageattach', :id, '--storagectl', 'SATAController', '--port', 1, '--device', 0, '--type', 'hdd', '--medium', disk_file]
29
+ end
30
+
31
+ end
32
+
33
+ lxd1.vm.provision "ansible" do |ansible|
34
+ ansible.playbook = File.expand_path("../spec/vagrant/lxd-playbook.yml", __FILE__)
35
+ ansible.extra_vars = {
36
+ lxd_port: 8443,
37
+ lxd_trust_password: "lxd1"
38
+ }
39
+ end
40
+
41
+ end
42
+
43
+ config.vm.define "lxd2" do |lxd2|
44
+ lxd2.vm.box = "ubuntu/trusty64"
45
+ lxd2.vm.network "forwarded_port", guest: 8444, host: 8444
46
+ lxd2.vm.network "private_network", ip: "192.168.103.102"
47
+
48
+ lxd2.vm.provider "virtualbox" do |vb|
49
+ vb.memory = "256"
50
+
51
+ disk_file = File.expand_path("../.vagrant/disks/lxd2-disk1.vdi", __FILE__)
52
+
53
+ if ! File.exist?(disk_file)
54
+ vb.customize ['createhd', '--filename', disk_file, '--size', 1024 * 1024]
55
+ vb.customize ['storageattach', :id, '--storagectl', 'SATAController', '--port', 1, '--device', 0, '--type', 'hdd', '--medium', disk_file]
56
+ end
57
+ end
58
+
59
+ lxd2.vm.provision "ansible" do |ansible|
60
+ ansible.playbook = File.expand_path("../spec/vagrant/lxd-playbook.yml", __FILE__)
61
+ ansible.extra_vars = {
62
+ lxd_port: 8443,
63
+ lxd_trust_password: "lxd2"
64
+ }
65
+ end
66
+
67
+ end
68
+
69
+ # Disable automatic box update checking. If you disable this, then
70
+ # boxes will only be checked for updates when the user runs
71
+ # `vagrant box outdated`. This is not recommended.
72
+ # config.vm.box_check_update = false
73
+
74
+ # Create a forwarded port mapping which allows access to a specific port
75
+ # within the machine from a port on the host machine. In the example below,
76
+ # accessing "localhost:8080" will access port 80 on the guest machine.
77
+ # config.vm.network "forwarded_port", guest: 80, host: 8080
78
+
79
+ # Create a private network, which allows host-only access to the machine
80
+ # using a specific IP.
81
+ # config.vm.network "private_network", ip: "192.168.33.10"
82
+
83
+ # Create a public network, which generally matched to bridged network.
84
+ # Bridged networks make the machine appear as another physical device on
85
+ # your network.
86
+ # config.vm.network "public_network"
87
+
88
+ # Share an additional folder to the guest VM. The first argument is
89
+ # the path on the host to the actual folder. The second argument is
90
+ # the path on the guest to mount the folder. And the optional third
91
+ # argument is a set of non-required options.
92
+ # config.vm.synced_folder "../data", "/vagrant_data"
93
+
94
+ # Provider-specific configuration so you can fine-tune various
95
+ # backing providers for Vagrant. These expose provider-specific options.
96
+ # Example for VirtualBox:
97
+ #
98
+ # config.vm.provider "virtualbox" do |vb|
99
+ # # Display the VirtualBox GUI when booting the machine
100
+ # vb.gui = true
101
+ #
102
+ # # Customize the amount of memory on the VM:
103
+ # vb.memory = "1024"
104
+ # end
105
+ #
106
+ # View the documentation for the provider you are using for more
107
+ # information on available options.
108
+
109
+ # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
110
+ # such as FTP and Heroku are also available. See the documentation at
111
+ # https://docs.vagrantup.com/v2/push/atlas.html for more information.
112
+ # config.push.define "atlas" do |push|
113
+ # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
114
+ # end
115
+
116
+ # Enable provisioning with a shell script. Additional provisioners such as
117
+ # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
118
+ # documentation for more information about their specific syntax and use.
119
+ # config.vm.provision "shell", inline: <<-SHELL
120
+ # sudo apt-get update
121
+ # sudo apt-get install -y apache2
122
+ # SHELL
123
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "hyperkit"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hyperkit/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hyperkit"
8
+ spec.version = Hyperkit::VERSION
9
+ spec.authors = ["Jeff Shantz"]
10
+ spec.email = ["hyperkit@jeffshantz.com"]
11
+
12
+ spec.summary = %q{Hyperkit is a flat API wrapper for LXD, the next-generation hypervisor}
13
+ spec.homepage = "https://github.com/jeffshantz/hyperkit"
14
+ spec.license = "MIT"
15
+
16
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
+ # delete this section to allow pushing this gem to any host.
18
+ #if spec.respond_to?(:metadata)
19
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
20
+ #else
21
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
+ #end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency "activesupport", "~> 4.2.6"
30
+ spec.add_dependency "sawyer"
31
+ spec.add_development_dependency "bundler", "~> 1.11"
32
+
33
+ end
@@ -0,0 +1,58 @@
1
+ ################################################################################
2
+ # #
3
+ # Based on Octokit #
4
+ # #
5
+ # Original Octokit license #
6
+ # ---------------------------------------------------------------------------- #
7
+ # Copyright (c) 2009-2016 Wynn Netherland, Adam Stacoviak, Erik Michaels-Ober #
8
+ # #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a #
10
+ # copy of this software and associated documentation files (the "Software"), #
11
+ # to deal in the Software without restriction, including without limitation #
12
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense, #
13
+ # and/or sell copies of the Software, and to permit persons to whom the #
14
+ # Software is furnished to do so, subject to the following conditions: #
15
+ # #
16
+ # The above copyright notice and this permission notice shall be included #
17
+ # in all copies or substantial portions of the Software. #
18
+ # ---------------------------------------------------------------------------- #
19
+ # #
20
+ ################################################################################
21
+
22
+ require 'hyperkit/version'
23
+ require 'hyperkit/client'
24
+ require 'hyperkit/default'
25
+
26
+ # Ruby toolkit for the LXD API.
27
+ # LXD - the next-generation container hypervisor for Linux
28
+ module Hyperkit
29
+
30
+ class << self
31
+ include Hyperkit::Configurable
32
+
33
+ # API client based on configured options {Configurable}
34
+ #
35
+ # @return [Hyperkit::Client] API wrapper
36
+ def client
37
+ return @client if defined?(@client) && @client.same_options?(options)
38
+ @client = Hyperkit::Client.new(options)
39
+ end
40
+
41
+ private
42
+
43
+ def respond_to_missing?(method_name, include_private=false)
44
+ client.respond_to?(method_name, include_private)
45
+ end
46
+
47
+ def method_missing(method_name, *args, &block)
48
+ if client.respond_to?(method_name)
49
+ return client.send(method_name, *args, &block)
50
+ end
51
+
52
+ super
53
+ end
54
+
55
+ end
56
+ end
57
+
58
+ Hyperkit.setup
@@ -0,0 +1,82 @@
1
+ ################################################################################
2
+ # #
3
+ # Modeled on Octokit::Client #
4
+ # #
5
+ # Original Octokit license #
6
+ # ---------------------------------------------------------------------------- #
7
+ # Copyright (c) 2009-2016 Wynn Netherland, Adam Stacoviak, Erik Michaels-Ober #
8
+ # #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a #
10
+ # copy of this software and associated documentation files (the "Software"), #
11
+ # to deal in the Software without restriction, including without limitation #
12
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense, #
13
+ # and/or sell copies of the Software, and to permit persons to whom the #
14
+ # Software is furnished to do so, subject to the following conditions: #
15
+ # #
16
+ # The above copyright notice and this permission notice shall be included #
17
+ # in all copies or substantial portions of the Software. #
18
+ # ---------------------------------------------------------------------------- #
19
+ # #
20
+ ################################################################################
21
+
22
+
23
+ require 'hyperkit/configurable'
24
+ require 'hyperkit/connection'
25
+ require 'hyperkit/client/certificates'
26
+ require 'hyperkit/client/containers'
27
+ require 'hyperkit/client/images'
28
+ require 'hyperkit/client/networks'
29
+ require 'hyperkit/client/operations'
30
+ require 'hyperkit/client/profiles'
31
+
32
+ module Hyperkit
33
+
34
+ # LXD client
35
+ # @see Hyperkit::Client::Certificates
36
+ # @see Hyperkit::Client::Containers
37
+ # @see Hyperkit::Client::Images
38
+ # @see Hyperkit::Client::Networks
39
+ # @see Hyperkit::Client::Operations
40
+ # @see Hyperkit::Client::Profiles
41
+ class Client
42
+
43
+ include Hyperkit::Configurable
44
+ include Hyperkit::Connection
45
+ include Hyperkit::Client::Certificates
46
+ include Hyperkit::Client::Containers
47
+ include Hyperkit::Client::Images
48
+ include Hyperkit::Client::Networks
49
+ include Hyperkit::Client::Operations
50
+ include Hyperkit::Client::Profiles
51
+
52
+ # Initialize a new Hyperkit client
53
+ #
54
+ # @param options [Hash] Any of the attributes listed in {Hyperkit::Configurable}
55
+ #
56
+ # @example Use a client with default options
57
+ # client = Hyperkit.client
58
+ #
59
+ # @example Create a new client and override the <code>api_endpoint</code>
60
+ # client = Hyperkit::Client.new(api_endpoint: "https://images.linuxcontainers.org:8443")
61
+ def initialize(options = {})
62
+
63
+ # Use options passed in, but fall back to module defaults
64
+ Hyperkit::Configurable.keys.each do |key|
65
+
66
+ # Allow user to explicitly override default values by passing 'key: nil'
67
+ next if options.has_key?(key) && options[key].nil?
68
+
69
+ if options.has_key?(key)
70
+ value = options[key]
71
+ else
72
+ value = Hyperkit.instance_variable_get(:"@#{key}")
73
+ end
74
+
75
+ instance_variable_set(:"@#{key}", value)
76
+ end
77
+
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,102 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+ require 'base64'
3
+
4
+ module Hyperkit
5
+
6
+ class Client
7
+
8
+ # Methods for the certificates API
9
+ #
10
+ # @see Hyperkit::Client
11
+ # @see https://github.com/lxc/lxd/blob/master/specs/rest-api.md
12
+ module Certificates
13
+
14
+ # List of trusted certificates on the server
15
+ #
16
+ # @return [Array<String>] An array of certificate fingerprints
17
+ #
18
+ # @example Get list of containers
19
+ # Hyperkit.certificates #=> [
20
+ # "c782c0f3530a04a5b2b78fc5292b7500aef1299370288b5eeb0450a6613a2c82",
21
+ # "b7720e1eb839056158cf65d182865491a0403f766983b95f5098d05911bbff89"
22
+ # ]
23
+ def certificates
24
+ response = get(certificates_path)
25
+ response.metadata.map { |path| path.split('/').last }
26
+ end
27
+
28
+ # Add a new trusted certificate to the server
29
+ #
30
+ # @param cert [String] Certificate contents in PEM format
31
+ # @param options [Hash] Additional data to be passed
32
+ # @option options [String] :name Optional name for the certificate. If not specified, the host in the TLS header for the request is used.
33
+ # @option options [String] :password The trust password for that server. Only required if untrusted.
34
+ # @return [Sawyer::Resource]
35
+ #
36
+ # @example Add trusted certificate
37
+ # Hyperkit.create_certificate(File.read("/tmp/cert.pem"))
38
+ #
39
+ # @example Add trusted certificate via untrusted client connection
40
+ # Hyperkit.create_certificate(File.read("/tmp/cert.pem"), password: "server-trust-password")
41
+ def create_certificate(cert, options={})
42
+ options = options.slice(:name, :password)
43
+ options = options.merge(type: "client", certificate: Base64.strict_encode64(OpenSSL::X509::Certificate.new(cert).to_der))
44
+ post(certificates_path, options).metadata
45
+ end
46
+
47
+ # Retrieve a trusted certificate from the server
48
+ #
49
+ # @param fingerprint [String] Fingerprint of the certificate to retrieve. Can be a prefix, as long as it is unambigous
50
+ # @return [Sawyer::Resource] Certificate information
51
+ #
52
+ # @example Retrieve a certificate
53
+ # Hyperkit.certificate("c782c0f3530a04a5b2b78fc5292b7500aef1299370288b5eeb0450a6613a2c82") #=> {
54
+ # :certificate => "-----BEGIN CERTIFICATE-----\nMIIEW...ceyg04=\n-----END CERTIFICATE-----\n",
55
+ # :fingerprint => "c782c0f3530a04a5b2b78fc5292b7500aef1299370288b5eeb0450a6613a2c82",
56
+ # :type => "client"
57
+ # }
58
+ #
59
+ # @example Retrieve a certificate by specifying a prefix of its fingerprint
60
+ # Hyperkit.certificate("c7") #=> {
61
+ # :certificate => "-----BEGIN CERTIFICATE-----\nMIIEW...ceyg04=\n-----END CERTIFICATE-----\n",
62
+ # :fingerprint => "c782c0f3530a04a5b2b78fc5292b7500aef1299370288b5eeb0450a6613a2c82",
63
+ # :type => "client"
64
+ # }
65
+ #
66
+ # @todo Write tests for the prefix
67
+ def certificate(fingerprint)
68
+ get(certificate_path(fingerprint)).metadata
69
+ end
70
+
71
+ # Delete a trusted certificate from the server
72
+ #
73
+ # @param fingerprint [String] Fingerprint of the certificate to retrieve. Can be a prefix, as long as it is unambigous
74
+ # @return [Sawyer::Resource]
75
+ #
76
+ # @example Delete a certificate
77
+ # Hyperkit.delete_certificate("c782c0f3530a04a5b2b78fc5292b7500aef1299370288b5eeb0450a6613a2c82")
78
+ #
79
+ # @example Delete a certificate by specifying a prefix of its fingerprint
80
+ # Hyperkit.delete_certificate("c7")
81
+ #
82
+ # @todo Write tests for the prefix
83
+ def delete_certificate(fingerprint)
84
+ delete(certificate_path(fingerprint)).metadata
85
+ end
86
+
87
+ private
88
+
89
+ def certificate_path(fingerprint)
90
+ File.join(certificates_path, fingerprint)
91
+ end
92
+
93
+ def certificates_path
94
+ "/1.0/certificates"
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+
@@ -0,0 +1,1100 @@
1
+ require 'active_support/core_ext/array/extract_options'
2
+ require 'shellwords'
3
+
4
+ module Hyperkit
5
+
6
+ class Client
7
+
8
+ # Methods for the containers API
9
+ #
10
+ # @see Hyperkit::Client
11
+ # @see https://github.com/lxc/lxd/blob/master/specs/rest-api.md
12
+ module Containers
13
+
14
+ # @!group Retrieval
15
+
16
+ # List of containers on the server (public or private)
17
+ #
18
+ # @return [Array<String>] An array of container names
19
+ #
20
+ # @example Get list of containers
21
+ # Hyperkit.containers #=> ["container1", "container2", "container3"]
22
+ def containers
23
+ response = get(containers_path)
24
+ response.metadata.map { |path| path.split('/').last }
25
+ end
26
+
27
+ # Get information on a container
28
+ #
29
+ # @param name [String] Container name
30
+ # @return [Sawyer::Resource] Container information
31
+ #
32
+ # @example Get information about a container
33
+ # Hyperkit.container("test-container") #=> {
34
+ # :architecture => "x86_64",
35
+ # :config => {
36
+ # :"volatile.base_image" => "097e75d6f7419d3a5e204d8125582f2d7bdd4ee4c35bd324513321c645f0c415",
37
+ # :"volatile.eth0.hwaddr" => "00:16:3e:24:5d:7a",
38
+ # :"volatile.eth0.name" => "eth0",
39
+ # :"volatile.last_state.idmap" =>
40
+ # "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":165536,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":165536,\"Nsid\":0,\"Maprange\":65536}]"
41
+ # },
42
+ # :created_at => 2016-03-18 20:55:26 UTC,
43
+ # :devices => {
44
+ # :root => {:path => "/", :type => "disk"}
45
+ # },
46
+ # :ephemeral => false,
47
+ # :expanded_config => {
48
+ # :"volatile.base_image" => "097e75d6f7419d3a5e204d8125582f2d7bdd4ee4c35bd324513321c645f0c415",
49
+ # :"volatile.eth0.hwaddr" => "00:16:3e:24:5d:7a",
50
+ # :"volatile.eth0.name" => "eth0",
51
+ # :"volatile.last_state.idmap" =>
52
+ # "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":165536,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":165536,\"Nsid\":0,\"Maprange\":65536}]"
53
+ # },
54
+ # :expanded_devices => {
55
+ # :eth0 => { :nictype => "bridged", :parent => "lxcbr0", :type => "nic"},
56
+ # :root => { :path => "/", :type => "disk"}
57
+ # },
58
+ # :name => "test-container",
59
+ # :profiles => ["default"],
60
+ # :stateful => false,
61
+ # :status => "Stopped",
62
+ # :status_code => 102
63
+ # }
64
+ def container(name)
65
+ get(container_path(name)).metadata
66
+ end
67
+
68
+ # @!endgroup
69
+
70
+ # @!group Creation
71
+
72
+ # Create a container from an image (local or remote). The container will
73
+ # be created in the <code>Stopped</code> state.
74
+ #
75
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
76
+ #
77
+ # @param name [String] Container name
78
+ # @param options [Hash] Additional data to be passed
79
+ # @option options [String] :alias Alias of the source image. <b>Either <code>:alias</code>, <code>:fingerprint</code>, <code>:properties</code>, or <code>empty: true</code> must be specified</b>.
80
+ # @option options [String] :architecture Architecture of the container (e.g. <code>x86_64</code>). By default, this will be obtained from the image metadata
81
+ # @option options [String] :certificate PEM certificate to use to authenticate with the remote server. If not specified, and the source image is private, the target LXD server's certificate is used for authentication. <b>This option is valid only when transferring an image from a remote server using the <code>:server</code> option.</b>
82
+ # @option options [Hash] :config Container configuration
83
+ # @option options [Boolean] :ephemeral Whether to make the container ephemeral (i.e. delete it when it is stopped; default: <code>false</code>)
84
+ # @option options [Boolean] :empty Whether to make an empty container (i.e. not from an image). Specifying <code>true</code> will cause LXD to create a container with no rootfs. That is, /var/lib/lxd/<container-name> will simply be an empty directly. One can then create a rootfs directory within this directory and populate it manually. This is useful when migrating LXC containers to LXD.
85
+ # @option options [String] :fingerprint SHA-256 fingerprint of the source image. This can be a prefix of a fingerprint, as long as it is unambiguous. <b>Either <code>:alias</code>, <code>:fingerprint</code>, <code>:properties</code>, or <code>empty: true</code> must be specified</b>.
86
+ # @option options [Array] :profiles List of profiles to be applied to the container (default: <code>[]</code>)
87
+ # @option options [String] :properties Properties of the source image. <b>Either <code>:alias</code>, <code>:fingerprint</code>, <code>:properties</code>, or <code>empty: true</code> must be specified</b>.
88
+ # @option options [String] :protocol Protocol to use in transferring the image (<code>lxd</code> or <code>simplestreams</code>; defaults to <code>lxd</code>). <b>This option is valid only when transferring an image from a remote server using the <code>:server</code> option.</b>
89
+ # @option options [String] :secret Secret to use to retrieve the image. <b>This option is valid only when transferring an image from a remote server using the <code>:server</code> option.</b>
90
+ # @option options [String] :server URL of remote server from which to obtain image. By default, the image will be obtained from the client's <code>api_endpoint</code>.
91
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
92
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
93
+ #
94
+ # @example Create container from image specified by alias
95
+ # Hyperkit.create_container("test-container", alias: "ubuntu/xenial/amd64")
96
+ #
97
+ # @example Create container from image specified by fingerprint
98
+ # Hyperkit.create_container("test-container",
99
+ # fingerprint: "097e75d6f7419d3a5e204d8125582f2d7bdd4ee4c35bd324513321c645f0c415")
100
+ #
101
+ # @example Create container from image specified by fingerprint prefix
102
+ # Hyperkit.create_container("test-container", fingerprint: "097")
103
+ #
104
+ # @example Create container based on most recent match of image properties
105
+ # Hyperkit.create_container("test-container",
106
+ # properties: { os: "ubuntu", release: "14.04", architecture: "x86_64" }
107
+ #
108
+ # @example Create an empty container
109
+ # Hyperkit.create_container("test-container", empty: true)
110
+ #
111
+ # @example Create container with custom configuration.
112
+ #
113
+ # # Set the MAC address of the container's eth0 device
114
+ # Hyperkit.create_container("test-container",
115
+ # alias: "ubuntu/xenial/amd64",
116
+ # config: {
117
+ # "volatile.eth0.hwaddr" => "aa:bb:cc:dd:ee:ff"
118
+ # }
119
+ # )
120
+ #
121
+ # @example Create container and apply profiles to it
122
+ # Hyperkit.create_container("test-container",
123
+ # alias: "ubuntu/xenial/amd64",
124
+ # profiles: ["migratable", "unconfined"]
125
+ # )
126
+ #
127
+ # @example Create container from a publicly-accessible remote image
128
+ # Hyperkit.create_container("test-container",
129
+ # server: "https://images.linuxcontainers.org:8443",
130
+ # alias: "ubuntu/xenial/amd64")
131
+ #
132
+ # @example Create container from a private remote image (authenticated by a secret)
133
+ # Hyperkit.create_container("test-container",
134
+ # server: "https://private.example.com:8443",
135
+ # alias: "ubuntu/xenial/amd64",
136
+ # secret: "shhhhh")
137
+ def create_container(name, options={})
138
+
139
+ source = container_source_attribute(options)
140
+ opts = options.except(:sync)
141
+
142
+ if ! opts[:empty] && source.empty?
143
+ raise Hyperkit::ImageIdentifierRequired.new("Specify source image by alias, fingerprint, or properties, or create an empty container with 'empty: true'")
144
+ end
145
+
146
+ if opts[:empty]
147
+ opts = empty_container_options(name, opts)
148
+ elsif options[:server]
149
+ opts = remote_image_container_options(name, source, opts)
150
+ else
151
+ opts = local_image_container_options(name, source, opts)
152
+ end
153
+
154
+ response = post(containers_path, opts).metadata
155
+ handle_async(response, options[:sync])
156
+ end
157
+
158
+ # Create a copy of an existing local container.
159
+ #
160
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
161
+ #
162
+ # @param source_name [String] Source container name
163
+ # @param target_name [String] Target container name
164
+ # @param options [Hash] Additional data to be passed
165
+ # @option options [String] :architecture Architecture of the container (e.g. <code>x86_64</code>). By default, this will be obtained from the image metadata
166
+ # @option options [Hash] :config Container configuration
167
+ # @option options [Boolean] :ephemeral Whether to make the container ephemeral (i.e. delete it when it is stopped; default: <code>false</code>)
168
+ # @option options [Array] :profiles List of profiles to be applied to the container (default: <code>[]</code>)
169
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
170
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
171
+ #
172
+ # @example Copy container
173
+ # Hyperkit.copy_container("existing", "new")
174
+ #
175
+ # @example Copy container and override its configuration.
176
+ #
177
+ # # Set the MAC address of the container's eth0 device
178
+ # Hyperkit.copy_container("existing", "new", config: {
179
+ # "volatile.eth0.hwaddr" => "aa:bb:cc:dd:ee:ff"
180
+ # }
181
+ # )
182
+ #
183
+ # @example Copy container and apply profiles to it
184
+ # Hyperkit.copy_container("existing", "new", profiles: ["migratable", "unconfined"])
185
+ #
186
+ # @example Create container from a publicly-accessible remote image
187
+ # Hyperkit.create_container("test-container",
188
+ # server: "https://images.linuxcontainers.org:8443",
189
+ # alias: "ubuntu/xenial/amd64")
190
+ def copy_container(source_name, target_name, options={})
191
+
192
+ opts = {
193
+ source: {
194
+ type: "copy",
195
+ source: source_name
196
+ }
197
+ }.merge(extract_container_options(target_name, options))
198
+
199
+ response = post(containers_path, opts).metadata
200
+ handle_async(response, options[:sync])
201
+
202
+ end
203
+
204
+ # @!endgroup
205
+
206
+ # @!group Editing
207
+
208
+ # Update the configuration of a container.
209
+ #
210
+ # Configuration is overwritten, not merged. Accordingly, clients should
211
+ # first call container to obtain the current configuration of a
212
+ # container. The resulting object should be modified and then passed to
213
+ # update_container.
214
+ #
215
+ # Note that LXD does not allow certain attributes to be changed (e.g.
216
+ # <code>status</code>, <code>status_code</code>, <code>stateful</code>,
217
+ # <code>name</code>, etc.) through this call.
218
+ #
219
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
220
+ #
221
+ # @param name [String] Container name
222
+ # @param config [Sawyer::Resource|Hash] Container configuration obtained from #container
223
+ # @param options [Hash] Additional data to be passed
224
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
225
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
226
+ #
227
+ # @example Add 'eth1' device to a container
228
+ # container = Hyperkit.container("test-container")
229
+ # container.devices.eth1 = {nictype: "bridged", parent: "lxcbr0", type: "nic"}
230
+ # Hyperkit.update_container("test-container", container)
231
+ #
232
+ # @example Change container to be ephemeral (i.e. it will be deleted when stopped)
233
+ # container = Hyperkit.container("test-container")
234
+ # container.ephemeral = true
235
+ # Hyperkit.update_container("test-container", container)
236
+ #
237
+ # @example Change container's AppArmor profile to 'unconfined'.
238
+ # container = Hyperkit.container("test-container")
239
+ #
240
+ # # Note: due to a bug in Sawyer::Resource, the following will fail
241
+ # container.config[:"raw.lxc"] = "lxc.aa_profile=unconfined"
242
+ #
243
+ # # Instead, convert 'config' to a Hash, and update the Hash
244
+ # container.config = container.config.to_hash
245
+ # container.config["raw.lxc"] = "lxc.aa_profile=unconfined"
246
+ #
247
+ # Hyperkit.update_container("test-container", container)
248
+ def update_container(name, config, options={})
249
+
250
+ config = config.to_hash
251
+
252
+ # Stringify values in the config hash, since LXD chokes on non-String values
253
+ if config[:config]
254
+ config[:config] = config[:config].inject({}){|h,(k,v)| h[k.to_s] = v.to_s; h}
255
+ end
256
+
257
+ response = put(container_path(name), config).metadata
258
+ handle_async(response, options[:sync])
259
+ end
260
+
261
+ # Rename a container
262
+ #
263
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
264
+ #
265
+ # @param old_name [String] Existing container name
266
+ # @param new_name [String] New container name
267
+ # @param options [Hash] Additional data to be passed
268
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
269
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
270
+ #
271
+ # @example Rename container "test" to "test2"
272
+ # Hyperkit.rename_container("test", "test2")
273
+ def rename_container(old_name, new_name, options={})
274
+ response = post(container_path(old_name), { "name": new_name }).metadata
275
+ handle_async(response, options[:sync])
276
+ end
277
+
278
+ # @!endgroup
279
+
280
+ # @!group Commands
281
+
282
+ # Execute a command in a container
283
+ #
284
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
285
+ #
286
+ # @param container [String] Container name
287
+ # @param command [Array|String] Command to execute
288
+ # @param options [Hash] Additional data to be passed
289
+ # @option options [Hash] :environment Environment variables to set prior to command execution
290
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
291
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
292
+ #
293
+ # @example Run a command (passed as a string) in container "test-container"
294
+ # Hyperkit.execute_command("test-container", "echo 'hello world'")
295
+ #
296
+ # @example Run a command (passed as an array) in container "test-container"
297
+ # Hyperkit.execute_command("test-container",
298
+ # ["bash", "-c", "echo 'hello world' > /tmp/test.txt"]
299
+ # )
300
+ #
301
+ # @example Run a command and pass environment variables
302
+ # Hyperkit.execute_command("test-container",
303
+ # "/bin/sh -c 'echo \"$MYVAR\" $MYVAR2 > /tmp/test.txt'",
304
+ # environment: {
305
+ # MYVAR: "hello world",
306
+ # MYVAR2: 42
307
+ # }
308
+ # )
309
+ def execute_command(container, command, options={})
310
+
311
+ opts = options.slice(:environment)
312
+ command = Shellwords.shellsplit(command) if command.is_a?(String)
313
+
314
+ # Stringify any environment values since LXD croaks on non-String values
315
+ if opts[:environment]
316
+ opts[:environment] = opts[:environment].inject({}){|h,(k,v)| h[k.to_s] = v.to_s; h}
317
+ end
318
+
319
+ response = post(File.join(container_path(container), "exec"), {
320
+ command: command,
321
+ environment: opts[:environment] || {},
322
+ "wait-for-websocket" => false,
323
+ interactive: false
324
+ }).metadata
325
+
326
+ handle_async(response, options[:sync])
327
+
328
+ end
329
+
330
+ alias_method :run_command, :execute_command
331
+
332
+ # @!endgroup
333
+
334
+ # @!group Deletion
335
+
336
+ # Delete a container. Throws an error if the container is running.
337
+ #
338
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
339
+ #
340
+ # @param name [String] Container name
341
+ # @param options [Hash] Additional data to be passed
342
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
343
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
344
+ #
345
+ # @example Delete container "test"
346
+ # Hyperkit.delete_container("test")
347
+ #
348
+ def delete_container(name, options={})
349
+ response = delete(container_path(name)).metadata
350
+ handle_async(response, options[:sync])
351
+ end
352
+
353
+ # @!endgroup
354
+
355
+ # @!group State
356
+
357
+ # Retrieve the current state of a container
358
+ #
359
+ # @param name [String] Container name
360
+ # @return [Sawyer::Resource] Container state
361
+ #
362
+ # @example Get container state
363
+ # Hyperkit.container_state("test-container") #=> {
364
+ # }
365
+ def container_state(name)
366
+ get(container_state_path(name)).metadata
367
+ end
368
+
369
+ # Start a container
370
+ #
371
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
372
+ #
373
+ # @param name [String] Container name
374
+ # @param options [Hash] Additional data to be passed
375
+ # @option options [Boolean] :stateful Whether to restore previously saved runtime state (default: <code>false</false>)
376
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
377
+ # @option options [Fixnum] :timeout Time after which the operation is considered to have failed (default: no timeout)
378
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
379
+ #
380
+ # @example Start container
381
+ # Hyperkit.start_container("test")
382
+ #
383
+ # @example Start container and restore previously saved runtime state
384
+ # # Stop the container and save its runtime state
385
+ # Hyperkit.stop_container("test", stateful: true)
386
+ #
387
+ # # Start the container and restore its runtime state
388
+ # Hyperkit.start_container("test", stateful: true)
389
+ #
390
+ # @example Start container with a timeout
391
+ # Hyperkit.start_container("test", timeout: 30)
392
+ def start_container(name, options={})
393
+ opts = options.slice(:stateful, :timeout)
394
+ response = put(container_state_path(name), opts.merge(action: "start")).metadata
395
+ handle_async(response, options[:sync])
396
+ end
397
+
398
+ # Stop a container
399
+ #
400
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
401
+ #
402
+ # @param name [String] Container name
403
+ # @param options [Hash] Additional data to be passed
404
+ # @option options [Boolean] :force Whether to force the operation by killing the container
405
+ # @option options [Boolean] :stateful Whether to restore previously saved runtime state (default: <code>false</false>)
406
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
407
+ # @option options [Fixnum] :timeout Time after which the operation is considered to have failed (default: no timeout)
408
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
409
+ #
410
+ # @example Stop container
411
+ # Hyperkit.stop_container("test")
412
+ #
413
+ # @example Stop container and save its runtime state
414
+ # # Stop the container and save its runtime state
415
+ # Hyperkit.stop_container("test", stateful: true)
416
+ #
417
+ # # Start the container and restore its runtime state
418
+ # Hyperkit.start_container("test", stateful: true)
419
+ #
420
+ # @example Stop the container forcefully (i.e. kill it)
421
+ # Hyperkit.stop_container("test", force: true)
422
+ def stop_container(name, options={})
423
+ opts = options.slice(:force, :stateful, :timeout)
424
+ response = put(container_state_path(name), opts.merge(action: "stop")).metadata
425
+ handle_async(response, options[:sync])
426
+ end
427
+
428
+ # Restart a running container
429
+ #
430
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
431
+ #
432
+ # @param name [String] Container name
433
+ # @param options [Hash] Additional data to be passed
434
+ # @option options [Boolean] :force Whether to force the operation by killing the container
435
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
436
+ # @option options [Fixnum] :timeout Time after which the operation is considered to have failed (default: no timeout)
437
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
438
+ #
439
+ # @example Restart container
440
+ # Hyperkit.restart_container("test")
441
+ #
442
+ # @example Restart container forcefully
443
+ # Hyperkit.restart_container("test", force: true)
444
+ #
445
+ # @example Restart container with timeout
446
+ # Hyperkit.restart_container("test", timeout: 30)
447
+ def restart_container(name, options={})
448
+ opts = options.slice(:force, :timeout)
449
+ response = put(container_state_path(name), opts.merge(action: "restart")).metadata
450
+ handle_async(response, options[:sync])
451
+ end
452
+
453
+ # Freeze (suspend) a running container
454
+ #
455
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
456
+ #
457
+ # @param name [String] Container name
458
+ # @param options [Hash] Additional data to be passed
459
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
460
+ # @option options [Fixnum] :timeout Time after which the operation is considered to have failed (default: no timeout)
461
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
462
+ #
463
+ # @example Suspend container
464
+ # Hyperkit.freeze_container("test")
465
+ #
466
+ # @example Suspend container with timeout
467
+ # Hyperkit.freeze_container("test", timeout: 30)
468
+ def freeze_container(name, options={})
469
+ opts = options.slice(:timeout)
470
+ response = put(container_state_path(name), opts.merge(action: "freeze")).metadata
471
+ handle_async(response, options[:sync])
472
+ end
473
+
474
+ alias_method :pause_container, :freeze_container
475
+ alias_method :suspend_container, :freeze_container
476
+
477
+ # Unfreeze (resume) a frozen container
478
+ #
479
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
480
+ #
481
+ # @param name [String] Container name
482
+ # @param options [Hash] Additional data to be passed
483
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
484
+ # @option options [Fixnum] :timeout Time after which the operation is considered to have failed (default: no timeout)
485
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
486
+ #
487
+ # @example Resume container
488
+ # Hyperkit.unfreeze_container("test")
489
+ #
490
+ # @example Resume container with timeout
491
+ # Hyperkit.unfreeze_container("test", timeout: 30)
492
+ def unfreeze_container(name, options={})
493
+ opts = options.slice(:timeout)
494
+ response = put(container_state_path(name), opts.merge(action: "unfreeze")).metadata
495
+ handle_async(response, options[:sync])
496
+ end
497
+
498
+ alias_method :resume_container, :unfreeze_container
499
+
500
+ # @!endgroup
501
+
502
+ # @!group Migration
503
+
504
+ # Prepare to migrate a container or snapshot. Generates source data to be passed to {#migrate}.
505
+ #
506
+ # Note that CRIU must be installed on the server to migrate a running container, or LXD will
507
+ # return a 500 error. On Ubuntu, you can install it with
508
+ # <code>sudo apt-get install criu</code>.
509
+ #
510
+ # @param name [String] Container name
511
+ # @return [Sawyer::Resource] Source data to be passed to {#migrate}
512
+ #
513
+ # @example Retrieve migration source data for container "test"
514
+ # Hyperkit.init_migration("test") #=> {
515
+ # :architecture => "x86_64",
516
+ # :config => {
517
+ # :"volatile.base_image" => "b41f6b96f103335eafbf38ba65488eda66b05b08b590130e473803631d66ff38",
518
+ # :"volatile.eth0.hwaddr" => "00:16:3e:e9:d5:5c",
519
+ # :"volatile.last_state.idmap" =>
520
+ # "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":231072,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":231072,\"Nsid\":0,\"Maprange\":65536}]"
521
+ # },
522
+ # :profiles => ["default"],
523
+ # :websocket => {
524
+ # :url => "https://192.168.103.101:8443/1.0/operations/a30aca8e-8ff3-4437-b1da-bb28b43ee876",
525
+ # :secrets => {
526
+ # :control => "a6f8d21ebfe9ec76bf56585c98fd6d700fd43edee513ce61e48e1abeef479106",
527
+ # :criu => "c8601ec0d07f97f206835dde5783640c08640e9b27e45624d8555546b0cca327",
528
+ # :fs => "ddf9d064331b9f3728d098873a8a89a7742b8e656f2cd0815f0aee4777ff2b54"
529
+ # }
530
+ # },
531
+ # :certificate => "source server SSL certificate"
532
+ # }
533
+ #
534
+ # @example Retrieve migration source data for snapshot "snap" of container "test"
535
+ # Hyperkit.init_migration("test", "snap") #=> {
536
+ # :architecture => "x86_64",
537
+ # :config => {
538
+ # :"volatile.apply_template" => "create",
539
+ # :"volatile.base_image" => "b41f6b96f103335eafbf38ba65488eda66b05b08b590130e473803631d66ff38",
540
+ # :"volatile.eth0.hwaddr" => "00:16:3e:e9:d5:5c",
541
+ # :"volatile.last_state.idmap" =>
542
+ # "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":231072,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":231072,\"Nsid\":0,\"Maprange\":65536}]"
543
+ # },
544
+ # :profiles => ["default"],
545
+ # :websocket => {
546
+ # :url => "https://192.168.103.101:8443/1.0/operations/a30aca8e-8ff3-4437-b1da-bb28b43ee876",
547
+ # :secrets => {
548
+ # :control => "a6f8d21ebfe9ec76bf56585c98fd6d700fd43edee513ce61e48e1abeef479106",
549
+ # :criu => "c8601ec0d07f97f206835dde5783640c08640e9b27e45624d8555546b0cca327",
550
+ # :fs => "ddf9d064331b9f3728d098873a8a89a7742b8e656f2cd0815f0aee4777ff2b54"
551
+ # }
552
+ # },
553
+ # :certificate => "source server SSL certificate"
554
+ # }
555
+ def init_migration(container, snapshot=nil)
556
+
557
+ if snapshot
558
+ url = snapshot_path(container, snapshot)
559
+ source = snapshot(container, snapshot)
560
+ else
561
+ url = container_path(container)
562
+ source = container(container)
563
+ end
564
+
565
+ response = post(url, { "migration": true })
566
+ agent = response.agent
567
+
568
+ source_data = {
569
+ architecture: source.architecture,
570
+ config: source.config.to_hash,
571
+ profiles: source.profiles,
572
+ websocket: {
573
+ url: File.join(api_endpoint, response.operation),
574
+ secrets: response.metadata.metadata.to_hash,
575
+ },
576
+ certificate: get("/1.0").metadata.environment.certificate,
577
+ snapshot: ! snapshot.nil?
578
+ }
579
+
580
+ Sawyer::Resource.new(response.agent, source_data)
581
+ end
582
+
583
+ # Migrate a remote container or snapshot to the server
584
+ #
585
+ # Note that CRIU must be installed on the server to migrate a running container, or LXD will
586
+ # return a 500 error. On Ubuntu, you can install it with
587
+ # <code>sudo apt-get install criu</code>.
588
+ #
589
+ # Also note that, unless overridden with the <code>profiles</code> parameter, if the source
590
+ # container has profiles applied to it that do not exist on the target LXD instance, this
591
+ # method will throw an exception.
592
+ #
593
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
594
+ #
595
+ # @param source [Sawyer::Resource] Source data retrieve from the remote server with {#init_migration}
596
+ # @param dest_name [String] Name of the new container
597
+ # @param options [Hash] Additional data to be passed
598
+ # @option options [String] :architecture Architecture of the container (e.g. <code>x86_64</code>). By default, this will be obtained from the image metadata
599
+ # @option options [String] :certificate PEM certificate of the source server. If not specified, defaults to the certificate returned by the source server in the <code>source</code> parameter.
600
+ # @option options [Hash] :config Container configuration
601
+ # @option options [Boolean] :ephemeral Whether to make the container ephemeral (i.e. delete it when it is stopped; default: <code>false</code>)
602
+ # @option options [Boolean] :move Whether the container is being moved (<code>true</code>) or copied (<code>false</code>). Note that this does not actually delete the container from the remote LXD instance. Specifying <code>move: true</code> prevents regenerating volatile data (such as a container's MAC addresses), while <code>move: false</code> will regenerate all of this data. Defaults to <code>false</code> (a copy)
603
+ # @option options [Array] :profiles List of profiles to be applied to the container (default: <code>[]</code>)
604
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
605
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
606
+ #
607
+ # @example Migrate container from remote instance
608
+ # remote_lxd = Hyperkit::Client.new(api_endpoint: "remote.example.com")
609
+ # source_data = remote_lxd.init_migration("remote-container")
610
+ # Hyperkit.migrate(source_data, "new-container")
611
+ #
612
+ # @example Migrate container and do not regenerate volatile data (e.g. MAC addresses)
613
+ # remote_lxd = Hyperkit::Client.new(api_endpoint: "remote.example.com")
614
+ # source_data = remote_lxd.init_migration("remote-container")
615
+ # Hyperkit.migrate(source_data, "new-container", move: true)
616
+ #
617
+ # @example Migrate container and override its profiles
618
+ # remote_lxd = Hyperkit::Client.new(api_endpoint: "remote.example.com")
619
+ # source_data = remote_lxd.init_migration("remote-container")
620
+ # Hyperkit.migrate(source_data, "new-container", profiles: %w[test-profile1 test-profile2])
621
+ #
622
+ # @example Migrate a snapshot
623
+ # remote_lxd = Hyperkit::Client.new(api_endpoint: "remote.example.com")
624
+ # source_data = remote_lxd.init_migration("remote-container", "remote-snapshot")
625
+ # Hyperkit.migrate(source_data, "new-container", profiles: %w[test-profile1 test-profile2])
626
+ def migrate(source, dest_name, options={})
627
+
628
+ opts = {
629
+ name: dest_name,
630
+ architecture: options[:architecture] || source.architecture,
631
+ source: {
632
+ type: "migration",
633
+ mode: "pull",
634
+ operation: source.websocket.url,
635
+ certificate: options[:certificate] || source.certificate,
636
+ secrets: source.websocket.secrets.to_hash
637
+ }
638
+ }
639
+
640
+ if ! source.snapshot
641
+ opts["base-image"] = source.config["volatile.base_image"]
642
+ opts[:config] = options[:config] || source.config.to_hash
643
+
644
+ # If we're only copying the container, and configuration was explicitly
645
+ # overridden, then remove the volatile entries
646
+ if ! options[:move] && ! options.has_key?(:config)
647
+ opts[:config].delete_if { |k,v| k.to_s.start_with?("volatile") }
648
+ end
649
+
650
+ else
651
+ opts[:config] = options[:config] || {}
652
+ end
653
+
654
+ if options.has_key?(:profiles)
655
+ opts[:profiles] = options[:profiles]
656
+ else
657
+
658
+ dest_profiles = profiles()
659
+
660
+ if (source.profiles - dest_profiles).empty?
661
+ opts[:profiles] = source.profiles
662
+ else
663
+ raise Hyperkit::MissingProfiles.new("Not all profiles applied to source container exist on the target LXD instance")
664
+ end
665
+
666
+ end
667
+
668
+ if options.has_key?(:ephemeral)
669
+ opts[:ephemeral] = options[:ephemeral]
670
+ else
671
+ opts[:ephemeral] = !! source.ephemeral
672
+ end
673
+
674
+ response = post(containers_path, opts).metadata
675
+ handle_async(response, options[:sync])
676
+ end
677
+
678
+ # @!endgroup
679
+
680
+ # @!group Snapshots
681
+
682
+ # List of snapshots for a container
683
+ #
684
+ # @param container [String] Container name
685
+ # @return [Array<String>] An array of snapshot names
686
+ #
687
+ # @example Get list of snapshots for container "test"
688
+ # Hyperkit.snapshots("test") #=> ["snap1", "snap2", "snap3"]
689
+ def snapshots(container)
690
+ response = get snapshots_path(container)
691
+ response.metadata.map { |path| path.split('/').last }
692
+ end
693
+
694
+ # Get information on a snapshot
695
+ #
696
+ # @param container [String] Container name
697
+ # @param Snapshot [String] Snapshot name
698
+ # @return [Sawyer::Resource] Snapshot information
699
+ #
700
+ # @example Get information about a snapshot
701
+ # Hyperkit.snapshot("test-container", "test-snapshot") #=> {
702
+ # :architecture => "x86_64",
703
+ # :config => {
704
+ # :"volatile.apply_template" => "create",
705
+ # :"volatile.base_image" => "097e75d6f7419d3a5e204d8125582f2d7bdd4ee4c35bd324513321c645f0c415",
706
+ # :"volatile.eth0.hwaddr" => "00:16:3e:24:5d:7a",
707
+ # :"volatile.eth0.name" => "eth0",
708
+ # :"volatile.last_state.idmap" =>
709
+ # "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":165536,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":165536,\"Nsid\":0,\"Maprange\":65536}]"
710
+ # },
711
+ # :created_at => 2016-03-18 20:55:26 UTC,
712
+ # :devices => {
713
+ # :root => {:path => "/", :type => "disk"}
714
+ # },
715
+ # :ephemeral => false,
716
+ # :expanded_config => {
717
+ # :"volatile.apply_template" => "create",
718
+ # :"volatile.base_image" => "097e75d6f7419d3a5e204d8125582f2d7bdd4ee4c35bd324513321c645f0c415",
719
+ # :"volatile.eth0.hwaddr" => "00:16:3e:24:5d:7a",
720
+ # :"volatile.eth0.name" => "eth0",
721
+ # :"volatile.last_state.idmap" =>
722
+ # "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":165536,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":165536,\"Nsid\":0,\"Maprange\":65536}]"
723
+ # },
724
+ # :expanded_devices => {
725
+ # :eth0 => { :name => "eth0", :nictype => "bridged", :parent => "lxcbr0", :type => "nic"},
726
+ # :root => { :path => "/", :type => "disk"}
727
+ # },
728
+ # :name => "test-container/test-snapshot",
729
+ # :profiles => ["default"],
730
+ # :stateful => false
731
+ # }
732
+ def snapshot(container, snapshot)
733
+ get(snapshot_path(container, snapshot)).metadata
734
+ end
735
+
736
+ # Create a snapshot of a container
737
+ #
738
+ # If <code>stateful: true</code> is passed when creating a snapshot of a
739
+ # running container, the container's runtime state will be stored in the
740
+ # snapshot. Note that CRIU must be installed on the server to create a
741
+ # stateful snapshot, or LXD will return a 500 error. On Ubuntu, you can
742
+ # install it with
743
+ # <code>sudo apt-get install criu</code>.
744
+ #
745
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
746
+ #
747
+ # @param container [String] Container name
748
+ # @param snapshot [String] Snapshot name
749
+ # @param options [Hash] Additional data to be passed
750
+ # @option options [Boolean] :stateful Whether to save runtime state for a running container (requires CRIU on the server; default: <code>false</false>)
751
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
752
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
753
+ #
754
+ # @example Create stateless snapshot for container 'test'
755
+ # Hyperkit.create_snapshot("test", "snap1")
756
+ #
757
+ # @example Create snapshot and save runtime state for running container 'test'
758
+ # Hyperkit.create_snapshot("test", "snap1", stateful: true)
759
+ def create_snapshot(container, snapshot, options={})
760
+ opts = options.slice(:stateful)
761
+ opts[:name] = snapshot
762
+ response = post(snapshots_path(container), opts).metadata
763
+ handle_async(response, options[:sync])
764
+ end
765
+
766
+ # Delete a snapshot
767
+ #
768
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
769
+ #
770
+ # @param container [String] Container name
771
+ # @param snapshot [String] Snapshot name
772
+ # @param options [Hash] Additional data to be passed
773
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
774
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
775
+ #
776
+ # @example Delete snapshot "snap" from container "test"
777
+ # Hyperkit.delete_snapshot("test", "snap")
778
+ #
779
+ def delete_snapshot(container, snapshot, options={})
780
+ response = delete(snapshot_path(container, snapshot)).metadata
781
+ handle_async(response, options[:sync])
782
+ end
783
+
784
+ # Rename a snapshot
785
+ #
786
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
787
+ #
788
+ # @param container [String] Container name
789
+ # @param old_name [String] Existing snapshot name
790
+ # @param new_name [String] New snapshot name
791
+ # @param options [Hash] Additional data to be passed
792
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
793
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
794
+ #
795
+ # @example Rename snapshot "test/snap1" to "snap2"
796
+ # Hyperkit.rename_snapshot("test", "snap1", "snap2")
797
+ def rename_snapshot(container, old_name, new_name, options={})
798
+ response = post(snapshot_path(container, old_name), { "name": new_name }).metadata
799
+ handle_async(response, options[:sync])
800
+ end
801
+
802
+ # Restore a snapshot
803
+ #
804
+ # @async This method is asynchronous. See {Hyperkit::Configurable#auto_sync} for more information.
805
+ #
806
+ # @param container [String] Container name
807
+ # @param snapshot [String] Name of snapshot to restore
808
+ # @param options [Hash] Additional data to be passed
809
+ # @option options [Boolean] :sync If <code>false</code>, returns an asynchronous operation that must be passed to {Hyperkit::Client::Operations#wait_for_operation}. If <code>true</code>, automatically waits and returns the result of the operation. Defaults to value of {Hyperkit::Configurable#auto_sync}.
810
+ # @return [Sawyer::Resource] Operation or result, depending value of <code>:sync</code> parameter and/or {Hyperkit::Client::auto_sync}
811
+ #
812
+ # @example Restore container "test" back to snapshot "snap1"
813
+ # Hyperkit.restore_snapshot("test", "snap1")
814
+ def restore_snapshot(container, snapshot, options={})
815
+ response = put(container_path(container), { "restore": snapshot }).metadata
816
+ handle_async(response, options[:sync])
817
+ end
818
+
819
+ alias_method :revert_to_snapshot, :restore_snapshot
820
+
821
+ # @!endgroup
822
+
823
+ # @!group Files
824
+
825
+ # Read the contents of a file in a container
826
+ #
827
+ # @param container [String] Container name
828
+ # @param file [String] Full path to a file within the container
829
+ # @return [String] The contents of the file
830
+ #
831
+ # @example Read the file /etc/hostname in container "test"
832
+ # Hyperkit.read_file("test", "/etc/hostname") #=> "test-container.example.com"
833
+ def read_file(container, file)
834
+ get(file_path(container, file), url_encode: false)
835
+ end
836
+
837
+ # Copy a file from a container to the local system. The file will be
838
+ # written with the same permissions assigned to it in the container.
839
+ #
840
+ # @param container [String] Container name
841
+ # @param source_file [String] Full path to a file within the container
842
+ # @param dest_file [String] Full path of desired output file (will be created/overwritten)
843
+ # @return [String] Full path to the local output file
844
+ #
845
+ # @example Copy /etc/passwd in container "test" to the local file /tmp/passwd
846
+ # Hyperkit.pull_file("test", "/etc/passwd", "/tmp/passwd") #=> "/tmp/passwd"
847
+ def pull_file(container, source_file, dest_file)
848
+ contents = get(file_path(container, source_file), url_encode: false)
849
+ headers = last_response.headers
850
+
851
+ File.open(dest_file, "wb") do |f|
852
+ f.write(contents)
853
+ end
854
+
855
+ if headers["x-lxd-mode"]
856
+ File.chmod(headers["x-lxd-mode"].to_i(8), dest_file)
857
+ end
858
+
859
+ dest_file
860
+
861
+ end
862
+
863
+ # Write to a file in a container
864
+ #
865
+ # @param container [String] Container name
866
+ # @param dest_file [String] Path to the output file in the container
867
+ # @param options [Hash] Additional data to be passed
868
+ # @option options [Fixnum] :uid Owner to assign to the file
869
+ # @option options [Fixnum] :gid Group to assign to the file
870
+ # @option options [Fixnum] :mode File permissions (in octal) to assign to the file
871
+ # @option options [Fixnum] :content Content to write to the file (if no block given)
872
+ # @yieldparam io [StringIO] IO to be used to write to the file from a block
873
+ # @return [Sawyer::Resource]
874
+ #
875
+ # @example Write string "hello" to /tmp/test.txt in container test-container
876
+ # Hyperkit.write_file("test-container", "/tmp/test.txt", content: "hello")
877
+ #
878
+ # @example Write to file using a block
879
+ # Hyperkit.write_file("test-container", "/tmp/test.txt") do |io|
880
+ # io.print "Hello "
881
+ # io.puts "world"
882
+ # end
883
+ #
884
+ # @example Assign uid, gid, and mode to a file:
885
+ # Hyperkit.write_file("test-container",
886
+ # "/tmp/test.txt",
887
+ # content: "hello",
888
+ # uid: 1000,
889
+ # gid: 1000,
890
+ # mode: 0644
891
+ # )
892
+ def write_file(container, dest_file, options={}, &block)
893
+
894
+ headers = { "Content-Type" => "application/octet-stream" }
895
+ headers["X-LXD-uid"] = options[:uid].to_s if options[:uid]
896
+ headers["X-LXD-gid"] = options[:gid].to_s if options[:gid]
897
+ headers["X-LXD-mode"] = options[:mode].to_s(8).rjust(4, "0") if options[:mode]
898
+
899
+ if ! block_given?
900
+ content = options[:content].to_s
901
+ else
902
+ io = StringIO.new
903
+ yield io
904
+ io.rewind
905
+ content = io.read
906
+ end
907
+
908
+ post(file_path(container, dest_file), {
909
+ raw_body: content,
910
+ headers: headers
911
+ })
912
+
913
+ end
914
+
915
+ # Copy a file from the local system to container
916
+ #
917
+ # @param container [String] Container name
918
+ # @param source_file [String] Full path to a file within the container
919
+ # @param dest_file [String] Full path of desired output file (will be created/overwritten)
920
+ # @param options [Hash] Additional data to be passed
921
+ # @option options [Fixnum] :uid Owner to assign to the file
922
+ # @option options [Fixnum] :gid Group to assign to the file
923
+ # @option options [Fixnum] :mode File permissions (in octal) to assign to the file
924
+ # @return [Sawyer::Resource]
925
+ #
926
+ # @example Copy /tmp/test.txt from the local system to /etc/passwd in the container
927
+ # Hyperkit.push_file("/tmp/test.txt", "test-container", "/etc/passwd")
928
+ #
929
+ # @example Assign uid, gid, and mode to a file:
930
+ # Hyperkit.push_file("/tmp/test.txt",
931
+ # "test-container",
932
+ # "/etc/passwd",
933
+ # uid: 1000,
934
+ # gid: 1000,
935
+ # mode: 0644
936
+ # )
937
+ def push_file(source_file, container, dest_file, options={})
938
+
939
+ write_file(container, dest_file, options) do |f|
940
+ f.write File.read(source_file)
941
+ end
942
+
943
+ end
944
+
945
+ # @!endgroup
946
+
947
+ # @!group Logs
948
+
949
+ # Retrieve a list of logs for a container
950
+ #
951
+ # @param container [String] Container name
952
+ # @return [Array<String>] An array of log filenames
953
+ #
954
+ # @example Get list of logs for container "test-container"
955
+ # Hyperkit.logs("test-container")
956
+ def logs(container)
957
+ response = get(logs_path(container))
958
+ response.metadata.map { |path| path.sub(logs_path(container) + '/', '') }
959
+ end
960
+
961
+ # Retrieve the contents of a log for a container
962
+ #
963
+ # @param container [String] Container name
964
+ # @param log [String] Log filename
965
+ # @return [String] The contents of the log
966
+ #
967
+ # @example Get log "lxc.log" for container "test-container"
968
+ # Hyperkit.log("test-container", "lxc.log")
969
+ def log(container, log)
970
+ get(log_path(container, log))
971
+ end
972
+
973
+ # Delete a container's log
974
+ #
975
+ # @param container [String] Container name
976
+ # @param log [String] Filename of log to delete
977
+ #
978
+ # @example Delete log "lxc.log" for container "test-container"
979
+ # Hyperkit.delete_log("test-container", "lxc.log")
980
+ def delete_log(container, log)
981
+ delete(log_path(container, log))
982
+ end
983
+
984
+ # @!endgroup
985
+
986
+ private
987
+
988
+ REMOTE_IMAGE_ARGS = [:server, :protocol, :certificate, :secret]
989
+
990
+ def log_path(container, log)
991
+ File.join(logs_path(container), log)
992
+ end
993
+
994
+ def logs_path(container)
995
+ File.join(container_path(container), "logs")
996
+ end
997
+
998
+ def file_path(container, file)
999
+ File.join(container_path(container), "files") + "?path=#{file}"
1000
+ end
1001
+
1002
+ def snapshot_path(container, snapshot)
1003
+ File.join(snapshots_path(container), snapshot)
1004
+ end
1005
+
1006
+ def snapshots_path(name)
1007
+ File.join(container_path(name), "snapshots")
1008
+ end
1009
+
1010
+ def container_state_path(name)
1011
+ File.join(container_path(name), "state")
1012
+ end
1013
+
1014
+ def container_path(name)
1015
+ File.join(containers_path, name)
1016
+ end
1017
+
1018
+ def containers_path
1019
+ "/1.0/containers"
1020
+ end
1021
+
1022
+ def extract_container_options(name, options)
1023
+ opts = options.slice(:architecture, :profiles, :ephemeral, :config).
1024
+ merge({ name: name })
1025
+
1026
+ # Stringify any config values since LXD croaks on non-String values
1027
+ if opts[:config]
1028
+ opts[:config] = opts[:config].inject({}){|h,(k,v)| h[k.to_s] = v.to_s; h}
1029
+ end
1030
+
1031
+ opts
1032
+ end
1033
+
1034
+ def container_source_attribute(options)
1035
+
1036
+ [:fingerprint, :alias, :properties].each do |attr|
1037
+ return options.slice(attr) if options[attr]
1038
+ end
1039
+
1040
+ {}
1041
+
1042
+ end
1043
+
1044
+ def empty_container_options(name, options)
1045
+ opts = {
1046
+ source: {
1047
+ type: "none"
1048
+ }
1049
+ }.merge(extract_container_options(name, options))
1050
+
1051
+ [:alias, :certificate, :fingerprint, :properties, :protocol, :secret, :server].each do |prop|
1052
+ if ! (options.keys & [prop]).empty?
1053
+ raise Hyperkit::InvalidImageAttributes.new("empty: true is not compatible with the #{prop} option")
1054
+ end
1055
+ end
1056
+
1057
+ opts
1058
+
1059
+ end
1060
+
1061
+
1062
+ def remote_image_container_options(name, source, options)
1063
+
1064
+ opts = {
1065
+ source: {
1066
+ type: "image",
1067
+ mode: "pull"
1068
+ }.merge(options.slice(*REMOTE_IMAGE_ARGS)).merge(source)
1069
+
1070
+ }.merge(extract_container_options(name, options))
1071
+
1072
+ if options[:protocol] && ! %w[lxd simplestreams].include?(options[:protocol])
1073
+ raise Hyperkit::InvalidProtocol.new("Invalid protocol. Valid choices: lxd, simplestreams")
1074
+ end
1075
+
1076
+ opts
1077
+
1078
+ end
1079
+
1080
+ def local_image_container_options(name, source, options)
1081
+
1082
+ opts = {
1083
+ source: {
1084
+ type: "image"
1085
+ }.merge(source)
1086
+ }.merge(extract_container_options(name, options))
1087
+
1088
+ if ! (options.keys & REMOTE_IMAGE_ARGS).empty?
1089
+ raise Hyperkit::InvalidImageAttributes.new(":protocol, :certificate, and :secret only apply when :server is also passed")
1090
+ end
1091
+
1092
+ opts
1093
+
1094
+ end
1095
+
1096
+ end
1097
+
1098
+ end
1099
+
1100
+ end