hyperkit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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