├── .gitignore ├── Dockerfile.alpine3.4 ├── Dockerfile.alpine3.5 ├── Dockerfile.centos7 ├── Dockerfile.ubuntu14.04 ├── Dockerfile.ubuntu16.04 ├── LICENSE ├── Makefile ├── README.md ├── cmd └── docker-machine-driver-kvm │ ├── Makefile │ └── main.go └── kvm.go /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/docker-machine-driver-kvm/docker-machine-driver-kvm 2 | *.sw* 3 | docker-machine-driver-kvm-* 4 | -------------------------------------------------------------------------------- /Dockerfile.alpine3.4: -------------------------------------------------------------------------------- 1 | FROM alpine:3.4 2 | 3 | MAINTAINER Daniel Hiltgen 4 | 5 | ARG MACHINE_VERSION 6 | ENV GOPATH /go 7 | 8 | RUN apk -v add --update libvirt-dev curl go git musl-dev gcc 9 | RUN git clone --branch ${MACHINE_VERSION} https://github.com/docker/machine.git /go/src/github.com/docker/machine 10 | 11 | COPY . /go/src/github.com/dhiltgen/docker-machine-kvm 12 | WORKDIR /go/src/github.com/dhiltgen/docker-machine-kvm 13 | RUN go get -v -d ./... 14 | 15 | RUN go install -v ./cmd/docker-machine-driver-kvm 16 | -------------------------------------------------------------------------------- /Dockerfile.alpine3.5: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | 3 | MAINTAINER Daniel Hiltgen 4 | 5 | ARG MACHINE_VERSION 6 | ENV GOPATH /go 7 | 8 | RUN apk -v add --update libvirt-dev curl go git musl-dev gcc 9 | RUN git clone --branch ${MACHINE_VERSION} https://github.com/docker/machine.git /go/src/github.com/docker/machine 10 | 11 | COPY . /go/src/github.com/dhiltgen/docker-machine-kvm 12 | WORKDIR /go/src/github.com/dhiltgen/docker-machine-kvm 13 | RUN go get -v -d ./... 14 | 15 | RUN go install -v ./cmd/docker-machine-driver-kvm 16 | -------------------------------------------------------------------------------- /Dockerfile.centos7: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | 3 | MAINTAINER Daniel Hiltgen 4 | 5 | ARG MACHINE_VERSION 6 | ARG GO_VERSION 7 | ENV GOPATH /go 8 | 9 | RUN yum install -y libvirt-devel curl git gcc 10 | RUN curl -sSL https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz | tar -C /usr/local -xzf - 11 | ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/go/bin 12 | RUN git clone --branch ${MACHINE_VERSION} https://github.com/docker/machine.git /go/src/github.com/docker/machine 13 | 14 | COPY . /go/src/github.com/dhiltgen/docker-machine-kvm 15 | WORKDIR /go/src/github.com/dhiltgen/docker-machine-kvm 16 | RUN go get -v -d ./... 17 | 18 | RUN go install -v ./cmd/docker-machine-driver-kvm 19 | -------------------------------------------------------------------------------- /Dockerfile.ubuntu14.04: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | MAINTAINER Daniel Hiltgen 4 | 5 | ARG MACHINE_VERSION 6 | ARG GO_VERSION 7 | ENV GOPATH /go 8 | 9 | RUN apt-get update && apt-get install -y libvirt-dev curl git gcc 10 | RUN curl -sSL https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz | tar -C /usr/local -xzf - 11 | ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/go/bin 12 | RUN git clone --branch ${MACHINE_VERSION} https://github.com/docker/machine.git /go/src/github.com/docker/machine 13 | 14 | COPY . /go/src/github.com/dhiltgen/docker-machine-kvm 15 | WORKDIR /go/src/github.com/dhiltgen/docker-machine-kvm 16 | RUN go get -v -d ./... 17 | 18 | RUN go install -v ./cmd/docker-machine-driver-kvm 19 | -------------------------------------------------------------------------------- /Dockerfile.ubuntu16.04: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | MAINTAINER Daniel Hiltgen 4 | 5 | ARG MACHINE_VERSION 6 | ARG GO_VERSION 7 | ENV GOPATH /go 8 | 9 | RUN apt-get update && apt-get install -y libvirt-dev curl git gcc 10 | RUN curl -sSL https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz | tar -C /usr/local -xzf - 11 | ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/go/bin 12 | RUN git clone --branch ${MACHINE_VERSION} https://github.com/docker/machine.git /go/src/github.com/docker/machine 13 | 14 | COPY . /go/src/github.com/dhiltgen/docker-machine-kvm 15 | WORKDIR /go/src/github.com/dhiltgen/docker-machine-kvm 16 | RUN go get -v -d ./... 17 | 18 | RUN go install -v ./cmd/docker-machine-driver-kvm 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2014 Docker, Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX=docker-machine-driver-kvm 2 | MACHINE_VERSION=v0.10.0 3 | GO_VERSION=1.8.1 4 | DESCRIBE=$(shell git describe --tags) 5 | 6 | TARGETS=$(addprefix $(PREFIX)-, alpine3.4 alpine3.5 ubuntu14.04 ubuntu16.04 centos7) 7 | 8 | build: $(TARGETS) 9 | 10 | $(PREFIX)-%: Dockerfile.% 11 | docker rmi -f $@ >/dev/null 2>&1 || true 12 | docker rm -f $@-extract > /dev/null 2>&1 || true 13 | echo "Building binaries for $@" 14 | docker build --build-arg "MACHINE_VERSION=$(MACHINE_VERSION)" --build-arg "GO_VERSION=$(GO_VERSION)" -t $@ -f $< . 15 | docker create --name $@-extract $@ sh 16 | docker cp $@-extract:/go/bin/docker-machine-driver-kvm ./ 17 | mv ./docker-machine-driver-kvm ./$@ 18 | docker rm $@-extract || true 19 | docker rmi $@ || true 20 | 21 | clean: 22 | rm -f ./$(PREFIX)-* 23 | 24 | 25 | release: build 26 | @echo "Paste the following into the release page on github and upload the binaries..." 27 | @echo "" 28 | @for bin in $(PREFIX)-* ; do \ 29 | target=$$(echo $${bin} | cut -f5- -d-) ; \ 30 | md5=$$(md5sum $${bin}) ; \ 31 | echo "* $${target} - md5: $${md5}" ; \ 32 | echo '```' ; \ 33 | echo " curl -L https://github.com/dhiltgen/docker-machine-kvm/releases/download/$(DESCRIBE)/$${bin} > /usr/local/bin/$(PREFIX) \\ " ; \ 34 | echo " chmod +x /usr/local/bin/$(PREFIX)" ; \ 35 | echo '```' ; \ 36 | done 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-machine-kvm 2 | KVM driver for docker-machine 3 | 4 | This driver leverages the new [plugin architecture](https://github.com/docker/machine/issues/1626) being 5 | developed for Docker Machine. 6 | 7 | # Quick start instructions 8 | 9 | * Install `libvirt` and `qemu-kvm` on your system (e.g., `sudo apt-get install libvirt-bin qemu-kvm`) 10 | * Add yourself to the `libvirtd` group (may vary by linux distro) so you don't need to sudo 11 | * Install [docker-machine](https://github.com/docker/machine/releases) 12 | * Go to the 13 | [releases](https://github.com/dhiltgen/docker-machine-kvm/releases) 14 | page and download the docker-machine-driver-kvm binary, putting it 15 | in your PATH. 16 | * You can now create virtual machines using this driver with 17 | `docker-machine create -d kvm myengine0`. 18 | 19 | # Dependencies 20 | 21 | This driver leverages [libvirt](http://libvirt.org/) and the [libvirt-go 22 | library](https://github.com/libvirt/libvirt-go) to create and manage 23 | KVM based virtual machines. It has been tested with Ubuntu 12.04 through 15.04 24 | and should work on most platforms with KVM/libvirt support. If you run into 25 | compatibility problems, please file an [issue](https://github.com/dhiltgen/docker-machine-kvm/issues). 26 | 27 | Typically you'll run `docker-machine` as yourself, so you'll want to 28 | follow your distro specific instructions on allowing libvirt access 29 | from your account. For most distro's, you accomplish this by adding 30 | your account to the `libvirtd` group. 31 | 32 | 33 | # Capabilities 34 | 35 | ## Images 36 | By default `docker-machine-kvm` uses a [boot2docker.iso](https://github.com/boot2docker/boot2docker) as guest os for the kvm hypervisior. It's also possible to use every guest os image that is derived from [boot2docker.iso](https://github.com/boot2docker/boot2docker) as well. 37 | For using another image use the `--kvm-boot2docker-url` parameter. 38 | 39 | Community Members did some tests and it works with [rancher/os](https://github.com/rancher/os) as guest os too. 40 | 41 | ## Dual Network 42 | 43 | * **eth1** - A host private network called **docker-machines** is automatically created to ensure we always have connectivity to the VMs. The `docker-machine ip` command will always return this IP address which is only accessible from your local system. 44 | * **eth0** - You can specify any libvirt named network. If you don't specify one, the "default" named network will be used. 45 | * If you have exotic networking topolgies (openvswitch, etc.), you can use `virsh edit mymachinename` after creation, modify the first network definition by hand, then reboot the VM for the changes to take effect. 46 | * Typically this would be your "public" network accessible from external systems 47 | * To retrieve the IP address of this network, you can run a command like the following: 48 | ```bash 49 | docker-machine ssh mymachinename "ip -one -4 addr show dev eth0|cut -f7 -d' '" 50 | ``` 51 | 52 | ## Driver Parameters 53 | 54 | Here are all currently driver parameters listed that you can use. 55 | 56 | | Parameter | Description| 57 | | ------------- | ------------- | 58 | | **--kvm-cpu-count** | Sets the used CPU Cores for the KVM Machine. Defaults to `1` . | 59 | | **--kvm-disk-size** | Sets the kvm machine Disk size in MB. Defaults to `20000` . | 60 | | **--kvm-memory** | Sets the Memory of the kvm machine in MB. Defaults to `1024`. | 61 | | **--kvm-network** | Sets the Network of the kvm machinee which it should connect to. Defaults to `default`. | 62 | | **--kvm-boot2docker-url** | Sets the url from which host the image is loaded. By default it's not set. | 63 | | **--kvm-cache-mode** | Sets the caching mode of the kvm machine. Defaults to `default`. | 64 | | **--kvm-io-mode-url** | Sets the disk io mode of the kvm machine. Defaults to `threads`. | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /cmd/docker-machine-driver-kvm/Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | build: 4 | GOGC=off go build -i -o docker-machine-driver-kvm 5 | -------------------------------------------------------------------------------- /cmd/docker-machine-driver-kvm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dhiltgen/docker-machine-kvm" 5 | "github.com/docker/machine/libmachine/drivers/plugin" 6 | ) 7 | 8 | func main() { 9 | plugin.RegisterDriver(kvm.NewDriver("default", "path")) 10 | } 11 | -------------------------------------------------------------------------------- /kvm.go: -------------------------------------------------------------------------------- 1 | package kvm 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "encoding/json" 7 | "encoding/xml" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "text/template" 16 | "time" 17 | 18 | libvirt "github.com/libvirt/libvirt-go" 19 | 20 | "github.com/docker/machine/libmachine/drivers" 21 | "github.com/docker/machine/libmachine/log" 22 | "github.com/docker/machine/libmachine/mcnflag" 23 | "github.com/docker/machine/libmachine/mcnutils" 24 | "github.com/docker/machine/libmachine/ssh" 25 | "github.com/docker/machine/libmachine/state" 26 | ) 27 | 28 | const ( 29 | connectionString = "qemu:///system" 30 | privateNetworkName = "docker-machines" 31 | isoFilename = "boot2docker.iso" 32 | dnsmasqLeases = "/var/lib/libvirt/dnsmasq/%s.leases" 33 | dnsmasqStatus = "/var/lib/libvirt/dnsmasq/%s.status" 34 | defaultSSHUser = "docker" 35 | 36 | domainXMLTemplate = ` 37 | {{.MachineName}} {{.Memory}} 38 | {{.CPU}} 39 | 40 | 41 | 42 | hvm 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ` 69 | networkXML = ` 70 | %s 71 | 72 | 73 | 74 | 75 | 76 | ` 77 | ) 78 | 79 | type Driver struct { 80 | *drivers.BaseDriver 81 | 82 | Memory int 83 | DiskSize int 84 | CPU int 85 | Network string 86 | PrivateNetwork string 87 | ISO string 88 | Boot2DockerURL string 89 | CaCertPath string 90 | PrivateKeyPath string 91 | DiskPath string 92 | CacheMode string 93 | IOMode string 94 | connectionString string 95 | conn *libvirt.Connect 96 | VM *libvirt.Domain 97 | vmLoaded bool 98 | } 99 | 100 | func (d *Driver) GetCreateFlags() []mcnflag.Flag { 101 | return []mcnflag.Flag{ 102 | mcnflag.IntFlag{ 103 | Name: "kvm-memory", 104 | Usage: "Size of memory for host in MB", 105 | Value: 1024, 106 | }, 107 | mcnflag.IntFlag{ 108 | Name: "kvm-disk-size", 109 | Usage: "Size of disk for host in MB", 110 | Value: 20000, 111 | }, 112 | mcnflag.IntFlag{ 113 | Name: "kvm-cpu-count", 114 | Usage: "Number of CPUs", 115 | Value: 1, 116 | }, 117 | // TODO - support for multiple networks 118 | mcnflag.StringFlag{ 119 | Name: "kvm-network", 120 | Usage: "Name of network to connect to", 121 | Value: "default", 122 | }, 123 | mcnflag.StringFlag{ 124 | EnvVar: "KVM_BOOT2DOCKER_URL", 125 | Name: "kvm-boot2docker-url", 126 | Usage: "The URL of the boot2docker image. Defaults to the latest available version", 127 | Value: "", 128 | }, 129 | mcnflag.StringFlag{ 130 | Name: "kvm-cache-mode", 131 | Usage: "Disk cache mode: default, none, writethrough, writeback, directsync, or unsafe", 132 | Value: "default", 133 | }, 134 | mcnflag.StringFlag{ 135 | Name: "kvm-io-mode", 136 | Usage: "Disk IO mode: threads, native", 137 | Value: "threads", 138 | }, 139 | mcnflag.StringFlag{ 140 | EnvVar: "KVM_SSH_USER", 141 | Name: "kvm-ssh-user", 142 | Usage: "SSH username", 143 | Value: defaultSSHUser, 144 | }, 145 | /* Not yet implemented 146 | mcnflag.Flag{ 147 | Name: "kvm-no-share", 148 | Usage: "Disable the mount of your home directory", 149 | }, 150 | */ 151 | } 152 | } 153 | 154 | func (d *Driver) GetMachineName() string { 155 | return d.MachineName 156 | } 157 | 158 | func (d *Driver) GetSSHHostname() (string, error) { 159 | return d.GetIP() 160 | } 161 | 162 | func (d *Driver) GetSSHKeyPath() string { 163 | return d.ResolveStorePath("id_rsa") 164 | } 165 | 166 | func (d *Driver) GetSSHPort() (int, error) { 167 | if d.SSHPort == 0 { 168 | d.SSHPort = 22 169 | } 170 | 171 | return d.SSHPort, nil 172 | } 173 | 174 | func (d *Driver) GetSSHUsername() string { 175 | if d.SSHUser == "" { 176 | d.SSHUser = "docker" 177 | } 178 | 179 | return d.SSHUser 180 | } 181 | 182 | func (d *Driver) DriverName() string { 183 | return "kvm" 184 | } 185 | 186 | func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { 187 | log.Debugf("SetConfigFromFlags called") 188 | d.Memory = flags.Int("kvm-memory") 189 | d.DiskSize = flags.Int("kvm-disk-size") 190 | d.CPU = flags.Int("kvm-cpu-count") 191 | d.Network = flags.String("kvm-network") 192 | d.Boot2DockerURL = flags.String("kvm-boot2docker-url") 193 | d.CacheMode = flags.String("kvm-cache-mode") 194 | d.IOMode = flags.String("kvm-io-mode") 195 | 196 | d.SwarmMaster = flags.Bool("swarm-master") 197 | d.SwarmHost = flags.String("swarm-host") 198 | d.SwarmDiscovery = flags.String("swarm-discovery") 199 | d.ISO = d.ResolveStorePath(isoFilename) 200 | d.SSHUser = flags.String("kvm-ssh-user") 201 | d.SSHPort = 22 202 | d.DiskPath = d.ResolveStorePath(fmt.Sprintf("%s.img", d.MachineName)) 203 | return nil 204 | } 205 | 206 | func (d *Driver) GetURL() (string, error) { 207 | log.Debugf("GetURL called") 208 | ip, err := d.GetIP() 209 | if err != nil { 210 | log.Warnf("Failed to get IP: %s", err) 211 | return "", err 212 | } 213 | if ip == "" { 214 | return "", nil 215 | } 216 | return fmt.Sprintf("tcp://%s:2376", ip), nil // TODO - don't hardcode the port! 217 | } 218 | 219 | func (d *Driver) getConn() (*libvirt.Connect, error) { 220 | if d.conn == nil { 221 | conn, err := libvirt.NewConnect(connectionString) 222 | if err != nil { 223 | log.Errorf("Failed to connect to libvirt: %s", err) 224 | return &libvirt.Connect{}, errors.New("Unable to connect to kvm driver, did you add yourself to the libvirtd group?") 225 | } 226 | d.conn = conn 227 | } 228 | return d.conn, nil 229 | } 230 | 231 | // Create, or verify the private network is properly configured 232 | func (d *Driver) validatePrivateNetwork() error { 233 | log.Debug("Validating private network") 234 | conn, err := d.getConn() 235 | if err != nil { 236 | return err 237 | } 238 | network, err := conn.LookupNetworkByName(d.PrivateNetwork) 239 | if err == nil { 240 | xmldoc, err := network.GetXMLDesc(0) 241 | if err != nil { 242 | return err 243 | } 244 | /* XML structure: 245 | 246 | ... 247 | 248 | 249 | 250 | 251 | */ 252 | type Ip struct { 253 | Address string `xml:"address,attr"` 254 | Netmask string `xml:"netmask,attr"` 255 | } 256 | type Network struct { 257 | Ip Ip `xml:"ip"` 258 | } 259 | 260 | var nw Network 261 | err = xml.Unmarshal([]byte(xmldoc), &nw) 262 | if err != nil { 263 | return err 264 | } 265 | 266 | if nw.Ip.Address == "" { 267 | return fmt.Errorf("%s network doesn't have DHCP configured properly", d.PrivateNetwork) 268 | } 269 | // Corner case, but might happen... 270 | if active, err := network.IsActive(); !active { 271 | log.Debugf("Reactivating private network: %s", err) 272 | err = network.Create() 273 | if err != nil { 274 | log.Warnf("Failed to Start network: %s", err) 275 | return err 276 | } 277 | } 278 | return nil 279 | } 280 | // TODO - try a couple pre-defined networks and look for conflicts before 281 | // settling on one 282 | xml := fmt.Sprintf(networkXML, d.PrivateNetwork, 283 | "192.168.42.1", 284 | "255.255.255.0", 285 | "192.168.42.2", 286 | "192.168.42.254") 287 | 288 | network, err = conn.NetworkDefineXML(xml) 289 | if err != nil { 290 | log.Errorf("Failed to create private network: %s", err) 291 | return nil 292 | } 293 | err = network.SetAutostart(true) 294 | if err != nil { 295 | log.Warnf("Failed to set private network to autostart: %s", err) 296 | } 297 | err = network.Create() 298 | if err != nil { 299 | log.Warnf("Failed to Start network: %s", err) 300 | return err 301 | } 302 | return nil 303 | } 304 | 305 | func (d *Driver) validateNetwork(name string) error { 306 | log.Debugf("Validating network %s", name) 307 | conn, err := d.getConn() 308 | if err != nil { 309 | return err 310 | } 311 | _, err = conn.LookupNetworkByName(name) 312 | if err != nil { 313 | log.Errorf("Unable to locate network %s", name) 314 | return err 315 | } 316 | return nil 317 | } 318 | 319 | func (d *Driver) PreCreateCheck() error { 320 | conn, err := d.getConn() 321 | if err != nil { 322 | return err 323 | } 324 | 325 | // TODO We could look at conn.GetCapabilities() 326 | // parse the XML, and look for kvm 327 | log.Debug("About to check libvirt version") 328 | 329 | // TODO might want to check minimum version 330 | _, err = conn.GetLibVersion() 331 | if err != nil { 332 | log.Warnf("Unable to get libvirt version") 333 | return err 334 | } 335 | err = d.validatePrivateNetwork() 336 | if err != nil { 337 | return err 338 | } 339 | err = d.validateNetwork(d.Network) 340 | if err != nil { 341 | return err 342 | } 343 | // Others...? 344 | return nil 345 | } 346 | 347 | func (d *Driver) Create() error { 348 | b2dutils := mcnutils.NewB2dUtils(d.StorePath) 349 | if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil { 350 | return err 351 | } 352 | 353 | log.Infof("Creating SSH key...") 354 | if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { 355 | return err 356 | } 357 | 358 | if err := os.MkdirAll(d.ResolveStorePath("."), 0755); err != nil { 359 | return err 360 | } 361 | 362 | // Libvirt typically runs as a deprivileged service account and 363 | // needs the execute bit set for directories that contain disks 364 | for dir := d.ResolveStorePath("."); dir != "/"; dir = filepath.Dir(dir) { 365 | log.Debugf("Verifying executable bit set on %s", dir) 366 | info, err := os.Stat(dir) 367 | if err != nil { 368 | return err 369 | } 370 | mode := info.Mode() 371 | if mode&0001 != 1 { 372 | log.Debugf("Setting executable bit set on %s", dir) 373 | mode |= 0001 374 | os.Chmod(dir, mode) 375 | } 376 | } 377 | 378 | log.Debugf("Creating VM data disk...") 379 | if err := d.generateDiskImage(d.DiskSize); err != nil { 380 | return err 381 | } 382 | 383 | log.Debugf("Defining VM...") 384 | tmpl, err := template.New("domain").Parse(domainXMLTemplate) 385 | if err != nil { 386 | return err 387 | } 388 | var xml bytes.Buffer 389 | err = tmpl.Execute(&xml, d) 390 | if err != nil { 391 | return err 392 | } 393 | 394 | conn, err := d.getConn() 395 | if err != nil { 396 | return err 397 | } 398 | vm, err := conn.DomainDefineXML(xml.String()) 399 | if err != nil { 400 | log.Warnf("Failed to create the VM: %s", err) 401 | return err 402 | } 403 | d.VM = vm 404 | d.vmLoaded = true 405 | 406 | return d.Start() 407 | } 408 | 409 | func (d *Driver) Start() error { 410 | log.Debugf("Starting VM %s", d.MachineName) 411 | if err := d.validateVMRef(); err != nil { 412 | return err 413 | } 414 | if err := d.VM.Create(); err != nil { 415 | log.Warnf("Failed to start: %s", err) 416 | return err 417 | } 418 | 419 | // They wont start immediately 420 | time.Sleep(5 * time.Second) 421 | 422 | for i := 0; i < 90; i++ { 423 | time.Sleep(time.Second) 424 | ip, _ := d.GetIP() 425 | if ip != "" { 426 | // Add a second to let things settle 427 | time.Sleep(time.Second) 428 | return nil 429 | } 430 | log.Debugf("Waiting for the VM to come up... %d", i) 431 | } 432 | log.Warnf("Unable to determine VM's IP address, did it fail to boot?") 433 | return nil 434 | } 435 | 436 | func (d *Driver) Stop() error { 437 | log.Debugf("Stopping VM %s", d.MachineName) 438 | if err := d.validateVMRef(); err != nil { 439 | return err 440 | } 441 | s, err := d.GetState() 442 | if err != nil { 443 | return err 444 | } 445 | 446 | if s != state.Stopped { 447 | err := d.VM.Shutdown() 448 | if err != nil { 449 | log.Warnf("Failed to gracefully shutdown VM") 450 | return err 451 | } 452 | for i := 0; i < 90; i++ { 453 | time.Sleep(time.Second) 454 | s, _ := d.GetState() 455 | log.Debugf("VM state: %s", s) 456 | if s == state.Stopped { 457 | return nil 458 | } 459 | } 460 | return errors.New("VM Failed to gracefully shutdown, try the kill command") 461 | } 462 | return nil 463 | } 464 | 465 | func (d *Driver) Remove() error { 466 | log.Debugf("Removing VM %s", d.MachineName) 467 | if err := d.validateVMRef(); err != nil { 468 | return err 469 | } 470 | // Note: If we switch to qcow disks instead of raw the user 471 | // could take a snapshot. If you do, then Undefine 472 | // will fail unless we nuke the snapshots first 473 | d.VM.Destroy() // Ignore errors 474 | return d.VM.Undefine() 475 | } 476 | 477 | func (d *Driver) Restart() error { 478 | log.Debugf("Restarting VM %s", d.MachineName) 479 | if err := d.Stop(); err != nil { 480 | return err 481 | } 482 | return d.Start() 483 | } 484 | 485 | func (d *Driver) Kill() error { 486 | log.Debugf("Killing VM %s", d.MachineName) 487 | if err := d.validateVMRef(); err != nil { 488 | return err 489 | } 490 | return d.VM.Destroy() 491 | } 492 | 493 | func (d *Driver) GetState() (state.State, error) { 494 | log.Debugf("Getting current state...") 495 | if err := d.validateVMRef(); err != nil { 496 | return state.None, err 497 | } 498 | virState, _, err := d.VM.GetState() 499 | if err != nil { 500 | return state.None, err 501 | } 502 | switch virState { 503 | case libvirt.DOMAIN_NOSTATE: 504 | return state.None, nil 505 | case libvirt.DOMAIN_RUNNING: 506 | return state.Running, nil 507 | case libvirt.DOMAIN_BLOCKED: 508 | // TODO - Not really correct, but does it matter? 509 | return state.Error, nil 510 | case libvirt.DOMAIN_PAUSED: 511 | return state.Paused, nil 512 | case libvirt.DOMAIN_SHUTDOWN: 513 | return state.Stopped, nil 514 | case libvirt.DOMAIN_CRASHED: 515 | return state.Error, nil 516 | case libvirt.DOMAIN_PMSUSPENDED: 517 | return state.Saved, nil 518 | case libvirt.DOMAIN_SHUTOFF: 519 | return state.Stopped, nil 520 | } 521 | return state.None, nil 522 | } 523 | 524 | func (d *Driver) validateVMRef() error { 525 | if !d.vmLoaded { 526 | log.Debugf("Fetching VM...") 527 | conn, err := d.getConn() 528 | if err != nil { 529 | return err 530 | } 531 | vm, err := conn.LookupDomainByName(d.MachineName) 532 | if err != nil { 533 | log.Warnf("Failed to fetch machine") 534 | } else { 535 | d.VM = vm 536 | d.vmLoaded = true 537 | } 538 | } 539 | return nil 540 | } 541 | 542 | // This implementation is specific to default networking in libvirt 543 | // with dnsmasq 544 | func (d *Driver) getMAC() (string, error) { 545 | if err := d.validateVMRef(); err != nil { 546 | return "", err 547 | } 548 | xmldoc, err := d.VM.GetXMLDesc(0) 549 | if err != nil { 550 | return "", err 551 | } 552 | /* XML structure: 553 | 554 | ... 555 | 556 | ... 557 | 558 | ... 559 | 560 | ... 561 | 562 | ... 563 | */ 564 | type Mac struct { 565 | Address string `xml:"address,attr"` 566 | } 567 | type Source struct { 568 | Network string `xml:"network,attr"` 569 | } 570 | type Interface struct { 571 | Type string `xml:"type,attr"` 572 | Mac Mac `xml:"mac"` 573 | Source Source `xml:"source"` 574 | } 575 | type Devices struct { 576 | Interfaces []Interface `xml:"interface"` 577 | } 578 | type Domain struct { 579 | Devices Devices `xml:"devices"` 580 | } 581 | 582 | var dom Domain 583 | err = xml.Unmarshal([]byte(xmldoc), &dom) 584 | if err != nil { 585 | return "", err 586 | } 587 | // Always assume the second interface is the one we want 588 | if len(dom.Devices.Interfaces) < 2 { 589 | return "", fmt.Errorf("VM doesn't have enough network interfaces. Expected at least 2, found %d", 590 | len(dom.Devices.Interfaces)) 591 | } 592 | return dom.Devices.Interfaces[1].Mac.Address, nil 593 | } 594 | 595 | func (d *Driver) getIPByMACFromLeaseFile(mac string) (string, error) { 596 | leaseFile := fmt.Sprintf(dnsmasqLeases, d.PrivateNetwork) 597 | data, err := ioutil.ReadFile(leaseFile) 598 | if err != nil { 599 | log.Debugf("Failed to retrieve dnsmasq leases from %s", leaseFile) 600 | return "", err 601 | } 602 | for lineNum, line := range strings.Split(string(data), "\n") { 603 | if len(line) == 0 { 604 | continue 605 | } 606 | entries := strings.Split(line, " ") 607 | if len(entries) < 3 { 608 | log.Warnf("Malformed dnsmasq line %d", lineNum+1) 609 | return "", errors.New("Malformed dnsmasq file") 610 | } 611 | if strings.ToLower(entries[1]) == strings.ToLower(mac) { 612 | log.Debugf("IP address: %s", entries[2]) 613 | return entries[2], nil 614 | } 615 | } 616 | return "", nil 617 | } 618 | 619 | func (d *Driver) getIPByMacFromSettings(mac string) (string, error) { 620 | conn, err := d.getConn() 621 | if err != nil { 622 | return "", err 623 | } 624 | network, err := conn.LookupNetworkByName(d.PrivateNetwork) 625 | if err != nil { 626 | log.Warnf("Failed to find network: %s", err) 627 | return "", err 628 | } 629 | bridge_name, err := network.GetBridgeName() 630 | if err != nil { 631 | log.Warnf("Failed to get network bridge: %s", err) 632 | return "", err 633 | } 634 | statusFile := fmt.Sprintf(dnsmasqStatus, bridge_name) 635 | data, err := ioutil.ReadFile(statusFile) 636 | type Lease struct { 637 | Ip_address string `json:"ip-address"` 638 | Mac_address string `json:"mac-address"` 639 | // Other unused fields omitted 640 | } 641 | var s []Lease 642 | 643 | err = json.Unmarshal(data, &s) 644 | if err != nil { 645 | log.Warnf("Failed to decode dnsmasq lease status: %s", err) 646 | return "", err 647 | } 648 | ipAddr := "" 649 | for _, value := range s { 650 | if strings.ToLower(value.Mac_address) == strings.ToLower(mac) { 651 | // If there are multiple entries, 652 | // the last one is the most current 653 | ipAddr = value.Ip_address 654 | } 655 | } 656 | if ipAddr != "" { 657 | log.Debugf("IP address: %s", ipAddr) 658 | } 659 | return ipAddr, nil 660 | } 661 | 662 | func (d *Driver) GetIP() (string, error) { 663 | log.Debugf("GetIP called for %s", d.MachineName) 664 | mac, err := d.getMAC() 665 | if err != nil { 666 | return "", err 667 | } 668 | /* 669 | * TODO - Figure out what version of libvirt changed behavior and 670 | * be smarter about selecting which algorithm to use 671 | */ 672 | ip, err := d.getIPByMACFromLeaseFile(mac) 673 | if ip == "" { 674 | ip, err = d.getIPByMacFromSettings(mac) 675 | } 676 | log.Debugf("Unable to locate IP address for MAC %s", mac) 677 | return ip, err 678 | } 679 | 680 | func (d *Driver) publicSSHKeyPath() string { 681 | return d.GetSSHKeyPath() + ".pub" 682 | } 683 | 684 | // Make a boot2docker VM disk image. 685 | func (d *Driver) generateDiskImage(size int) error { 686 | log.Debugf("Creating %d MB hard disk image...", size) 687 | 688 | magicString := "boot2docker, please format-me" 689 | 690 | buf := new(bytes.Buffer) 691 | tw := tar.NewWriter(buf) 692 | 693 | // magicString first so the automount script knows to format the disk 694 | file := &tar.Header{Name: magicString, Size: int64(len(magicString))} 695 | if err := tw.WriteHeader(file); err != nil { 696 | return err 697 | } 698 | if _, err := tw.Write([]byte(magicString)); err != nil { 699 | return err 700 | } 701 | // .ssh/key.pub => authorized_keys 702 | file = &tar.Header{Name: ".ssh", Typeflag: tar.TypeDir, Mode: 0700} 703 | if err := tw.WriteHeader(file); err != nil { 704 | return err 705 | } 706 | pubKey, err := ioutil.ReadFile(d.publicSSHKeyPath()) 707 | if err != nil { 708 | return err 709 | } 710 | file = &tar.Header{Name: ".ssh/authorized_keys", Size: int64(len(pubKey)), Mode: 0644} 711 | if err := tw.WriteHeader(file); err != nil { 712 | return err 713 | } 714 | if _, err := tw.Write([]byte(pubKey)); err != nil { 715 | return err 716 | } 717 | file = &tar.Header{Name: ".ssh/authorized_keys2", Size: int64(len(pubKey)), Mode: 0644} 718 | if err := tw.WriteHeader(file); err != nil { 719 | return err 720 | } 721 | if _, err := tw.Write([]byte(pubKey)); err != nil { 722 | return err 723 | } 724 | if err := tw.Close(); err != nil { 725 | return err 726 | } 727 | raw := bytes.NewReader(buf.Bytes()) 728 | return createDiskImage(d.DiskPath, size, raw) 729 | } 730 | 731 | // createDiskImage makes a disk image at dest with the given size in MB. If r is 732 | // not nil, it will be read as a raw disk image to convert from. 733 | func createDiskImage(dest string, size int, r io.Reader) error { 734 | // Convert a raw image from stdin to the dest VMDK image. 735 | sizeBytes := int64(size) << 20 // usually won't fit in 32-bit int (max 2GB) 736 | f, err := os.Create(dest) 737 | if err != nil { 738 | return err 739 | } 740 | 741 | _, err = io.Copy(f, r) 742 | if err != nil { 743 | return err 744 | } 745 | // Rely on seeking to create a sparse raw file for qemu 746 | f.Seek(sizeBytes-1, 0) 747 | f.Write([]byte{0}) 748 | return f.Close() 749 | } 750 | 751 | func NewDriver(hostName, storePath string) drivers.Driver { 752 | return &Driver{ 753 | PrivateNetwork: privateNetworkName, 754 | BaseDriver: &drivers.BaseDriver{ 755 | SSHUser: defaultSSHUser, 756 | MachineName: hostName, 757 | StorePath: storePath, 758 | }, 759 | } 760 | } 761 | --------------------------------------------------------------------------------