├── .gitignore ├── LICENSE ├── README.md ├── cmd └── docker-machine-driver-proxmoxve │ ├── Makefile │ └── main.go ├── proxmox.go ├── proxmox_test.go ├── proxmoxdriver.go ├── testconfig.json.example └── testconfig_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # docker machine driver 2 | cmd/docker-machine-driver-proxmoxve/docker-machine-driver-proxmox* 3 | 4 | # MacOS stuff 5 | .DS_Store 6 | 7 | # Visual Studio Code stuff 8 | .history 9 | *.code-workspace 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andreas Steinel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Machine Driver for Proxmox VE 2 | 3 | This driver can be used to kickstart a VM in Proxmox VE to be used with Docker/Docker Machine. 4 | 5 | * [Download](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/releases/tag/v4) and copy it into your `PATH` (don't forget to `chmod +x`) or build your own driver 6 | * Check if it works: 7 | 8 | $ docker-machine create --driver proxmoxve --help | grep -c proxmox 9 | 35 10 | 11 | ## Operation 12 | 13 | Now you have two modes of operation: 14 | * use an iso to install a Docker distribution (e.g. RancherOS) 15 | * use a previously created cloud-init-based image VM template as a base 16 | 17 | There are also other options to customize your VM which are not shown here, so 18 | please feel free to explore them with `docker-machine create --driver proxmoxve --help` 19 | 20 | ### Preparing a special test user in PVE 21 | 22 | If you want to test this docker-machine driver, i strongly recommend to secure it properly. 23 | Best way to do this to create a special user that has its own pool and storage for creating 24 | the test machines. This corresponds to the examples below. 25 | 26 | Here is what I use (based on ZFS): 27 | 28 | * create a pool for use as `--proxmoxve-proxmox-pool docker-machine` 29 | 30 | pvesh create /pools -poolid docker-machine 31 | 32 | * create an user `docker-machine` with password `D0ck3rS3cr3t` 33 | 34 | pvesh create /access/users -userid docker-machine@pve -password D0ck3rS3cr3t 35 | 36 | * creating a special ZFS dataset and use it as PVE storage 37 | 38 | zfs create -o refquota=50G rpool/docker-machine-test 39 | zfs create rpool/docker-machine-test/iso 40 | pvesh create /storage -storage docker-machine -type zfspool -pool rpool/docker-machine-test 41 | pvesh create /storage -storage docker-machine-iso -type dir -path /rpool/docker-machine-test/iso -content iso 42 | pvesh set /pools/docker-machine -storage docker-machine 43 | pvesh set /pools/docker-machine -storage docker-machine-iso 44 | 45 | * set proper permissions for the user 46 | 47 | pvesh set /access/acl -path /pool/docker-machine -roles PVEVMAdmin,PVEDatastoreAdmin,PVEPoolAdmin -users docker-machine@pve 48 | 49 | If you have additional test storages, you can also add them easily: 50 | 51 | pvesh set /pools/docker-machine -storage nfs 52 | pvesh set /pools/docker-machine -storage lvm 53 | pvesh set /pools/docker-machine -storage directory 54 | 55 | Ceph is currently not directly tested by me, but there are [fine people](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/issues/32) 56 | out there wo tried it. 57 | 58 | 59 | ### Clone VM 60 | 61 | This approach uses a predefined VM template with cloud-init support to be cloned 62 | and used. There a lot of ways to do that, here is an adopted one 63 | (courtesy of [@travisghansen](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/34#issuecomment-665277775)): 64 | 65 | ```sh 66 | #!/bin/bash 67 | 68 | set -x 69 | set -e 70 | 71 | export IMGID=9007 72 | export BASE_IMG="debian-10-openstack-amd64.qcow2" 73 | export IMG="debian-10-openstack-amd64-${IMGID}.qcow2" 74 | export STORAGEID="docker-machine" 75 | 76 | if [ ! -f "${BASE_IMG}" ];then 77 | wget https://cloud.debian.org/images/cloud/OpenStack/current-10/debian-10-openstack-amd64.qcow2 78 | fi 79 | 80 | if [ ! -f "${IMG}" ];then 81 | cp -f "${BASE_IMG}" "${IMG}" 82 | fi 83 | 84 | # prepare mounts 85 | guestmount -a ${IMG} -m /dev/sda1 /mnt/tmp/ 86 | mount --bind /dev/ /mnt/tmp/dev/ 87 | mount --bind /proc/ /mnt/tmp/proc/ 88 | 89 | # get resolving working 90 | mv /mnt/tmp/etc/resolv.conf /mnt/tmp/etc/resolv.conf.orig 91 | cp -a --force /etc/resolv.conf /mnt/tmp/etc/resolv.conf 92 | 93 | # install desired apps 94 | chroot /mnt/tmp /bin/bash -c "apt-get update" 95 | chroot /mnt/tmp /bin/bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y net-tools curl qemu-guest-agent nfs-common open-iscsi lsscsi sg3-utils multipath-tools scsitools" 96 | 97 | # https://www.electrictoolbox.com/sshd-hostname-lookups/ 98 | sed -i 's:#UseDNS no:UseDNS no:' /mnt/tmp/etc/ssh/sshd_config 99 | 100 | sed -i '/package-update-upgrade-install/d' /mnt/tmp/etc/cloud/cloud.cfg 101 | 102 | cat > /mnt/tmp/etc/cloud/cloud.cfg.d/99_custom.cfg << '__EOF__' 103 | #cloud-config 104 | 105 | # Install additional packages on first boot 106 | # 107 | # Default: none 108 | # 109 | # if packages are specified, this apt_update will be set to true 110 | # 111 | # packages may be supplied as a single package name or as a list 112 | # with the format [, ] wherein the specifc 113 | # package version will be installed. 114 | #packages: 115 | # - qemu-guest-agent 116 | # - nfs-common 117 | 118 | ntp: 119 | enabled: true 120 | 121 | # datasource_list: [ NoCloud, ConfigDrive ] 122 | __EOF__ 123 | 124 | cat > /mnt/tmp/etc/multipath.conf << '__EOF__' 125 | defaults { 126 | user_friendly_names yes 127 | find_multipaths yes 128 | } 129 | __EOF__ 130 | 131 | # enable services 132 | chroot /mnt/tmp systemctl enable open-iscsi.service || true 133 | chroot /mnt/tmp systemctl enable multipath-tools.service || true 134 | 135 | # restore systemd-resolved settings 136 | mv /mnt/tmp/etc/resolv.conf.orig /mnt/tmp/etc/resolv.conf 137 | 138 | # umount everything 139 | umount /mnt/tmp/dev 140 | umount /mnt/tmp/proc 141 | umount /mnt/tmp 142 | 143 | # create template 144 | qm create ${IMGID} --memory 512 --net0 virtio,bridge=vmbr0 145 | qm importdisk ${IMGID} ${IMG} ${STORAGEID} --format qcow2 146 | qm set ${IMGID} --scsihw virtio-scsi-pci --scsi0 ${STORAGEID}:vm-${IMGID}-disk-0 147 | qm set ${IMGID} --ide2 ${STORAGEID}:cloudinit 148 | qm set ${IMGID} --boot c --bootdisk scsi0 149 | qm set ${IMGID} --serial0 socket --vga serial0 150 | qm template ${IMGID} 151 | 152 | # set host cpu, ssh key, etc 153 | qm set ${IMGID} --scsihw virtio-scsi-pci 154 | qm set ${IMGID} --cpu host 155 | qm set ${IMGID} --agent enabled=1 156 | qm set ${IMGID} --autostart 157 | qm set ${IMGID} --onboot 1 158 | qm set ${IMGID} --ostype l26 159 | qm set ${IMGID} --ipconfig0 "ip=dhcp" 160 | ``` 161 | 162 | Adapt to fit your needs and run it on your Proxmox VE until it works without 163 | any problems and creates a template in your Proxmox VE. You may need to install 164 | `libguestfs-tools`. 165 | 166 | After the image is created, you can start to use the machine driver to create 167 | new VMs: 168 | 169 | ```sh 170 | #!/bin/sh 171 | set -ex 172 | 173 | export PATH=$PWD:$PATH 174 | 175 | PVE_NODE="proxmox" 176 | PVE_HOST="192.168.1.5" 177 | 178 | PVE_USER="docker-machine" 179 | PVE_REALM="pve" 180 | PVE_PASSWD="D0ck3rS3cr3t" 181 | 182 | PVE_STORAGE_NAME="${1:-docker-machine}" 183 | PVE_POOL="docker-machine" 184 | 185 | VM_NAME="docker-clone" 186 | 187 | docker-machine rm --force $VM_NAME >/dev/null 2>&1 || true 188 | 189 | docker-machine --debug \ 190 | create \ 191 | --driver proxmoxve \ 192 | --proxmoxve-proxmox-host $PVE_HOST \ 193 | --proxmoxve-proxmox-node $PVE_NODE \ 194 | --proxmoxve-proxmox-user-name $PVE_USER \ 195 | --proxmoxve-proxmox-user-password $PVE_PASSWD \ 196 | --proxmoxve-proxmox-realm $PVE_REALM \ 197 | --proxmoxve-proxmox-pool $PVE_POOL \ 198 | \ 199 | --proxmoxve-provision-strategy clone \ 200 | --proxmoxve-ssh-username 'debian' \ 201 | --proxmoxve-ssh-password 'geheim' \ 202 | --proxmoxve-vm-clone-vmid 9007 \ 203 | \ 204 | --proxmoxve-debug-resty \ 205 | --proxmoxve-debug-driver \ 206 | \ 207 | $* \ 208 | \ 209 | $VM_NAME 210 | 211 | eval $(docker-machine env $VM_NAME) 212 | 213 | docker ps 214 | ``` 215 | 216 | 217 | ### Rancher OS 218 | 219 | * Use a recent, e.g. `1.5.6` version of [RancherOS](https://github.com/rancher/os/releases) and copy the 220 | `rancheros-proxmoxve-autoformat.iso` to your iso image storage on your PVE 221 | * Create a script with the following contents and *adapt to your needs*: 222 | 223 | ```sh 224 | PVE_NODE="proxmox-docker-machine" 225 | PVE_HOST="192.168.1.10" 226 | 227 | PVE_USER="docker-machine" 228 | PVE_REALM="pve" 229 | PVE_PASSWD="D0ck3rS3cr3t" 230 | 231 | PVE_STORAGE_NAME="docker-machine" 232 | PVE_STORAGE_SIZE="4" 233 | PVE_POOL="docker-machine" 234 | 235 | SSH_USERNAME="docker" 236 | SSH_PASSWORD="tcuser" 237 | 238 | PVE_MEMORY=2 239 | PVE_CPU_CORES=4 240 | PVE_IMAGE_FILE="docker-machine-iso:iso/rancheros-proxmoxve-autoformat-v1.5.5.iso" 241 | VM_NAME="docker-rancher" 242 | 243 | docker-machine rm --force $VM_NAME >/dev/null 2>&1 || true 244 | 245 | docker-machine --debug \ 246 | create \ 247 | --driver proxmoxve \ 248 | --proxmoxve-proxmox-host $PVE_HOST \ 249 | --proxmoxve-proxmox-node $PVE_NODE \ 250 | --proxmoxve-proxmox-user-name $PVE_USER \ 251 | --proxmoxve-proxmox-user-password $PVE_PASSWD \ 252 | --proxmoxve-proxmox-realm $PVE_REALM \ 253 | --proxmoxve-proxmox-pool $PVE_POOL \ 254 | \ 255 | --proxmoxve-vm-storage-path $PVE_STORAGE_NAME \ 256 | --proxmoxve-vm-storage-size $PVE_STORAGE_SIZE \ 257 | --proxmoxve-vm-cpu-cores $PVE_CPU_CORES \ 258 | --proxmoxve-vm-memory $PVE_MEMORY \ 259 | --proxmoxve-vm-image-file "$PVE_IMAGE_FILE" \ 260 | \ 261 | --proxmoxve-ssh-username $SSH_USERNAME \ 262 | --proxmoxve-ssh-password $SSH_PASSWORD \ 263 | \ 264 | --proxmoxve-debug-resty \ 265 | --proxmoxve-debug-driver \ 266 | \ 267 | $VM_NAME 268 | 269 | 270 | eval $(docker-machine env $VM_NAME) 271 | 272 | docker ps 273 | ``` 274 | * Run the script 275 | 276 | At the first run, it is advisable to not comment out the `debug` flags. If everything works as expected, you can remove them. 277 | 278 | ## Changes 279 | 280 | ### Version 4 281 | 282 | * [support for using clones+cloud-init](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/34) (Thanks to @travisghansen) 283 | * [enable custom network bridge without vlan tag](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/30) (Thanks to @guyguy333) 284 | * [including args to choice scsi model](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/28) (Thanks to @bemanuel) 285 | * [fix remove error, add further flags](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/26) (Thanks to @Psayker) 286 | 287 | ### Version 3 288 | 289 | * [Renaming driver from `proxmox-ve` to `proxmoxve` due to identification problem with RancherOS's K8S implementation](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/18) (Thanks to [`@Sellto` for reporting #16](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/issues/16)) 290 | * fixing issue with created disk detection (Thanks to [`@Sellto` for reporting #16](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/issues/16)) 291 | * [Add `IPAddress` property needed by rancher to know the ip address of the created VM](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/18) (Thanks to `@Sellto`) 292 | * [Change the name of each flag for better display in the rancher `Node Templates`](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/18) (Thanks to `@Sellto`) 293 | * [Add number of `CPU cores configuration paramater`](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/18) (Thanks to `@Sellto`) 294 | * [LVM-thin fixes](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/24) (Thanks to `@vstconsulting`) 295 | * [Bridge and VLAN tag support](https://github.com/lnxbil/docker-machine-driver-proxmox-ve/pull/22) (Thanks to `@bemanuel`) 296 | * Fixing filename detection including NFS support 297 | 298 | ### Version 2 299 | 300 | * exclusive RancherOS support due to their special Proxmox VE iso files 301 | * adding wait cycles for asynchronous background tasks, e.g. `create`, `stop` etc. 302 | * use one logger engine 303 | * add guest username, password and ssh-port as new command line arguments 304 | * more and potentially better error handling 305 | 306 | ### Version 1 307 | 308 | * Initial Version 309 | -------------------------------------------------------------------------------- /cmd/docker-machine-driver-proxmoxve/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | GOOS=linux GOARCH=amd64 go build -o docker-machine-driver-proxmoxve.linux-amd64 3 | GOOS=darwin GOARCH=amd64 go build -o docker-machine-driver-proxmoxve.macos-amd64 4 | -------------------------------------------------------------------------------- /cmd/docker-machine-driver-proxmoxve/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/docker/machine/libmachine/drivers/plugin" 5 | proxmoxve "github.com/lnxbil/docker-machine-driver-proxmox-ve" 6 | ) 7 | 8 | func main() { 9 | plugin.RegisterDriver(proxmoxve.NewDriver("default", "")) 10 | } 11 | -------------------------------------------------------------------------------- /proxmox.go: -------------------------------------------------------------------------------- 1 | package dockermachinedriverproxmoxve 2 | 3 | // This file is most copy & pasted from my private project to 4 | // auto-generate the API client on basis of the JSON described API 5 | // in https://pve.proxmox.com/pve-docs/api-viewer/apidoc.js 6 | 7 | import ( 8 | "crypto/tls" 9 | "encoding/json" 10 | "fmt" 11 | "net/http" 12 | "net/url" 13 | "reflect" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | "github.com/labstack/gommon/log" 19 | resty "gopkg.in/resty.v1" 20 | ) 21 | 22 | // ProxmoxVE open api connection representation 23 | type ProxmoxVE struct { 24 | // connection parameters 25 | Username string // root 26 | password string // must be given 27 | Realm string // pam 28 | Host string 29 | Port int // default 8006 30 | 31 | // not so imported internal stuff 32 | Node string // if not present, use first node present 33 | Prefix string // if PVE is proxied, this is the added prefix 34 | CSRFPreventionToken string // filled by the framework 35 | Ticket string // filled by the framework 36 | 37 | Version string // ProxmoxVE version of the connected host 38 | 39 | client *resty.Client // resty client 40 | } 41 | 42 | // GetProxmoxVEConnectionByValues is a wrapper for GetProxmoxVEConnection with strings as input 43 | func GetProxmoxVEConnectionByValues(username string, password string, realm string, hostname string) (*ProxmoxVE, error) { 44 | return GetProxmoxVEConnection(&ProxmoxVE{ 45 | Username: username, 46 | password: password, 47 | Realm: realm, 48 | Host: hostname, 49 | }) 50 | } 51 | 52 | // GetProxmoxVEConnection retrievs a connection to a Proxmox VE host 53 | func GetProxmoxVEConnection(data *ProxmoxVE) (*ProxmoxVE, error) { 54 | if data.Port == 0 { 55 | data.Port = 8006 56 | } 57 | 58 | if len(data.password) == 0 { 59 | return data, fmt.Errorf("You have to provide a password") 60 | } 61 | 62 | if len(data.Username) == 0 { 63 | data.Username = "root" 64 | } 65 | if len(data.Realm) == 0 { 66 | data.Realm = "pam" 67 | } 68 | 69 | data.client = resty.New() 70 | 71 | data.client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) 72 | //data.client.SetTimeout(time.Duration(3 * time.Second)) 73 | 74 | outp, err := data.accessTicketPost(&AccessTicketPostParameter{ 75 | Username: data.Username, 76 | Realm: data.Realm, 77 | Password: data.password, 78 | }) 79 | 80 | if err != nil { 81 | return data, err 82 | } 83 | 84 | if outp.Csrfpreventiontoken == "" { 85 | return nil, fmt.Errorf("Could not extract CSRFPreventionToken") 86 | } 87 | 88 | data.CSRFPreventionToken = outp.Csrfpreventiontoken 89 | data.client.SetHeader("CSRFPreventionToken", outp.Csrfpreventiontoken) 90 | data.client.SetCookie(&http.Cookie{ 91 | Name: "PVEAuthCookie", 92 | Value: outp.Ticket, 93 | }) 94 | data.Ticket = outp.Ticket 95 | 96 | ver, err := data.versionGet() 97 | if err != nil { 98 | return data, err 99 | } 100 | 101 | data.Version = ver.Version 102 | 103 | return data, nil 104 | } 105 | 106 | // EnableDebugging enables Resty debugging of requests 107 | func (p ProxmoxVE) EnableDebugging() { 108 | p.client.SetLogger(log.Output()) 109 | p.client.SetDebug(true) 110 | } 111 | 112 | func (p ProxmoxVE) getURL(str string) string { 113 | return fmt.Sprintf("https://%s:%d/%sapi2/json%s", p.Host, p.Port, p.Prefix, str) 114 | } 115 | 116 | // idea taken from https://gist.github.com/tonyhb/5819315 117 | func (p ProxmoxVE) structToStringMap(i interface{}) map[string]string { 118 | retval := make(map[string]string, 0) 119 | if i == nil { 120 | return retval 121 | } 122 | iVal := reflect.ValueOf(i).Elem() 123 | typ := iVal.Type() 124 | for i := 0; i < iVal.NumField(); i++ { 125 | f := iVal.Field(i) 126 | // You ca use tags here... 127 | // tag := typ.Field(i).Tag.Get("tagname") 128 | // Convert each type into a string for the url.Values string map 129 | var v string 130 | switch f.Interface().(type) { 131 | case int, int8, int16, int32, int64: 132 | v = strconv.FormatInt(f.Int(), 10) 133 | case uint, uint8, uint16, uint32, uint64: 134 | v = strconv.FormatUint(f.Uint(), 10) 135 | case float32: 136 | v = strconv.FormatFloat(f.Float(), 'f', 4, 32) 137 | case float64: 138 | v = strconv.FormatFloat(f.Float(), 'f', 4, 64) 139 | case []byte: 140 | v = string(f.Bytes()) 141 | case string: 142 | v = f.String() 143 | case bool: 144 | // map to Proxmox VE API boolean, which is int with 1 for true and 0 for false 145 | if f.Bool() { 146 | v = "1" 147 | } else { 148 | v = "0" 149 | } 150 | } 151 | if len(v) > 0 { 152 | retval[strings.ToLower(typ.Field(i).Name)] = v 153 | } 154 | } 155 | return retval 156 | } 157 | 158 | func (p ProxmoxVE) post(input interface{}, output interface{}, path string) error { 159 | return p.runMethod("post", input, output, path) 160 | } 161 | 162 | func (p ProxmoxVE) get(input interface{}, output interface{}, path string) error { 163 | return p.runMethod("get", input, output, path) 164 | } 165 | 166 | func (p ProxmoxVE) put(input interface{}, output interface{}, path string) error { 167 | return p.runMethod("put", input, output, path) 168 | } 169 | 170 | func (p ProxmoxVE) delete(input interface{}, output interface{}, path string) error { 171 | return p.runMethod("delete", input, output, path) 172 | } 173 | 174 | func (p ProxmoxVE) runMethod(method string, input interface{}, output interface{}, path string) error { 175 | var response *resty.Response 176 | var err error 177 | 178 | switch method { 179 | case "get": 180 | response, err = p.client.R().SetQueryParams(p.structToStringMap(input)).Get(p.getURL(path)) 181 | case "post": 182 | response, err = p.client.R().SetFormData(p.structToStringMap(input)).Post(p.getURL(path)) 183 | case "put": 184 | response, err = p.client.R().SetQueryParams(p.structToStringMap(input)).Put(p.getURL(path)) 185 | case "delete": 186 | response, err = p.client.R().SetQueryParams(p.structToStringMap(input)).Delete(p.getURL(path)) 187 | default: 188 | return fmt.Errorf("method '%s' not known", method) 189 | } 190 | 191 | if err != nil { 192 | return err 193 | } 194 | code := response.StatusCode() 195 | 196 | if code < 200 || code > 300 { 197 | return fmt.Errorf("status code was '%d' and error is\n%s", code, response.Status()) 198 | } 199 | 200 | if output == nil { 201 | return nil 202 | } 203 | 204 | var f map[string]interface{} 205 | 206 | err = json.Unmarshal([]byte(response.String()), &f) 207 | if err != nil { 208 | return err 209 | } 210 | zz, err := json.Marshal(f["data"]) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | err = json.Unmarshal(zz, &output) 216 | 217 | return err 218 | } 219 | 220 | // AccessTicketPostParameter represents the input data for /access/ticket 221 | // Original Description: 222 | // Create or verify authentication ticket. 223 | type AccessTicketPostParameter struct { 224 | Privs string // optional 225 | Realm string // optional 226 | Username string 227 | OTP string // optional 228 | Password string 229 | Path string // optional 230 | } 231 | 232 | // AccessTicketReturnParameter represents the returned data from /access/ticket 233 | // Original Description: 234 | // Create or verify authentication ticket. 235 | type AccessTicketReturnParameter struct { 236 | Username string 237 | Csrfpreventiontoken string 238 | Ticket string 239 | } 240 | 241 | // AccessTicketPost access the API 242 | // Create or verify authentication ticket. 243 | func (p ProxmoxVE) accessTicketPost(input *AccessTicketPostParameter) (*AccessTicketReturnParameter, error) { 244 | path := "/access/ticket" 245 | outp := AccessTicketReturnParameter{} 246 | err := p.post(input, &outp, path) 247 | return &outp, err 248 | } 249 | 250 | // VersionReturnParameter represents the returned data from /version 251 | // Original Description: 252 | // API version details. The result also includes the global datacenter confguration. 253 | type VersionReturnParameter struct { 254 | RepoID string 255 | Version string 256 | Release string 257 | } 258 | 259 | // VersionGet access the API 260 | // API version details. The result also includes the global datacenter confguration. 261 | func (p ProxmoxVE) versionGet() (*VersionReturnParameter, error) { 262 | path := "/version" 263 | outp := VersionReturnParameter{} 264 | err := p.get(nil, &outp, path) 265 | return &outp, err 266 | } 267 | 268 | // NodesNodeStorageStorageContentPostParameter represents the input data for /nodes/{node}/storage/{storage}/content 269 | // Original Description: 270 | // Allocate disk images. 271 | type NodesNodeStorageStorageContentPostParameter struct { 272 | Filename string // The name of the file to create. 273 | Size string // Size in kilobyte (1024 bytes). Optional suffixes 'M' (megabyte, 1024K) and 'G' (gigabyte, 1024M) 274 | VMID string // Specify owner VM 275 | Format string // optional, 276 | } 277 | 278 | // NodesNodeStorageStorageContentPost access the API 279 | // Allocate disk images. 280 | func (p ProxmoxVE) NodesNodeStorageStorageContentPost(node string, storage string, input *NodesNodeStorageStorageContentPostParameter) (diskname string, err error) { 281 | path := fmt.Sprintf("/nodes/%s/storage/%s/content", node, storage) 282 | err = p.post(input, &diskname, path) 283 | return diskname, err 284 | } 285 | 286 | // ClusterNextIDGet Get next free VMID. If you pass an VMID it will raise an error if the ID is already used. 287 | func (p ProxmoxVE) ClusterNextIDGet(id int) (vmid string, err error) { 288 | path := "/cluster/nextid" 289 | 290 | if id == 0 { 291 | err = p.get(nil, &vmid, path) 292 | } else { 293 | input := struct { 294 | VMID int 295 | }{ 296 | VMID: id, 297 | } 298 | err = p.get(&input, &vmid, path) 299 | } 300 | return vmid, err 301 | } 302 | 303 | // NodesNodeQemuPostParameter represents the input data for /nodes/{node}/qemu 304 | // Original Description: 305 | // Create or restore a virtual machine. 306 | type NodesNodeQemuPostParameter struct { 307 | VMID string // The (unique) ID of the VM. 308 | Memory int `json:"memory,omitempty"` // optional, Amount of RAM for the VM in MB. This is the maximum available memory when you use the balloon device. 309 | Autostart string // optional, Automatic restart after crash (currently ignored). 310 | Agent string // optional, Enable/disable Qemu GuestAgent. 311 | Net0 string 312 | Name string // optional, Set a name for the VM. Only used on the configuration web interface. 313 | SCSI0 string // optional, Use volume as VIRTIO hard disk (n is 0 to 15). 314 | Ostype string // optional, Specify guest operating system. 315 | KVM string // optional, Enable/disable KVM hardware virtualization. 316 | Pool string // optional, Add the VM to the specified pool. 317 | Sockets string `json:"sockets,omitempty"` // optional, The number of cpus. 318 | Cores string `json:"cores,omitempty"` // optional, The number of cores per socket. 319 | Cdrom string // optional, This is an alias for option -ide2 320 | Ide3 string 321 | Citype string // Specifies the cloud-init configuration format. 322 | Scsihw string // SCSI controller model. 323 | Onboot string // Specifies whether a VM will be started during system bootup. 324 | Protection string // Sets the protection flag of the VM. This will disable the remove VM and remove disk operations. 325 | NUMA string // Enable/disable NUMA 326 | CPU string // Emulated CPU type. 327 | } 328 | 329 | type nNodesNodeQemuPostParameter struct { 330 | VMID string // The (unique) ID of the VM. 331 | Acpi bool // optional, Enable/disable ACPI. 332 | Agent string // optional, Enable/disable Qemu GuestAgent. 333 | Archive string // optional, The backup file. 334 | Args string // optional, Arbitrary arguments passed to kvm. 335 | Autostart string // optional, Automatic restart after crash (currently ignored). 336 | Balloon int // optional, Amount of target RAM for the VM in MB. Using zero disables the ballon driver. 337 | Bios string // optional, Select BIOS implementation. 338 | Boot string // optional, Boot on floppy (a), hard disk (c), CD-ROM (d), or network (n). 339 | Bootdisk string // optional, Enable booting from specified disk. 340 | Cdrom string // optional, This is an alias for option -ide2 341 | Cores string // optional, The number of cores per socket. 342 | CPU string // optional, Emulated CPU type. 343 | Cpulimit int // optional, Limit of CPU usage. 344 | Cpuunits int // optional, CPU weight for a VM. 345 | Description string // optional, Description for the VM. Only used on the configuration web interface. This is saved as comment inside the configuration file. 346 | Force bool // optional, Allow to overwrite existing VM. 347 | Freeze bool // optional, Freeze CPU at startup (use 'c' monitor command to start execution). 348 | Hostpci []string // optional, Map host PCI devices into guest. 349 | Hotplug string // optional, Selectively enable hotplug features. This is a comma separated list of hotplug features: 'network', 'disk', 'cpu', 'memory' and 'usb'. Use '0' to disable hotplug completely. Value '1' is an alias for the default 'network,disk,usb'. 350 | Hugepages string // optional, Enable/disable hugepages memory. 351 | IDE []string // optional, Use volume as IDE hard disk or CD-ROM (n is 0 to 3). 352 | Keyboard string // optional, Keybord layout for vnc server. Default is read from the '/etc/pve/datacenter.conf' configuration file. 353 | KVM bool // optional, Enable/disable KVM hardware virtualization. 354 | Localtime bool // optional, Set the real time clock to local time. This is enabled by default if ostype indicates a Microsoft OS. 355 | Lock string // optional, Lock/unlock the VM. 356 | Machine string // optional, Specific the Qemu machine type. 357 | Memory string // optional, Amount of RAM for the VM in MB. This is the maximum available memory when you use the balloon device. 358 | MigrateDowntime int // optional, Set maximum tolerated downtime (in seconds) for migrations. 359 | MigrateSpeed int // optional, Set maximum speed (in MB/s) for migrations. Value 0 is no limit. 360 | Name string // optional, Set a name for the VM. Only used on the configuration web interface. 361 | Net0 string 362 | //NET []string // optional, Specify network devices. 363 | // numa is defined more than once, we ignore the bool parameter 364 | //Numa bool // optional, Enable/disable NUMA. 365 | Numa []string // optional, NUMA topology. 366 | Onboot bool // optional, Specifies whether a VM will be started during system bootup. 367 | Ostype string // optional, Specify guest operating system. 368 | Parallel []string // optional, Map host parallel devices (n is 0 to 2). 369 | Pool string // optional, Add the VM to the specified pool. 370 | Protection bool // optional, Sets the protection flag of the VM. This will disable the remove VM and remove disk operations. 371 | Reboot bool // optional, Allow reboot. If set to '0' the VM exit on reboot. 372 | Sata []string // optional, Use volume as SATA hard disk or CD-ROM (n is 0 to 5). 373 | Scsi []string // optional, Use volume as SCSI hard disk or CD-ROM (n is 0 to 13). 374 | Scsihw string // optional, SCSI controller model 375 | Serial []string // optional, Create a serial device inside the VM (n is 0 to 3) 376 | Shares int // optional, Amount of memory shares for auto-ballooning. The larger the number is, the more memory this VM gets. Number is relative to weights of all other running VMs. Using zero disables auto-ballooning 377 | Smbios1 string // optional, Specify SMBIOS type 1 fields. 378 | SMP int // optional, The number of CPUs. Please use option -sockets instead. 379 | Sockets string // optional, The number of CPU sockets. 380 | Startdate string // optional, Set the initial date of the real time clock. Valid format for date are: 'now' or '2006-06-17T16:01:21' or '2006-06-17'. 381 | Startup string // optional, Startup and shutdown behavior. Order is a non-negative number defining the general startup order. Shutdown in done with reverse ordering. Additionally you can set the 'up' or 'down' delay in seconds, which specifies a delay to wait before the next VM is started or stopped. 382 | Storage string // optional, Default storage. 383 | Tablet bool // optional, Enable/disable the USB tablet device. 384 | TDF bool // optional, Enable/disable time drift fix. 385 | Template bool // optional, Enable/disable Template. 386 | Unique bool // optional, Assign a unique random ethernet address. 387 | Unused []string // optional, Reference to unused volumes. This is used internally, and should not be modified manually. 388 | USB []string // optional, Configure an USB device (n is 0 to 4). 389 | Vcpus int // optional, Number of hotplugged vcpus. 390 | VGA string // optional, Select the VGA type. 391 | Virtio []string // optional, Use volume as VIRTIO hard disk (n is 0 to 15). 392 | VMstatestorage string // optional, Default storage for VM state volumes/files. 393 | Watchdog string // optional, Create a virtual hardware watchdog device. 394 | } 395 | 396 | // NodesNodeQemuPost access the API 397 | // Create or restore a virtual machine. 398 | func (p ProxmoxVE) NodesNodeQemuPost(node string, input *NodesNodeQemuPostParameter) (taskid string, err error) { 399 | path := fmt.Sprintf("/nodes/%s/qemu", node) 400 | err = p.post(input, &taskid, path) 401 | return taskid, err 402 | } 403 | 404 | // NodesNodeQemuVMIDClonePostParameter represents the input data for /nodes/{node}/qemu/{vmid}/clone 405 | // Original Description: 406 | // Create a copy of virtual machine/template. 407 | type NodesNodeQemuVMIDClonePostParameter struct { 408 | Newid string // VMID for the clone. 409 | VMID string // The (unique) ID of the VM. 410 | Name string // Set a name for the new VM. 411 | Pool string // Add the new VM to the specified pool. 412 | Full string // Create a full copy of all disks. 413 | Storage string // Target storage for full clone. 414 | Format string // Target format for file storage. Only valid for full clone. 415 | } 416 | 417 | // NodesNodeQemuVMIDClonePost access the API 418 | // Create a copy of virtual machine/template. 419 | func (p ProxmoxVE) NodesNodeQemuVMIDClonePost(node string, vmid string, input *NodesNodeQemuVMIDClonePostParameter) (taskid string, err error) { 420 | path := fmt.Sprintf("/nodes/%s/qemu/%s/clone", node, vmid) 421 | err = p.post(input, &taskid, path) 422 | return taskid, err 423 | } 424 | 425 | // NodesNodeQemuVMIDResizePutParameter represents the input data for /nodes/{node}/qemu/{vmid}/resize 426 | // Original Description: 427 | // Extend volume size. 428 | type NodesNodeQemuVMIDResizePutParameter struct { 429 | Disk string // The disk you want to resize. 430 | Size string // The new size. 431 | } 432 | 433 | // NodesNodeQemuVMIDResizePut access the API 434 | // Extend volume size. 435 | func (p ProxmoxVE) NodesNodeQemuVMIDResizePut(node string, vmid string, input *NodesNodeQemuVMIDResizePutParameter) (err error) { 436 | path := fmt.Sprintf("/nodes/%s/qemu/%s/resize", node, vmid) 437 | err = p.put(input, nil, path) 438 | return err 439 | } 440 | 441 | // NodesNodeQemuVMIDConfigPost access the API 442 | // Set config options 443 | func (p ProxmoxVE) NodesNodeQemuVMIDConfigPost(node string, vmid string, input *NodesNodeQemuPostParameter) (taskid string, err error) { 444 | path := fmt.Sprintf("/nodes/%s/qemu/%s/config", node, vmid) 445 | err = p.post(input, &taskid, path) 446 | return taskid, err 447 | } 448 | 449 | // NodesNodeQemuVMIDConfigSetSSHKeys access the API 450 | // Set config options 451 | // https://forum.proxmox.com/threads/how-to-use-pvesh-set-vms-sshkeys.52570/ 452 | // cray encoding style *AND* double-encoded 453 | func (p ProxmoxVE) NodesNodeQemuVMIDConfigSetSSHKeys(node string, vmid string, SSHKeys string) (taskid string, err error) { 454 | r := strings.NewReplacer("+", "%2B", "=", "%3D", "@", "%40") 455 | 456 | path := fmt.Sprintf("/nodes/%s/qemu/%s/config", node, vmid) 457 | SSHKeys = url.PathEscape(SSHKeys) 458 | SSHKeys = r.Replace(SSHKeys) 459 | 460 | SSHKeys = url.PathEscape(SSHKeys) 461 | SSHKeys = r.Replace(SSHKeys) 462 | 463 | response, err := p.client.R().SetHeader("Content-Type", "application/x-www-form-urlencoded").SetBody("sshkeys=" + SSHKeys).Post(p.getURL(path)) 464 | 465 | if err != nil { 466 | return "", err 467 | } 468 | code := response.StatusCode() 469 | 470 | if code < 200 || code > 300 { 471 | return "", fmt.Errorf("status code was '%d' and error is\n%s", code, response.Status()) 472 | } 473 | 474 | var f map[string]interface{} 475 | 476 | err = json.Unmarshal([]byte(response.String()), &f) 477 | if err != nil { 478 | return "", err 479 | } 480 | zz, err := json.Marshal(f["data"]) 481 | if err != nil { 482 | return "", err 483 | } 484 | 485 | err = json.Unmarshal(zz, &taskid) 486 | 487 | return taskid, err 488 | } 489 | 490 | // NodesNodeQemuVMIDConfigGet access the API 491 | // Get the virtual machine configuration with pending configuration changes applied. Set the 'current' parameter to get the current configuration instead. 492 | func (p ProxmoxVE) NodesNodeQemuVMIDConfigGet(node string, vmid string) (err error) { 493 | path := fmt.Sprintf("/nodes/%s/qemu/%s/config", node, vmid) 494 | var i, r interface{} 495 | err = p.get(i, &r, path) 496 | return err 497 | } 498 | 499 | // NodesNodeQemuVMIDAgentPostParameter represents the input data for /nodes/{node}/qemu/{vmid}/agent 500 | // Original Description: 501 | // Execute Qemu Guest Agent commands. 502 | type NodesNodeQemuVMIDAgentPostParameter struct { 503 | Command string // The QGA command. 504 | } 505 | 506 | // NodesNodeQemuVMIDAgentPost access the API 507 | // Execute Qemu Guest Agent commands. 508 | func (p ProxmoxVE) NodesNodeQemuVMIDAgentPost(node string, vmid string, input *NodesNodeQemuVMIDAgentPostParameter) error { 509 | path := fmt.Sprintf("/nodes/%s/qemu/%s/agent", node, vmid) 510 | err := p.post(input, nil, path) 511 | return err 512 | } 513 | 514 | // NodesNodeQemuVMIDDelete access the API 515 | // Destroy the vm (also delete all used/owned volumes). 516 | func (p ProxmoxVE) NodesNodeQemuVMIDDelete(node string, vmid string) (taskid string, err error) { 517 | p.NodesNodeQemuVMIDStatusStopPost(node, vmid) 518 | time.Sleep(time.Second) 519 | 520 | path := fmt.Sprintf("/nodes/%s/qemu/%s", node, vmid) 521 | err = p.delete(nil, &taskid, path) 522 | return taskid, err 523 | } 524 | 525 | // NodesNodeQemuVMIDStatusRebootPost access the API 526 | // Reboot the VM by shutting it down, and starting it again. Applies pending changes. 527 | func (p ProxmoxVE) NodesNodeQemuVMIDStatusRebootPost(node string, vmid string) (taskid string, err error) { 528 | path := fmt.Sprintf("/nodes/%s/qemu/%s/status/reboot", node, vmid) 529 | err = p.post(nil, &taskid, path) 530 | return taskid, err 531 | } 532 | 533 | // NodesNodeQemuVMIDStatusResetPost access the API 534 | // Reset virtual machine. 535 | func (p ProxmoxVE) NodesNodeQemuVMIDStatusResetPost(node string, vmid string) (taskid string, err error) { 536 | path := fmt.Sprintf("/nodes/%s/qemu/%s/status/reset", node, vmid) 537 | err = p.post(nil, &taskid, path) 538 | return taskid, err 539 | } 540 | 541 | // NodesNodeQemuVMIDStatusResumePost access the API 542 | // Resume virtual machine. 543 | func (p ProxmoxVE) NodesNodeQemuVMIDStatusResumePost(node string, vmid string) (taskid string, err error) { 544 | path := fmt.Sprintf("/nodes/%s/qemu/%s/status/resume", node, vmid) 545 | err = p.post(nil, &taskid, path) 546 | return taskid, err 547 | } 548 | 549 | // NodesNodeQemuVMIDStatusShutdownPost access the API 550 | // Shutdown virtual machine. This is similar to pressing the power button on a physical machine.This will send an ACPI event for the guest OS, which should then proceed to a clean shutdown. 551 | func (p ProxmoxVE) NodesNodeQemuVMIDStatusShutdownPost(node string, vmid string) (taskid string, err error) { 552 | path := fmt.Sprintf("/nodes/%s/qemu/%s/status/shutdown", node, vmid) 553 | err = p.post(nil, &taskid, path) 554 | return taskid, err 555 | } 556 | 557 | // NodesNodeQemuVMIDStatusStartPost access the API 558 | // Start virtual machine. 559 | func (p ProxmoxVE) NodesNodeQemuVMIDStatusStartPost(node string, vmid string) (taskid string, err error) { 560 | path := fmt.Sprintf("/nodes/%s/qemu/%s/status/start", node, vmid) 561 | err = p.post(nil, &taskid, path) 562 | return taskid, err 563 | } 564 | 565 | // NodesNodeQemuVMIDStatusStopPost access the API 566 | // Stop virtual machine. The qemu process will exit immediately. This is akin to pulling the power plug of a running computer and may damage the VM data 567 | func (p ProxmoxVE) NodesNodeQemuVMIDStatusStopPost(node string, vmid string) (taskid string, err error) { 568 | path := fmt.Sprintf("/nodes/%s/qemu/%s/status/stop", node, vmid) 569 | err = p.post(nil, &taskid, path) 570 | return taskid, err 571 | } 572 | 573 | // NodesNodeQemuVMIDStatusSuspendPost access the API 574 | // Suspend virtual machine. 575 | func (p ProxmoxVE) NodesNodeQemuVMIDStatusSuspendPost(node string, vmid string) (taskid string, err error) { 576 | path := fmt.Sprintf("/nodes/%s/qemu/%s/status/suspend", node, vmid) 577 | err = p.post(nil, &taskid, path) 578 | return taskid, err 579 | } 580 | 581 | func unmarshallString(data string, value string) (string, error) { 582 | var f map[string]interface{} 583 | err := json.Unmarshal([]byte(data), &f) 584 | if err != nil { 585 | return "", err 586 | } 587 | zz, err := json.Marshal(f["value"]) 588 | if err != nil { 589 | return "", err 590 | } 591 | return string(zz), err 592 | } 593 | 594 | // IPReturn represents the result from the qemu-guest-agent call network-get-interfaces 595 | type IPReturn struct { 596 | Data struct { 597 | Result []struct { 598 | HardwareAddress string `json:"hardware-address"` 599 | Name string `json:"name"` 600 | IPAdresses []struct { 601 | IPAddress string `json:"ip-address"` 602 | IPAddressType string `json:"ip-address-type"` 603 | Prefix int `json:"prefix"` 604 | } `json:"ip-addresses"` 605 | } `json:"result"` 606 | } `json:"data"` 607 | } 608 | 609 | // GetEth0IPv4 access the API 610 | func (p ProxmoxVE) GetEth0IPv4(node string, vmid string) (string, error) { 611 | input := NodesNodeQemuVMIDAgentPostParameter{Command: "network-get-interfaces"} 612 | path := fmt.Sprintf("/nodes/%s/qemu/%s/agent", node, vmid) 613 | 614 | response, err := p.client.R().SetQueryParams(p.structToStringMap(&input)).Post(p.getURL(path)) 615 | 616 | var a IPReturn 617 | resp := response.String() 618 | err = json.Unmarshal([]byte(resp), &a) 619 | if err != nil { 620 | return "", err 621 | } 622 | for _, nic := range a.Data.Result { 623 | if nic.Name == "eth0" { 624 | for _, ip := range nic.IPAdresses { 625 | if ip.IPAddressType == "ipv4" { 626 | return ip.IPAddress, nil 627 | } 628 | } 629 | } 630 | } 631 | 632 | return "", err 633 | } 634 | 635 | // NodesNodeQemuVMIDStatusCurrentGet access the API 636 | // Get virtual machine status. 637 | func (p ProxmoxVE) NodesNodeQemuVMIDStatusCurrentGet(node string, vmid string) (string, error) { 638 | path := fmt.Sprintf("/nodes/%s/qemu/%s/status/current", node, vmid) 639 | response, err := p.client.R().Get(p.getURL(path)) 640 | var f map[string]interface{} 641 | 642 | err = json.Unmarshal([]byte(response.String()), &f) 643 | if err != nil { 644 | return "", err 645 | } 646 | 647 | zz, err := json.Marshal(f["data"]) 648 | if err != nil { 649 | return "", err 650 | } 651 | 652 | err = json.Unmarshal(zz, &f) 653 | if err != nil { 654 | return "", err 655 | } 656 | 657 | return f["status"].(string), nil 658 | } 659 | 660 | // IntBool represents a bool value as seen by the PERL API 661 | type IntBool bool 662 | 663 | // UnmarshalJSON for Integer-based boolean values returned from the API 664 | func (bit IntBool) UnmarshalJSON(data []byte) error { 665 | asString := string(data) 666 | if asString == "1" || asString == "true" { 667 | bit = true 668 | } else if asString == "0" || asString == "false" { 669 | bit = false 670 | } else { 671 | return fmt.Errorf("Boolean unmarshal error: invalid input %s", asString) 672 | } 673 | return nil 674 | } 675 | 676 | // ConfigReturn represents the config response from the API 677 | type ConfigReturn struct { 678 | Data struct { 679 | OSType string `json:"ostype"` 680 | SCSI0 string `json:"scsi0"` 681 | CPU string `json:"cpu"` 682 | ONBoot IntBool `json:"onboot"` 683 | SSHKeys string `json:"sshkeys"` 684 | Smbios1 string // optional, Specify SMBIOS type 1 fields. 685 | } `json:"data"` 686 | } 687 | 688 | // GetConfig returns the vm configuration data 689 | func (p ProxmoxVE) GetConfig(node string, vmid string) (ConfigReturn, error) { 690 | path := fmt.Sprintf("/nodes/%s/qemu/%s/config", node, vmid) 691 | 692 | var a ConfigReturn 693 | 694 | response, err := p.client.R().Get(p.getURL(path)) 695 | 696 | if err != nil { 697 | return a, err 698 | } 699 | code := response.StatusCode() 700 | 701 | if code < 200 || code > 300 { 702 | return a, fmt.Errorf("status code was '%d' and error is\n%s", code, response.Status()) 703 | } 704 | 705 | resp := response.String() 706 | err = json.Unmarshal([]byte(resp), &a) 707 | 708 | return a, err 709 | } 710 | 711 | // StorageReturn represents the storage response from the API 712 | type StorageReturn struct { 713 | Data []struct { 714 | Active int `json:"active"` 715 | Avail int `json:"avail"` 716 | Content string `json:"content"` 717 | Enabled IntBool `json:"enabled"` 718 | Shared IntBool `json:"shared"` 719 | Storage string `json:"storage"` 720 | Total int `json:"total"` 721 | Type string `json:"type"` 722 | Used int `json:"used"` 723 | } `json:"data"` 724 | } 725 | 726 | // GetStorageType returns the storage type as string 727 | func (p ProxmoxVE) GetStorageType(node string, storagename string) (string, error) { 728 | path := fmt.Sprintf("/nodes/%s/storage", node) 729 | 730 | response, err := p.client.R().Get(p.getURL(path)) 731 | 732 | var a StorageReturn 733 | resp := response.String() 734 | err = json.Unmarshal([]byte(resp), &a) 735 | if err != nil { 736 | return "", err 737 | } 738 | 739 | for _, storage := range a.Data { 740 | if storage.Storage == storagename { 741 | return storage.Type, nil 742 | } 743 | } 744 | return "", fmt.Errorf("storage '%s' not found", storagename) 745 | } 746 | 747 | // TaskStatusReturn represents a status return message from the API 748 | type TaskStatusReturn struct { 749 | UPID string `json:"upid"` 750 | Node string `json:"node"` 751 | User string `json:"user"` 752 | PID int `json:"pid"` 753 | ID string `json:"id"` 754 | StartTime int `json:"starttime"` 755 | Exitstatus string `json:"exitstatus"` 756 | Type string `json:"type"` 757 | PStart int `json:"pstart"` 758 | Status string `json:"status"` 759 | } 760 | 761 | // WaitForTaskToComplete waits until the given task in taskid is finished (exited or otherwise) 762 | func (p ProxmoxVE) WaitForTaskToComplete(node string, taskid string) error { 763 | path := fmt.Sprintf("/nodes/%s/tasks/%s/status", node, taskid) 764 | 765 | tsr := TaskStatusReturn{} 766 | 767 | for true { 768 | log.Infof("Waiting for task %s to finish", taskid) 769 | err := p.get(nil, &tsr, path) 770 | if err != nil { 771 | log.Infof("Got on read: %s", err.Error()) 772 | return err 773 | } 774 | log.Infof("Status is %s", tsr.Status) 775 | if tsr.Status != "running" { 776 | if tsr.Exitstatus != "OK" { 777 | return fmt.Errorf("%s -> %s: %s", tsr.Type, tsr.Status, tsr.Exitstatus) 778 | } 779 | log.Infof("exiting with %s", tsr.Exitstatus) 780 | return nil 781 | } 782 | log.Info("still running, waiting 500ms") 783 | time.Sleep(500 * time.Millisecond) 784 | } 785 | // unreachable code 786 | return nil 787 | } 788 | -------------------------------------------------------------------------------- /proxmox_test.go: -------------------------------------------------------------------------------- 1 | package dockermachinedriverproxmoxve_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | dockermachinedriverproxmoxve "github.com/lnxbil/docker-machine-driver-proxmox-ve" 10 | ) 11 | 12 | func TestSuccessfulConnection(t *testing.T) { 13 | api := EstablishConnection(t) 14 | 15 | val, err := strconv.ParseFloat(api.Version, 32) 16 | if err != nil { 17 | t.Error("Error occured") 18 | t.Error(err) 19 | } 20 | if val < 5.0 { 21 | t.Errorf("API version should be '5.x', but was '%f'", val) 22 | } 23 | } 24 | func TestWrongPass(t *testing.T) { 25 | username, _, realm, host := GetProxmoxAccess() 26 | _, err := dockermachinedriverproxmoxve.GetProxmoxVEConnectionByValues(username, "wrong_password", realm, host) 27 | if err == nil { 28 | t.Log(err) 29 | t.Error() 30 | } 31 | } 32 | func TestWrongUser(t *testing.T) { 33 | _, password, realm, host := GetProxmoxAccess() 34 | _, err := dockermachinedriverproxmoxve.GetProxmoxVEConnectionByValues("root", password, realm, host) 35 | if err == nil { 36 | t.Log(err) 37 | t.Error() 38 | } 39 | } 40 | 41 | func TestEmptyPass(t *testing.T) { 42 | username, _, realm, host := GetProxmoxAccess() 43 | _, err := dockermachinedriverproxmoxve.GetProxmoxVEConnectionByValues(username, "", realm, host) 44 | if err != nil && err.Error() != "You have to provide a password" { 45 | t.Log(err) 46 | t.Error() 47 | } 48 | } 49 | 50 | func TestWrongHost(t *testing.T) { 51 | username, password, realm, _ := GetProxmoxAccess() 52 | _, err := dockermachinedriverproxmoxve.GetProxmoxVEConnectionByValues(username, password, realm, "127.0.0.1") 53 | if err == nil { 54 | t.Log(err) 55 | t.Error() 56 | } 57 | } 58 | 59 | func checkStorageType(t *testing.T, api *dockermachinedriverproxmoxve.ProxmoxVE, storageName string, shouldStorageType string) error { 60 | ret, err := api.GetStorageType(GetProxmoxNode(), storageName) 61 | if err != nil { 62 | return err 63 | } 64 | if ret != shouldStorageType { 65 | return errors.New(fmt.Sprintf("storage type should have been '%s', but was '%s' for storage '%s'", shouldStorageType, ret, storageName)) 66 | } 67 | return nil 68 | } 69 | 70 | func TestStorageType(t *testing.T) { 71 | api := EstablishConnection(t) 72 | 73 | err := checkStorageType(t, api, "local-lvm", "lvmthin") 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | err = checkStorageType(t, api, "local", "dir") 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | err = checkStorageType(t, api, "nfs", "nfs") 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | err = checkStorageType(t, api, "zpool", "zfspool") 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | err = checkStorageType(t, api, "lvm", "lvm") 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | err = checkStorageType(t, api, "not-existent", "2") 99 | if err == nil { 100 | t.Fatalf("non-existent storage should have raised an error") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /proxmoxdriver.go: -------------------------------------------------------------------------------- 1 | package dockermachinedriverproxmoxve 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | mrand "math/rand" 13 | "net/url" 14 | "os" 15 | "regexp" 16 | "strconv" 17 | "strings" 18 | "time" 19 | 20 | "golang.org/x/crypto/ssh" 21 | resty "gopkg.in/resty.v1" 22 | 23 | sshrw "github.com/mosolovsa/go_cat_sshfilerw" 24 | 25 | "github.com/docker/machine/libmachine/drivers" 26 | "github.com/docker/machine/libmachine/mcnflag" 27 | mssh "github.com/docker/machine/libmachine/ssh" 28 | "github.com/docker/machine/libmachine/state" 29 | "github.com/labstack/gommon/log" 30 | ) 31 | 32 | // Driver for Proxmox VE 33 | type Driver struct { 34 | *drivers.BaseDriver 35 | driver *ProxmoxVE 36 | 37 | // Top-level strategy for proisioning a new node 38 | ProvisionStrategy string 39 | 40 | // Basic Authentication for Proxmox VE 41 | Host string // Host to connect to 42 | Node string // optional, node to create VM on, host used if omitted but must match internal node name 43 | User string // username 44 | Password string // password 45 | Realm string // realm, e.g. pam, pve, etc. 46 | 47 | // File to load as boot image RancherOS/Boot2Docker 48 | ImageFile string // in the format :iso/.iso 49 | 50 | Pool string // pool to add the VM to (necessary for users with only pool permission) 51 | Storage string // internal PVE storage name 52 | StorageType string // Type of the storage (currently QCOW2 and RAW) 53 | DiskSize string // disk size in GB 54 | Memory int // memory in GB 55 | StorageFilename string 56 | Onboot string // Specifies whether a VM will be started during system bootup. 57 | Protection string // Sets the protection flag of the VM. This will disable the remove VM and remove disk operations. 58 | Citype string // Specifies the cloud-init configuration format. 59 | NUMA string // Enable/disable NUMA 60 | 61 | CiEnabled string 62 | 63 | NetModel string // Net Interface Model, [e1000, virtio, realtek, etc...] 64 | NetFirewall string // Enable/disable firewall 65 | NetMtu string // set nic MTU 66 | NetBridge string // bridge applied to network interface 67 | NetVlanTag int // vlan tag 68 | 69 | ScsiController string 70 | ScsiAttributes string 71 | 72 | VMID string // VM ID only filled by create() 73 | VMIDRange string // acceptable range of VMIDs 74 | VMUUID string // UUID to confirm 75 | CloneVMID string // VM ID to clone 76 | CloneFull int // Make a full (detached) clone from parent (defaults to true if VMID is not a template, otherwise false) 77 | GuestUsername string // user to log into the guest OS to copy the public key 78 | GuestPassword string // password to log into the guest OS to copy the public key 79 | GuestSSHPort int // ssh port to log into the guest OS to copy the public key 80 | CPU string // Emulated CPU type. 81 | CPUSockets string // The number of cpu sockets. 82 | CPUCores string // The number of cores per socket. 83 | driverDebug bool // driver debugging 84 | restyDebug bool // enable resty debugging 85 | } 86 | 87 | func (d *Driver) debugf(format string, v ...interface{}) { 88 | if d.driverDebug { 89 | log.Infof(format, v...) 90 | } 91 | } 92 | 93 | func (d *Driver) debug(v ...interface{}) { 94 | if d.driverDebug { 95 | log.Info(v...) 96 | } 97 | } 98 | 99 | func (d *Driver) connectAPI() error { 100 | if d.driver == nil { 101 | d.debugf("Create called") 102 | 103 | d.debugf("Connecting to %s as %s@%s with password '%s'", d.Host, d.User, d.Realm, d.Password) 104 | c, err := GetProxmoxVEConnectionByValues(d.User, d.Password, d.Realm, d.Host) 105 | d.driver = c 106 | if err != nil { 107 | return fmt.Errorf("Could not connect to host '%s' with '%s@%s'", d.Host, d.User, d.Realm) 108 | } 109 | if d.restyDebug { 110 | c.EnableDebugging() 111 | } 112 | d.debugf("Connected to PVE version '" + d.driver.Version + "'") 113 | } 114 | return nil 115 | } 116 | 117 | // GetCreateFlags returns the argument flags for the program 118 | func (d *Driver) GetCreateFlags() []mcnflag.Flag { 119 | return []mcnflag.Flag{ 120 | mcnflag.StringFlag{ 121 | EnvVar: "PROXMOXVE_PROXMOX_HOST", 122 | Name: "proxmoxve-proxmox-host", 123 | Usage: "Host to connect to", 124 | Value: "192.168.1.253", 125 | }, 126 | mcnflag.StringFlag{ 127 | EnvVar: "PROXMOXVE_PROXMOX_NODE", 128 | Name: "proxmoxve-proxmox-node", 129 | Usage: "Node to use (defaults to host)", 130 | Value: "", 131 | }, 132 | mcnflag.StringFlag{ 133 | EnvVar: "PROXMOXVE_PROVISION_STRATEGY", 134 | Name: "proxmoxve-provision-strategy", 135 | Usage: "Provision strategy (cdrom|clone)", 136 | Value: "cdrom", 137 | }, 138 | mcnflag.StringFlag{ 139 | EnvVar: "PROXMOXVE_PROXMOX_USER_NAME", 140 | Name: "proxmoxve-proxmox-user-name", 141 | Usage: "User to connect as", 142 | Value: "root", 143 | }, 144 | mcnflag.StringFlag{ 145 | EnvVar: "PROXMOXVE_PROXMOX_USER_PASSWORD", 146 | Name: "proxmoxve-proxmox-user-password", 147 | Usage: "Password to connect with", 148 | Value: "", 149 | }, 150 | mcnflag.StringFlag{ 151 | EnvVar: "PROXMOXVE_PROXMOX_REALM", 152 | Name: "proxmoxve-proxmox-realm", 153 | Usage: "Realm to connect to (default: pam)", 154 | Value: "pam", 155 | }, 156 | mcnflag.StringFlag{ 157 | EnvVar: "PROXMOXVE_PROXMOX_POOL", 158 | Name: "proxmoxve-proxmox-pool", 159 | Usage: "pool to attach to", 160 | Value: "", 161 | }, 162 | mcnflag.StringFlag{ 163 | EnvVar: "PROXMOXVE_VM_VMID_RANGE", 164 | Name: "proxmoxve-vm-vmid-range", 165 | Usage: "range of acceptable vmid values [:]", 166 | Value: "", 167 | }, 168 | mcnflag.StringFlag{ 169 | EnvVar: "PROXMOXVE_VM_STORAGE_PATH", 170 | Name: "proxmoxve-vm-storage-path", 171 | Usage: "storage to create the VM volume on", 172 | Value: "", // leave the flag default value blank to support the clone default behavior if not explicity set of 'use what is most appropriate' 173 | }, 174 | mcnflag.StringFlag{ 175 | EnvVar: "PROXMOXVE_VM_STORAGE_SIZE", 176 | Name: "proxmoxve-vm-storage-size", 177 | Usage: "disk size in GB", 178 | Value: "16", 179 | }, 180 | mcnflag.StringFlag{ 181 | EnvVar: "PROXMOXVE_VM_STORAGE_TYPE", 182 | Name: "proxmoxve-vm-storage-type", 183 | Usage: "storage type to use (QCOW2 or RAW)", 184 | Value: "", // leave the flag default value blank to support the clone default behavior if not explicity set of 'use what is most appropriate' 185 | }, 186 | mcnflag.StringFlag{ 187 | EnvVar: "PROXMOXVE_VM_SCSI_CONTROLLER", 188 | Name: "proxmoxve-vm-scsi-controller", 189 | Usage: "scsi controller model (default: virtio-scsi-pci)", 190 | Value: "virtio-scsi-pci", 191 | }, 192 | mcnflag.StringFlag{ 193 | EnvVar: "PROXMOXVE_VM_SCSI_ATTRIBUTES", 194 | Name: "proxmoxve-vm-scsi-attributes", 195 | Usage: "scsi0 attributes", 196 | Value: "", 197 | }, 198 | mcnflag.IntFlag{ 199 | EnvVar: "PROXMOXVE_VM_MEMORY", 200 | Name: "proxmoxve-vm-memory", 201 | Usage: "memory in GB", 202 | Value: 8, 203 | }, 204 | mcnflag.StringFlag{ 205 | EnvVar: "PROXMOXVE_VM_NUMA", 206 | Name: "proxmoxve-vm-numa", 207 | Usage: "enable/disable NUMA", 208 | Value: "", // leave the flag default value blank to support the clone default behavior if not explicity set of 'use what is most appropriate' 209 | }, 210 | mcnflag.StringFlag{ 211 | EnvVar: "PROXMOXVE_VM_CPU", 212 | Name: "proxmoxve-vm-cpu", 213 | Usage: "Emulatd CPU", 214 | Value: "", // leave the flag default value blank to support the clone default behavior if not explicity set of 'use what is most appropriate' 215 | }, 216 | mcnflag.StringFlag{ 217 | EnvVar: "PROXMOXVE_VM_CPU_SOCKETS", 218 | Name: "proxmoxve-vm-cpu-sockets", 219 | Usage: "number of cpus", 220 | Value: "", 221 | }, 222 | mcnflag.StringFlag{ 223 | EnvVar: "PROXMOXVE_VM_CPU_CORES", 224 | Name: "proxmoxve-vm-cpu-cores", 225 | Usage: "number of cpu cores", 226 | Value: "", 227 | }, 228 | mcnflag.StringFlag{ 229 | EnvVar: "PROXMOXVE_VM_CLONE_VNID", 230 | Name: "proxmoxve-vm-clone-vmid", 231 | Usage: "vmid to clone", 232 | Value: "", 233 | }, 234 | mcnflag.IntFlag{ 235 | EnvVar: "PROXMOXVE_VM_CLONE_FULL", 236 | Name: "proxmoxve-vm-clone-full", 237 | Usage: "make a full clone or not (0=false, 1=true, 2=use proxmox default logic", 238 | Value: 2, 239 | }, 240 | mcnflag.StringFlag{ 241 | EnvVar: "PROXMOXVE_VM_START_ONBOOT", 242 | Name: "proxmoxve-vm-start-onboot", 243 | Usage: "make the VM start automatically onboot (0=false, 1=true, ''=default)", 244 | Value: "", // leave the flag default value blank to support the clone default behavior if not explicity set of 'use what is most appropriate' 245 | }, 246 | mcnflag.StringFlag{ 247 | EnvVar: "PROXMOXVE_VM_PROTECTION", 248 | Name: "proxmoxve-vm-protection", 249 | Usage: "protect the VM and disks from removal (0=false, 1=true, ''=default)", 250 | Value: "", // leave the flag default value blank to support the clone default behavior if not explicity set of 'use what is most appropriate' 251 | }, 252 | mcnflag.StringFlag{ 253 | EnvVar: "PROXMOXVE_VM_CITYPE", 254 | Name: "proxmoxve-vm-citype", 255 | Usage: "cloud-init type (nocloud|configdrive2)", 256 | Value: "", // leave the flag default value blank to support the clone default behavior if not explicity set of 'use what is most appropriate' 257 | }, 258 | mcnflag.StringFlag{ 259 | EnvVar: "PROXMOXVE_VM_CIENABLED", 260 | Name: "proxmoxve-vm-cienabled", 261 | Usage: "cloud-init enabled (implied with clone strategy 0=false, 1=true, ''=default)", 262 | Value: "", // leave the flag default value blank to support the clone default behavior if not explicity set of 'use what is most appropriate' 263 | }, 264 | mcnflag.StringFlag{ 265 | EnvVar: "PROXMOXVE_VM_IMAGE_FILE", 266 | Name: "proxmoxve-vm-image-file", 267 | Usage: "storage of the image file (e.g. local:iso/rancheros-proxmoxve-autoformat.iso)", 268 | Value: "", 269 | }, 270 | mcnflag.StringFlag{ 271 | EnvVar: "PROXMOXVE_VM_NET_MODEL", 272 | Name: "proxmoxve-vm-net-model", 273 | Usage: "Net Interface model, default virtio (e1000, virtio, realtek, etc...)", 274 | Value: "virtio", 275 | }, 276 | mcnflag.StringFlag{ 277 | EnvVar: "PROXMOXVE_VM_NET_FIREWALL", 278 | Name: "proxmoxve-vm-net-firewall", 279 | Usage: "enable/disable firewall (0=false, 1=true, ''=default)", 280 | Value: "", 281 | }, 282 | mcnflag.StringFlag{ 283 | EnvVar: "PROXMOXVE_VM_NET_MTU", 284 | Name: "proxmoxve-vm-net-mtu", 285 | Usage: "set nic mtu (''=default)", 286 | Value: "", 287 | }, 288 | mcnflag.StringFlag{ 289 | EnvVar: "PROXMOXVE_VM_NET_BRIDGE", 290 | Name: "proxmoxve-vm-net-bridge", 291 | Usage: "bridge to attach network to", 292 | Value: "", // leave the flag default value blank to support the clone default behavior if not explicity set of 'use what is most appropriate' 293 | }, 294 | mcnflag.IntFlag{ 295 | EnvVar: "PROXMOXVE_VM_NET_TAG", 296 | Name: "proxmoxve-vm-net-tag", 297 | Usage: "vlan tag", 298 | Value: 0, 299 | }, 300 | mcnflag.StringFlag{ 301 | EnvVar: "PROXMOXVE_SSH_USERNAME", 302 | Name: "proxmoxve-ssh-username", 303 | Usage: "Username to log in to the guest OS (default docker for rancheros)", 304 | Value: "", 305 | }, 306 | mcnflag.StringFlag{ 307 | EnvVar: "PROXMOXVE_SSH_PASSWORD", 308 | Name: "proxmoxve-ssh-password", 309 | Usage: "Password to log in to the guest OS (default tcuser for rancheros)", 310 | Value: "", 311 | }, 312 | mcnflag.IntFlag{ 313 | EnvVar: "PROXMOXVE_SSH_PORT", 314 | Name: "proxmoxve-ssh-port", 315 | Usage: "SSH port in the guest to log in to (defaults to 22)", 316 | Value: 22, 317 | }, 318 | mcnflag.BoolFlag{ 319 | EnvVar: "PROXMOXVE_DEBUG_RESTY", 320 | Name: "proxmoxve-debug-resty", 321 | Usage: "enables the resty debugging", 322 | }, 323 | mcnflag.BoolFlag{ 324 | EnvVar: "PROXMOXVE_DEBUG_DRIVER", 325 | Name: "proxmoxve-debug-driver", 326 | Usage: "enables debugging in the driver", 327 | }, 328 | } 329 | } 330 | 331 | func (d *Driver) ping() bool { 332 | if d.driver == nil { 333 | return false 334 | } 335 | 336 | command := NodesNodeQemuVMIDAgentPostParameter{Command: "ping"} 337 | err := d.driver.NodesNodeQemuVMIDAgentPost(d.Node, d.VMID, &command) 338 | 339 | if err != nil { 340 | d.debug(err) 341 | return false 342 | } 343 | 344 | return true 345 | } 346 | 347 | // DriverName returns the name of the driver 348 | func (d *Driver) DriverName() string { 349 | return "proxmoxve" 350 | } 351 | 352 | // SetConfigFromFlags configures all command line arguments 353 | func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { 354 | d.debug("SetConfigFromFlags called") 355 | 356 | d.ProvisionStrategy = flags.String("proxmoxve-provision-strategy") 357 | 358 | // PROXMOX API Connection settings 359 | d.Host = flags.String("proxmoxve-proxmox-host") 360 | d.Node = flags.String("proxmoxve-proxmox-node") 361 | if len(d.Node) == 0 { 362 | d.Node = d.Host 363 | } 364 | d.User = flags.String("proxmoxve-proxmox-user-name") 365 | d.Password = flags.String("proxmoxve-proxmox-user-password") 366 | d.Realm = flags.String("proxmoxve-proxmox-realm") 367 | d.Pool = flags.String("proxmoxve-proxmox-pool") 368 | 369 | // VM configuration 370 | d.DiskSize = flags.String("proxmoxve-vm-storage-size") 371 | d.Storage = flags.String("proxmoxve-vm-storage-path") 372 | d.StorageType = strings.ToLower(flags.String("proxmoxve-vm-storage-type")) 373 | d.Memory = flags.Int("proxmoxve-vm-memory") 374 | d.Memory *= 1024 375 | d.VMIDRange = flags.String("proxmoxve-vm-vmid-range") 376 | d.CloneVMID = flags.String("proxmoxve-vm-clone-vmid") 377 | d.CloneFull = flags.Int("proxmoxve-vm-clone-full") 378 | d.Onboot = flags.String("proxmoxve-vm-start-onboot") 379 | d.Protection = flags.String("proxmoxve-vm-protection") 380 | d.Citype = flags.String("proxmoxve-vm-citype") 381 | d.CiEnabled = flags.String("proxmoxve-vm-cienabled") 382 | d.ImageFile = flags.String("proxmoxve-vm-image-file") 383 | d.CPUSockets = flags.String("proxmoxve-vm-cpu-sockets") 384 | d.CPU = flags.String("proxmoxve-vm-cpu") 385 | d.CPUCores = flags.String("proxmoxve-vm-cpu-cores") 386 | d.NetModel = flags.String("proxmoxve-vm-net-model") 387 | d.NetFirewall = flags.String("proxmoxve-vm-net-firewall") 388 | d.NetMtu = flags.String("proxmoxve-vm-net-mtu") 389 | d.NetBridge = flags.String("proxmoxve-vm-net-bridge") 390 | d.NetVlanTag = flags.Int("proxmoxve-vm-net-tag") 391 | d.ScsiController = flags.String("proxmoxve-vm-scsi-controller") 392 | d.ScsiAttributes = flags.String("proxmoxve-vm-scsi-attributes") 393 | 394 | //SSH connection settings 395 | d.GuestSSHPort = flags.Int("proxmoxve-ssh-port") 396 | d.GuestUsername = flags.String("proxmoxve-ssh-username") 397 | d.GuestPassword = flags.String("proxmoxve-ssh-password") 398 | 399 | //SWARM Settings 400 | d.SwarmMaster = flags.Bool("swarm-master") 401 | d.SwarmHost = flags.String("swarm-host") 402 | 403 | //Debug option 404 | d.driverDebug = flags.Bool("proxmoxve-debug-driver") 405 | d.restyDebug = flags.Bool("proxmoxve-debug-resty") 406 | 407 | if d.restyDebug { 408 | d.debug("enabling Resty debugging") 409 | resty.SetLogger(log.Output()) 410 | resty.SetDebug(true) 411 | } 412 | 413 | return nil 414 | } 415 | 416 | // GetURL returns the URL for the target docker daemon 417 | func (d *Driver) GetURL() (string, error) { 418 | ip, err := d.GetIP() 419 | if err != nil { 420 | return "", err 421 | } 422 | if ip == "" { 423 | return "", nil 424 | } 425 | return fmt.Sprintf("tcp://%s:2376", ip), nil 426 | } 427 | 428 | // GetMachineName returns the machine name 429 | func (d *Driver) GetMachineName() string { 430 | return d.MachineName 431 | } 432 | 433 | // GetNetBridge returns the bridge 434 | func (d *Driver) GetNetBridge() string { 435 | return d.NetBridge 436 | } 437 | 438 | // GetNetVlanTag returns the vlan tag 439 | func (d *Driver) GetNetVlanTag() int { 440 | return d.NetVlanTag 441 | } 442 | 443 | // GetIP returns the ip 444 | func (d *Driver) GetIP() (string, error) { 445 | d.connectAPI() 446 | 447 | ip, err := d.driver.GetEth0IPv4(d.Node, d.VMID) 448 | if err != nil { 449 | // TODO: should we return the cached IP here? 450 | return ip, err 451 | } 452 | 453 | // set/update IP if success 454 | if d.IPAddress != ip { 455 | d.debugf("driver IP is set as '%s'", ip) 456 | d.IPAddress = ip 457 | } 458 | 459 | return ip, err 460 | } 461 | 462 | // GetSSHHostname returns the ssh host returned by the API 463 | func (d *Driver) GetSSHHostname() (string, error) { 464 | return d.GetIP() 465 | } 466 | 467 | // GetSSHPort returns the ssh port, 22 if not specified 468 | func (d *Driver) GetSSHPort() (int, error) { 469 | return d.GuestSSHPort, nil 470 | } 471 | 472 | // GetSSHUsername returns the ssh user name, root if not specified 473 | func (d *Driver) GetSSHUsername() string { 474 | return d.GuestUsername 475 | } 476 | 477 | // GetState returns the state of the VM 478 | func (d *Driver) GetState() (state.State, error) { 479 | if len(d.VMID) < 1 { 480 | return state.Error, errors.New("invalid VMID") 481 | } 482 | 483 | err := d.connectAPI() 484 | if err != nil { 485 | return state.Error, err 486 | } 487 | 488 | // sanity check the UUID 489 | config, err := d.driver.GetConfig(d.Node, d.VMID) 490 | if err != nil { 491 | return state.Error, err 492 | } 493 | 494 | cVMMUUID := getUUIDFromSmbios1(config.Data.Smbios1) 495 | if len(d.VMUUID) > 1 && d.VMUUID != cVMMUUID { 496 | return state.Error, fmt.Errorf("UUID mismatch - %s (stored) vs %s (current)", d.VMUUID, cVMMUUID) 497 | } 498 | 499 | //Starting 500 | //Running 501 | //Stopped 502 | //Paused 503 | //Error 504 | 505 | ip, err := d.GetIP() 506 | 507 | // only consider fully running when IP is available 508 | if err == nil && len(ip) > 0 { 509 | return state.Running, nil 510 | } 511 | 512 | // starting if qemu-guest-agent is available but no IP 513 | if d.ping() { 514 | return state.Starting, nil 515 | } 516 | 517 | pveState, err := d.driver.NodesNodeQemuVMIDStatusCurrentGet(d.Node, d.VMID) 518 | if err != nil { 519 | return state.Error, err 520 | } 521 | 522 | switch pveState { 523 | case "stopped": 524 | return state.Stopped, nil 525 | case "running": 526 | return state.Starting, nil 527 | } 528 | 529 | return state.Error, fmt.Errorf("unkown error detecting VM state") 530 | } 531 | 532 | // PreCreateCheck is called to enforce pre-creation steps 533 | func (d *Driver) PreCreateCheck() error { 534 | 535 | err := d.connectAPI() 536 | if err != nil { 537 | return err 538 | } 539 | 540 | switch d.ProvisionStrategy { 541 | case "cdrom": 542 | // set defaults for cdrom 543 | // replicating pre-clone behavior of setting a default on the parameter 544 | if len(d.Storage) < 1 { 545 | d.Storage = "local" 546 | } 547 | 548 | if len(d.StorageType) < 1 { 549 | d.StorageType = "raw" 550 | } 551 | 552 | if len(d.NetBridge) < 1 { 553 | d.NetBridge = "vmbr0" 554 | } 555 | 556 | if len(d.GuestUsername) < 1 { 557 | d.GuestUsername = "docker" 558 | } 559 | 560 | if len(d.GuestPassword) < 1 { 561 | d.GuestPassword = "tcuser" 562 | } 563 | 564 | // prepare StorageFilename 565 | switch d.StorageType { 566 | case "raw": 567 | fallthrough 568 | case "qcow2": 569 | break 570 | default: 571 | return fmt.Errorf("storage type '%s' is not supported", d.StorageType) 572 | } 573 | 574 | storageType, err := d.driver.GetStorageType(d.Node, d.Storage) 575 | if err != nil { 576 | return err 577 | } 578 | 579 | filename := "-disk-0" 580 | switch storageType { 581 | case "lvmthin": 582 | fallthrough 583 | case "zfs": 584 | fallthrough 585 | case "ceph": 586 | if d.StorageType != "raw" { 587 | return fmt.Errorf("type '%s' on storage '%s' does only support raw", storageType, d.Storage) 588 | } 589 | case "nfs": 590 | fallthrough 591 | case "dir": 592 | filename += "." + d.StorageType 593 | } 594 | // this is not the finale filename, it'll be constructed in Create() 595 | d.StorageFilename = filename 596 | case "clone": 597 | break 598 | default: 599 | return fmt.Errorf("invalid provision strategy '%s'", d.ProvisionStrategy) 600 | } 601 | 602 | return nil 603 | } 604 | 605 | // Create creates a new VM with storage 606 | func (d *Driver) Create() error { 607 | 608 | // create and save a new SSH key pair 609 | d.debug("creating new ssh keypair") 610 | key, err := d.createSSHKey() 611 | if err != nil { 612 | return err 613 | } 614 | 615 | // !! Workaround for MC-7982. 616 | key = strings.TrimSpace(key) 617 | key = fmt.Sprintf("%s %s-%d", key, d.MachineName, time.Now().Unix()) 618 | // !! End workaround for MC-7982. 619 | 620 | // add some random wait time here to help with race conditions in the proxmox api 621 | // the app could be getting invoked several times in rapid succession so some small waits may be helpful 622 | mrand.Seed(time.Now().UnixNano()) // Seed the random number generator using the current time (nanoseconds since epoch) 623 | r := mrand.Intn(5000) 624 | d.debugf("sleeping %d milliseconds before retrieving next ID", r) 625 | time.Sleep(time.Duration(r) * time.Millisecond) 626 | 627 | // get next available VMID 628 | // NOTE: we want to lock in the ID as quickly as possible after retrieving (ie: invoke QemuPost or Clone ASAP to avoid race conditions with other instances) 629 | d.debug("Retrieving next ID") 630 | 631 | var id string 632 | if len(d.VMIDRange) > 0 { 633 | VMIDParts := strings.Split(d.VMIDRange, ":") 634 | low, err := strconv.Atoi(VMIDParts[0]) 635 | if err != nil { 636 | return err 637 | } 638 | 639 | var high = 0 640 | if len(VMIDParts) > 1 { 641 | high, err = strconv.Atoi(VMIDParts[1]) 642 | if err != nil { 643 | return err 644 | } 645 | } 646 | 647 | d.debugf("Looking for available VMID in range '%d:%d'", low, high) 648 | 649 | var i = low 650 | for len(id) < 1 { 651 | id, err = d.driver.ClusterNextIDGet(i) 652 | if err != nil && high > 0 && i > high { 653 | return err 654 | } 655 | 656 | if high > 0 && i >= high { 657 | return fmt.Errorf("no VMIDs available in range '%d:%d'", low, high) 658 | } 659 | 660 | i++ 661 | } 662 | } else { 663 | id, err = d.driver.ClusterNextIDGet(0) 664 | if err != nil { 665 | return err 666 | } 667 | } 668 | 669 | d.debugf("Next ID is '%s'", id) 670 | d.VMID = id 671 | 672 | switch d.ProvisionStrategy { 673 | case "cdrom": 674 | 675 | // prefixing StorageFilename with VMID 676 | d.StorageFilename = "vm-" + d.VMID + d.StorageFilename 677 | 678 | volume := NodesNodeStorageStorageContentPostParameter{ 679 | Filename: d.StorageFilename, 680 | Size: d.DiskSize + "G", 681 | VMID: d.VMID, 682 | } 683 | 684 | d.debugf("Creating disk volume '%s' with size '%s'", volume.Filename, volume.Size) 685 | diskname, err := d.driver.NodesNodeStorageStorageContentPost(d.Node, d.Storage, &volume) 686 | if err != nil { 687 | return err 688 | } 689 | 690 | if !strings.HasSuffix(diskname, d.StorageFilename) { 691 | return fmt.Errorf("returned diskname is not correct: should be '%s' but was '%s'", d.StorageFilename, diskname) 692 | } 693 | 694 | npp := NodesNodeQemuPostParameter{ 695 | VMID: d.VMID, 696 | Agent: "1", 697 | Autostart: "1", 698 | Onboot: d.Onboot, 699 | Memory: d.Memory, 700 | Sockets: d.CPUSockets, 701 | Cores: d.CPUCores, 702 | SCSI0: d.StorageFilename, 703 | Ostype: "l26", 704 | Name: d.BaseDriver.MachineName, 705 | KVM: "1", // if you test in a nested environment, you may have to change this to 0 if you do not have nested virtualization 706 | Scsihw: d.ScsiController, 707 | Cdrom: d.ImageFile, 708 | Pool: d.Pool, 709 | Protection: d.Protection, 710 | } 711 | 712 | if d.CiEnabled == "1" { 713 | npp.Citype = d.Citype 714 | npp.Ide3 = d.Storage + ":cloudinit" 715 | } 716 | 717 | if len(d.ScsiAttributes) > 0 { 718 | npp.SCSI0 += "," + d.ScsiAttributes 719 | } 720 | 721 | if len(d.NUMA) > 0 { 722 | npp.NUMA = d.NUMA 723 | } 724 | 725 | if len(d.CPU) > 0 { 726 | npp.CPU = d.CPU 727 | } 728 | 729 | npp.Net0, _ = d.generateNetString() 730 | 731 | if d.StorageType == "qcow2" { 732 | npp.SCSI0 = d.Storage + ":" + d.VMID + "/" + volume.Filename 733 | } else if d.StorageType == "raw" { 734 | if strings.HasSuffix(volume.Filename, ".raw") { 735 | // raw files (having .raw) should have the VMID in the path 736 | npp.SCSI0 = d.Storage + ":" + d.VMID + "/" + volume.Filename 737 | } else { 738 | npp.SCSI0 = d.Storage + ":" + volume.Filename 739 | } 740 | } 741 | d.debugf("Creating VM '%s' with '%d' of memory", npp.VMID, npp.Memory) 742 | taskid, err := d.driver.NodesNodeQemuPost(d.Node, &npp) 743 | if err != nil { 744 | return err 745 | } 746 | 747 | err = d.driver.WaitForTaskToComplete(d.Node, taskid) 748 | if err != nil { 749 | return err 750 | } 751 | 752 | if d.CiEnabled == "1" { 753 | // specially handle setting sshkeys 754 | // https://forum.proxmox.com/threads/how-to-use-pvesh-set-vms-sshkeys.52570/ 755 | taskid, err = d.driver.NodesNodeQemuVMIDConfigSetSSHKeys(d.Node, d.VMID, key) 756 | if err != nil { 757 | return err 758 | } 759 | 760 | err = d.driver.WaitForTaskToComplete(d.Node, taskid) 761 | if err != nil { 762 | return err 763 | } 764 | } 765 | 766 | break 767 | case "clone": 768 | 769 | // clone 770 | clone := NodesNodeQemuVMIDClonePostParameter{ 771 | Newid: d.VMID, 772 | Name: d.BaseDriver.MachineName, 773 | Pool: d.Pool, 774 | } 775 | 776 | switch d.CloneFull { 777 | case 0: 778 | clone.Full = "0" 779 | break 780 | case 1: 781 | clone.Full = "1" 782 | clone.Format = d.StorageType 783 | clone.Storage = d.Storage 784 | break 785 | case 2: 786 | clone.Format = d.StorageType 787 | clone.Storage = d.Storage 788 | break 789 | } 790 | 791 | d.debugf("cloning template id '%s' as vmid '%s'", d.CloneVMID, clone.Newid) 792 | 793 | taskid, err := d.driver.NodesNodeQemuVMIDClonePost(d.Node, d.CloneVMID, &clone) 794 | if err != nil { 795 | return err 796 | } 797 | 798 | err = d.driver.WaitForTaskToComplete(d.Node, taskid) 799 | if err != nil { 800 | return err 801 | } 802 | 803 | // resize 804 | resize := NodesNodeQemuVMIDResizePutParameter{ 805 | Disk: "scsi0", 806 | Size: d.DiskSize + "G", 807 | } 808 | d.debugf("resizing disk '%s' on vmid '%s' to '%s'", resize.Disk, d.VMID, resize.Size) 809 | 810 | err = d.driver.NodesNodeQemuVMIDResizePut(d.Node, d.VMID, &resize) 811 | if err != nil { 812 | return err 813 | } 814 | 815 | // set config values 816 | d.debugf("setting VM config values for vmid '%s'", d.VMID) 817 | npp := NodesNodeQemuPostParameter{ 818 | Agent: "1", 819 | Autostart: "1", 820 | Memory: d.Memory, 821 | Sockets: d.CPUSockets, 822 | Cores: d.CPUCores, 823 | KVM: "1", // if you test in a nested environment, you may have to change this to 0 if you do not have nested virtualization, 824 | Citype: d.Citype, 825 | Onboot: d.Onboot, 826 | Protection: d.Protection, 827 | } 828 | 829 | if len(d.NetBridge) > 0 { 830 | npp.Net0, _ = d.generateNetString() 831 | } 832 | 833 | if len(d.NUMA) > 0 { 834 | npp.NUMA = d.NUMA 835 | } 836 | 837 | if len(d.CPU) > 0 { 838 | npp.CPU = d.CPU 839 | } 840 | 841 | taskid, err = d.driver.NodesNodeQemuVMIDConfigPost(d.Node, d.VMID, &npp) 842 | if err != nil { 843 | return err 844 | } 845 | 846 | // append newly minted ssh key to existing (if any) 847 | d.debugf("retrieving existing cloud-init sshkeys from vmid '%s'", d.VMID) 848 | config, err := d.driver.GetConfig(d.Node, d.CloneVMID) 849 | if err != nil { 850 | return err 851 | } 852 | 853 | var SSHKeys string 854 | 855 | if len(config.Data.SSHKeys) > 0 { 856 | SSHKeys, err = url.QueryUnescape(config.Data.SSHKeys) 857 | if err != nil { 858 | return err 859 | } 860 | 861 | SSHKeys = strings.TrimSpace(SSHKeys) 862 | SSHKeys += "\n" 863 | } 864 | 865 | SSHKeys += key 866 | SSHKeys = strings.TrimSpace(SSHKeys) 867 | 868 | // specially handle setting sshkeys 869 | // https://forum.proxmox.com/threads/how-to-use-pvesh-set-vms-sshkeys.52570/ 870 | taskid, err = d.driver.NodesNodeQemuVMIDConfigSetSSHKeys(d.Node, d.VMID, SSHKeys) 871 | if err != nil { 872 | return err 873 | } 874 | 875 | err = d.driver.WaitForTaskToComplete(d.Node, taskid) 876 | if err != nil { 877 | return err 878 | } 879 | break 880 | default: 881 | return fmt.Errorf("invalid provision strategy '%s'", d.ProvisionStrategy) 882 | } 883 | 884 | // Set the newly minted UUID 885 | config, err := d.driver.GetConfig(d.Node, d.VMID) 886 | if err != nil { 887 | return err 888 | } 889 | d.VMUUID = getUUIDFromSmbios1(config.Data.Smbios1) 890 | d.debugf("VM created with uuid '%s'", d.VMUUID) 891 | 892 | // start the VM 893 | err = d.Start() 894 | if err != nil { 895 | return err 896 | } 897 | 898 | // let VM start a settle a little 899 | d.debugf("waiting for VM to start, wait 10 seconds") 900 | time.Sleep(10 * time.Second) 901 | 902 | // wait for qemu-guest-agent 903 | err = d.waitForQemuGuestAgent() 904 | if err != nil { 905 | return err 906 | } 907 | 908 | // wait for network to come up 909 | err = d.waitForNetwork() 910 | 911 | // set the IPAddress 912 | _, err = d.GetIP() 913 | if err != nil { 914 | return err 915 | 916 | } 917 | 918 | switch d.ProvisionStrategy { 919 | case "cdrom": 920 | if len(d.GuestPassword) > 0 { 921 | return d.prepareSSHWithPassword() 922 | } 923 | return nil 924 | case "clone": 925 | fallthrough 926 | default: 927 | return nil 928 | } 929 | } 930 | 931 | func (d *Driver) waitForQemuGuestAgent() error { 932 | d.debugf("waiting for VM qemu-guest-agent to start") 933 | d.connectAPI() 934 | for !d.ping() { 935 | d.debugf("waiting for VM qemu-guest-agent to start") 936 | time.Sleep(5 * time.Second) 937 | } 938 | 939 | return nil 940 | } 941 | 942 | func (d *Driver) waitForNetwork() error { 943 | d.debugf("waiting for VM network to start") 944 | d.connectAPI() 945 | 946 | var up = false 947 | var ip string 948 | var err error 949 | 950 | for !up { 951 | ip, err = d.driver.GetEth0IPv4(d.Node, d.VMID) 952 | if err != nil { 953 | d.debugf("waiting for VM network to start") 954 | time.Sleep(5 * time.Second) 955 | } else { 956 | if len(ip) > 0 { 957 | up = true 958 | d.debugf("VM network started with ip: %s", ip) 959 | } else { 960 | d.debugf("waiting for VM network to start") 961 | time.Sleep(5 * time.Second) 962 | } 963 | } 964 | } 965 | 966 | return nil 967 | } 968 | 969 | func (d *Driver) generateNetString() (string, error) { 970 | var net string = fmt.Sprintf("model=%s,bridge=%s", d.NetModel, d.NetBridge) 971 | if d.NetVlanTag != 0 { 972 | net = fmt.Sprintf(net+",tag=%d", d.NetVlanTag) 973 | } 974 | 975 | if len(d.NetFirewall) > 0 { 976 | net = fmt.Sprintf(net+",firewall=%s", d.NetFirewall) 977 | } 978 | 979 | if len(d.NetMtu) > 0 { 980 | net = fmt.Sprintf(net+",mtu=%s", d.NetMtu) 981 | } 982 | 983 | return net, nil 984 | } 985 | 986 | func (d *Driver) prepareSSHWithPassword() error { 987 | sshConfig := &ssh.ClientConfig{ 988 | User: d.GetSSHUsername(), 989 | Auth: []ssh.AuthMethod{ 990 | ssh.Password(d.GuestPassword)}, 991 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 992 | } 993 | 994 | sshbasedir := "/home/" + d.GetSSHUsername() + "/.ssh" 995 | hostname, _ := d.GetSSHHostname() 996 | port, _ := d.GetSSHPort() 997 | clientstr := fmt.Sprintf("%s:%d", hostname, port) 998 | 999 | d.debugf("Creating directory '%s'", sshbasedir) 1000 | conn, err := ssh.Dial("tcp", clientstr, sshConfig) 1001 | if err != nil { 1002 | return err 1003 | } 1004 | session, err := conn.NewSession() 1005 | if err != nil { 1006 | return err 1007 | } 1008 | 1009 | var stdoutBuf bytes.Buffer 1010 | session.Stdout = &stdoutBuf 1011 | session.Run("mkdir -p " + sshbasedir) 1012 | d.debugf(fmt.Sprintf("%s -> %s", hostname, stdoutBuf.String())) 1013 | session.Close() 1014 | 1015 | d.debugf("Trying to copy to %s:%s", clientstr, sshbasedir) 1016 | c, err := sshrw.NewSSHclt(clientstr, sshConfig) 1017 | if err != nil { 1018 | return err 1019 | } 1020 | 1021 | // Open a file 1022 | f, err := os.Open(d.GetSSHKeyPath() + ".pub") 1023 | if err != nil { 1024 | return err 1025 | } 1026 | 1027 | // TODO: always fails with return status 127, but file was copied correclty 1028 | c.WriteFile(f, sshbasedir+"/authorized_keys") 1029 | // if err = c.WriteFile(f, sshbasedir+"/authorized_keys"); err != nil { 1030 | // d.debugf("Error on file write: ", err) 1031 | // } 1032 | 1033 | // Close the file after it has been copied 1034 | defer f.Close() 1035 | 1036 | return err 1037 | } 1038 | 1039 | // Start starts the VM 1040 | func (d *Driver) Start() error { 1041 | if len(d.VMID) < 1 { 1042 | return errors.New("invalid VMID") 1043 | } 1044 | 1045 | err := d.connectAPI() 1046 | if err != nil { 1047 | return err 1048 | } 1049 | 1050 | // sanity check the UUID 1051 | config, err := d.driver.GetConfig(d.Node, d.VMID) 1052 | if err != nil { 1053 | return err 1054 | } 1055 | 1056 | cVMMUUID := getUUIDFromSmbios1(config.Data.Smbios1) 1057 | if len(d.VMUUID) > 1 && d.VMUUID != cVMMUUID { 1058 | return fmt.Errorf("UUID mismatch - %s (stored) vs %s (current)", d.VMUUID, cVMMUUID) 1059 | } 1060 | 1061 | taskid, err := d.driver.NodesNodeQemuVMIDStatusStartPost(d.Node, d.VMID) 1062 | if err != nil { 1063 | return err 1064 | } 1065 | 1066 | err = d.driver.WaitForTaskToComplete(d.Node, taskid) 1067 | 1068 | return err 1069 | } 1070 | 1071 | // Stop stopps the VM 1072 | func (d *Driver) Stop() error { 1073 | if len(d.VMID) < 1 { 1074 | return errors.New("invalid VMID") 1075 | } 1076 | 1077 | err := d.connectAPI() 1078 | if err != nil { 1079 | return err 1080 | } 1081 | 1082 | // sanity check the UUID 1083 | config, err := d.driver.GetConfig(d.Node, d.VMID) 1084 | if err != nil { 1085 | return err 1086 | } 1087 | 1088 | cVMMUUID := getUUIDFromSmbios1(config.Data.Smbios1) 1089 | if len(d.VMUUID) > 1 && d.VMUUID != cVMMUUID { 1090 | return fmt.Errorf("UUID mismatch - %s (stored) vs %s (current)", d.VMUUID, cVMMUUID) 1091 | } 1092 | 1093 | // shutdown 1094 | taskid, err := d.driver.NodesNodeQemuVMIDStatusShutdownPost(d.Node, d.VMID) 1095 | if err != nil { 1096 | return err 1097 | } 1098 | 1099 | err = d.driver.WaitForTaskToComplete(d.Node, taskid) 1100 | 1101 | return err 1102 | } 1103 | 1104 | // Restart restarts the VM 1105 | func (d *Driver) Restart() error { 1106 | if len(d.VMID) < 1 { 1107 | return errors.New("invalid VMID") 1108 | } 1109 | 1110 | err := d.connectAPI() 1111 | if err != nil { 1112 | return err 1113 | } 1114 | 1115 | // sanity check the UUID 1116 | config, err := d.driver.GetConfig(d.Node, d.VMID) 1117 | if err != nil { 1118 | return err 1119 | } 1120 | 1121 | cVMMUUID := getUUIDFromSmbios1(config.Data.Smbios1) 1122 | if len(d.VMUUID) > 1 && d.VMUUID != cVMMUUID { 1123 | return fmt.Errorf("UUID mismatch - %s (stored) vs %s (current)", d.VMUUID, cVMMUUID) 1124 | } 1125 | 1126 | // reboot 1127 | taskid, err := d.driver.NodesNodeQemuVMIDStatusRebootPost(d.Node, d.VMID) 1128 | if err != nil { 1129 | return err 1130 | } 1131 | 1132 | err = d.driver.WaitForTaskToComplete(d.Node, taskid) 1133 | 1134 | return err 1135 | } 1136 | 1137 | // Kill the VM immediately 1138 | func (d *Driver) Kill() error { 1139 | if len(d.VMID) < 1 { 1140 | return errors.New("invalid VMID") 1141 | } 1142 | 1143 | err := d.connectAPI() 1144 | if err != nil { 1145 | return err 1146 | } 1147 | 1148 | // sanity check the UUID 1149 | config, err := d.driver.GetConfig(d.Node, d.VMID) 1150 | if err != nil { 1151 | return err 1152 | } 1153 | 1154 | cVMMUUID := getUUIDFromSmbios1(config.Data.Smbios1) 1155 | if len(d.VMUUID) > 1 && d.VMUUID != cVMMUUID { 1156 | return fmt.Errorf("UUID mismatch - %s (stored) vs %s (current)", d.VMUUID, cVMMUUID) 1157 | } 1158 | 1159 | // stop 1160 | taskid, err := d.driver.NodesNodeQemuVMIDStatusStopPost(d.Node, d.VMID) 1161 | if err != nil { 1162 | return err 1163 | } 1164 | 1165 | err = d.driver.WaitForTaskToComplete(d.Node, taskid) 1166 | 1167 | return err 1168 | } 1169 | 1170 | // Remove removes the VM 1171 | func (d *Driver) Remove() error { 1172 | if len(d.VMID) < 1 { 1173 | return nil 1174 | } 1175 | 1176 | err := d.connectAPI() 1177 | if err != nil { 1178 | return err 1179 | } 1180 | 1181 | // sanity check the UUID 1182 | config, err := d.driver.GetConfig(d.Node, d.VMID) 1183 | if err != nil { 1184 | return err 1185 | } 1186 | 1187 | cVMMUUID := getUUIDFromSmbios1(config.Data.Smbios1) 1188 | if len(d.VMUUID) > 1 && d.VMUUID != cVMMUUID { 1189 | return nil 1190 | } 1191 | 1192 | // force shut down VM before invoking delete 1193 | err = d.Kill() 1194 | if err != nil { 1195 | return err 1196 | } 1197 | 1198 | taskid, err := d.driver.NodesNodeQemuVMIDDelete(d.Node, d.VMID) 1199 | if err != nil { 1200 | return err 1201 | } 1202 | 1203 | err = d.driver.WaitForTaskToComplete(d.Node, taskid) 1204 | return err 1205 | } 1206 | 1207 | // Upgrade is currently a NOOP 1208 | func (d *Driver) Upgrade() error { 1209 | return nil 1210 | } 1211 | 1212 | // NewDriver returns a new driver 1213 | func NewDriver(hostName, storePath string) drivers.Driver { 1214 | return &Driver{ 1215 | BaseDriver: &drivers.BaseDriver{ 1216 | SSHUser: "docker", 1217 | MachineName: hostName, 1218 | StorePath: storePath, 1219 | }, 1220 | } 1221 | } 1222 | 1223 | func (d *Driver) createSSHKey() (string, error) { 1224 | sshKeyPath := d.ResolveStorePath("id_rsa") 1225 | if err := mssh.GenerateSSHKey(sshKeyPath); err != nil { 1226 | return "", err 1227 | } 1228 | key, err := ioutil.ReadFile(sshKeyPath + ".pub") 1229 | if err != nil { 1230 | return "", err 1231 | } 1232 | return string(key), nil 1233 | } 1234 | 1235 | // GetKeyPair returns a public/private key pair and an optional error 1236 | func GetKeyPair(file string) (string, string, error) { 1237 | // read keys from file 1238 | _, err := os.Stat(file) 1239 | if err == nil { 1240 | priv, err := ioutil.ReadFile(file) 1241 | if err != nil { 1242 | fmt.Printf("Failed to read file - %s", err) 1243 | goto genKeys 1244 | } 1245 | pub, err := ioutil.ReadFile(file + ".pub") 1246 | if err != nil { 1247 | fmt.Printf("Failed to read pub file - %s", err) 1248 | goto genKeys 1249 | } 1250 | return string(pub), string(priv), nil 1251 | } 1252 | 1253 | // generate keys and save to file 1254 | genKeys: 1255 | pub, priv, err := GenKeyPair() 1256 | err = ioutil.WriteFile(file, []byte(priv), 0600) 1257 | if err != nil { 1258 | return "", "", fmt.Errorf("Failed to write file - %s", err) 1259 | } 1260 | err = ioutil.WriteFile(file+".pub", []byte(pub), 0644) 1261 | if err != nil { 1262 | return "", "", fmt.Errorf("Failed to write pub file - %s", err) 1263 | } 1264 | 1265 | return pub, priv, nil 1266 | } 1267 | 1268 | // GenKeyPair returns a freshly created public/private key pair and an optional error 1269 | func GenKeyPair() (string, string, error) { 1270 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 1271 | if err != nil { 1272 | return "", "", err 1273 | } 1274 | 1275 | privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} 1276 | var private bytes.Buffer 1277 | if err := pem.Encode(&private, privateKeyPEM); err != nil { 1278 | return "", "", err 1279 | } 1280 | 1281 | // generate public key 1282 | pub, err := ssh.NewPublicKey(&privateKey.PublicKey) 1283 | if err != nil { 1284 | return "", "", err 1285 | } 1286 | 1287 | public := ssh.MarshalAuthorizedKey(pub) 1288 | return string(public), private.String(), nil 1289 | } 1290 | 1291 | func getUUIDFromSmbios1(str string) string { 1292 | var re = regexp.MustCompile(`(?m)uuid=([\d\w-]{1,})[,]{0,1}.*$`) 1293 | return re.FindStringSubmatch(fmt.Sprintf("%s", str))[1] 1294 | } 1295 | -------------------------------------------------------------------------------- /testconfig.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "host":"10.255.0.5", 3 | "user":"root", 4 | "realm": "pam", 5 | "password":"P@ssw0rd", 6 | "node": "pve" 7 | } 8 | -------------------------------------------------------------------------------- /testconfig_test.go: -------------------------------------------------------------------------------- 1 | package dockermachinedriverproxmoxve_test 2 | 3 | // Test Proxmox VE instance configuration 4 | // as a singleton according to http://marcio.io/2015/07/singleton-pattern-in-go/ 5 | 6 | import ( 7 | "encoding/json" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "sync" 12 | "testing" 13 | 14 | dockermachinedriverproxmoxve "github.com/lnxbil/docker-machine-driver-proxmox-ve" 15 | ) 16 | 17 | // ProxmoxConfig represents all needed login information 18 | type ProxmoxConfig struct { 19 | Host string `json:"host"` 20 | User string `json:"user"` 21 | Password string `json:"password"` 22 | Realm string `json:"realm"` 23 | Node string `json:"node"` 24 | } 25 | 26 | var instance *ProxmoxConfig 27 | var once sync.Once 28 | 29 | // GetProxmoxConfigInstance loads default config 30 | func GetProxmoxConfigInstance() *ProxmoxConfig { 31 | once.Do(func() { 32 | content, err := ioutil.ReadFile("testconfig.json") 33 | 34 | if err != nil { 35 | log.Fatal(err) 36 | os.Exit(1) 37 | } 38 | var config ProxmoxConfig 39 | json.Unmarshal(content, &config) 40 | instance = &config 41 | }) 42 | return instance 43 | } 44 | 45 | // GetProxmoxAccess gets working proxmox ve access information 46 | func GetProxmoxAccess() (string, string, string, string) { 47 | i := GetProxmoxConfigInstance() 48 | return i.User, i.Password, i.Realm, i.Host 49 | } 50 | 51 | // GetProxmoxNode return the defined test node 52 | func GetProxmoxNode() string { 53 | i := GetProxmoxConfigInstance() 54 | return i.Node 55 | } 56 | 57 | // GetProxmoxRealm return the defined test realm 58 | func GetProxmoxRealm() string { 59 | i := GetProxmoxConfigInstance() 60 | return i.Realm 61 | } 62 | 63 | // GetProxmoxHost return the defined test host 64 | func GetProxmoxHost() string { 65 | i := GetProxmoxConfigInstance() 66 | return i.Host 67 | } 68 | 69 | // EstablishConnection returns an open and test-checked Proxmox VE API connection 70 | func EstablishConnection(t *testing.T) *dockermachinedriverproxmoxve.ProxmoxVE { 71 | c, err := dockermachinedriverproxmoxve.GetProxmoxVEConnectionByValues(GetProxmoxAccess()) 72 | if err != nil { 73 | t.Log(c) 74 | t.Fatal(err) 75 | } 76 | return c 77 | } 78 | --------------------------------------------------------------------------------