From c3b113c5f2d8b5ad111f8262af6bd703f26be743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Gonz=C3=A1lez?= Date: Fri, 23 Mar 2018 12:58:29 +0100 Subject: [PATCH] Docker integration and GO API (#1889) * added docker integration files * changed dependencies for docker-integration files * README.md deleted * Deleted LICENSE --- SConstruct | 6 +- install.sh | 1 + src/docker_machine/SConstruct | 29 + src/docker_machine/generate.sh | 8 + .../src/docker_machine/Gopkg.lock | 95 ++ .../src/docker_machine/Gopkg.toml | 43 + .../src/docker_machine/Makefile | 17 + .../src/docker_machine/bin/main.go | 10 + .../src/docker_machine/opennebula.go | 731 +++++++++++++++ .../src/docker_machine/opennebula_test.go | 1 + src/oca/go/src/goca/Makefile | 18 + src/oca/go/src/goca/acl.go | 38 + src/oca/go/src/goca/cluster.go | 130 +++ src/oca/go/src/goca/datastore.go | 117 +++ src/oca/go/src/goca/document.go | 144 +++ src/oca/go/src/goca/glide.lock | 13 + src/oca/go/src/goca/glide.yaml | 4 + src/oca/go/src/goca/goca.go | 178 ++++ src/oca/go/src/goca/group.go | 99 +++ src/oca/go/src/goca/helper_test.go | 45 + src/oca/go/src/goca/host.go | 102 +++ src/oca/go/src/goca/image.go | 168 ++++ src/oca/go/src/goca/template.go | 136 +++ src/oca/go/src/goca/template_builder.go | 117 +++ src/oca/go/src/goca/template_builder_test.go | 36 + src/oca/go/src/goca/template_test.go | 132 +++ src/oca/go/src/goca/user.go | 134 +++ src/oca/go/src/goca/vdc.go | 167 ++++ src/oca/go/src/goca/version.go | 13 + src/oca/go/src/goca/virtualnetwork.go | 180 ++++ src/oca/go/src/goca/vm.go | 840 ++++++++++++++++++ src/oca/go/src/goca/vm_test.go | 204 +++++ src/oca/go/src/goca/xmlresource.go | 112 +++ src/oca/go/src/goca/zone.go | 88 ++ 34 files changed, 4155 insertions(+), 1 deletion(-) create mode 100644 src/docker_machine/SConstruct create mode 100755 src/docker_machine/generate.sh create mode 100644 src/docker_machine/src/docker_machine/Gopkg.lock create mode 100644 src/docker_machine/src/docker_machine/Gopkg.toml create mode 100644 src/docker_machine/src/docker_machine/Makefile create mode 100644 src/docker_machine/src/docker_machine/bin/main.go create mode 100644 src/docker_machine/src/docker_machine/opennebula.go create mode 100644 src/docker_machine/src/docker_machine/opennebula_test.go create mode 100644 src/oca/go/src/goca/Makefile create mode 100644 src/oca/go/src/goca/acl.go create mode 100644 src/oca/go/src/goca/cluster.go create mode 100644 src/oca/go/src/goca/datastore.go create mode 100644 src/oca/go/src/goca/document.go create mode 100644 src/oca/go/src/goca/glide.lock create mode 100644 src/oca/go/src/goca/glide.yaml create mode 100644 src/oca/go/src/goca/goca.go create mode 100644 src/oca/go/src/goca/group.go create mode 100644 src/oca/go/src/goca/helper_test.go create mode 100644 src/oca/go/src/goca/host.go create mode 100644 src/oca/go/src/goca/image.go create mode 100644 src/oca/go/src/goca/template.go create mode 100644 src/oca/go/src/goca/template_builder.go create mode 100644 src/oca/go/src/goca/template_builder_test.go create mode 100644 src/oca/go/src/goca/template_test.go create mode 100644 src/oca/go/src/goca/user.go create mode 100644 src/oca/go/src/goca/vdc.go create mode 100644 src/oca/go/src/goca/version.go create mode 100644 src/oca/go/src/goca/virtualnetwork.go create mode 100644 src/oca/go/src/goca/vm.go create mode 100644 src/oca/go/src/goca/vm_test.go create mode 100644 src/oca/go/src/goca/xmlresource.go create mode 100644 src/oca/go/src/goca/zone.go diff --git a/SConstruct b/SConstruct index 0973194b4f..c70767a793 100644 --- a/SConstruct +++ b/SConstruct @@ -167,6 +167,9 @@ main_env.Append(rubygems=ARGUMENTS.get('rubygems', 'no')) # Sunstone minified files generation main_env.Append(sunstone=ARGUMENTS.get('sunstone', 'no')) +# Docker-machine addon generation +main_env.Append(docker_machine=ARGUMENTS.get('docker_machine', 'no')) + if not main_env.GetOption('clean'): try: if mysql=='yes': @@ -263,7 +266,8 @@ build_scripts=[ 'src/sunstone/public/SConstruct', 'share/rubygems/SConstruct', 'src/im_mad/collectd/SConstruct', - 'src/client/SConstruct' + 'src/client/SConstruct', + 'src/docker_machine/SConstruct' ] for script in build_scripts: diff --git a/install.sh b/install.sh index a2b49be87c..cdb23c7843 100755 --- a/install.sh +++ b/install.sh @@ -591,6 +591,7 @@ BIN_FILES="src/nebula/oned \ src/cli/onevcenter \ src/onedb/onedb \ src/mad/utils/tty_expect \ + src/docker_machine/src/docker_machine/bin/docker-machine-driver-opennebula \ share/scripts/one" #------------------------------------------------------------------------------- diff --git a/src/docker_machine/SConstruct b/src/docker_machine/SConstruct new file mode 100644 index 0000000000..5f3e0db24e --- /dev/null +++ b/src/docker_machine/SConstruct @@ -0,0 +1,29 @@ +# SConstruct for share/scripts/rubygems + +# -------------------------------------------------------------------------- # +# Copyright 2002-2018, OpenNebula Project, OpenNebula Systems # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +import os + +Import('env') + +if env['docker_machine']=='yes': + print "Generating docker-machine-driver-opennebula\n" + exit_code=os.system("./generate.sh") + + if exit_code != 0: + print "Error generating docker-machine-driver-opennebula\n" + exit(-1) diff --git a/src/docker_machine/generate.sh b/src/docker_machine/generate.sh new file mode 100755 index 0000000000..b5a420cca7 --- /dev/null +++ b/src/docker_machine/generate.sh @@ -0,0 +1,8 @@ +#!/bin/bash +GOCA_PATH=$PWD/../oca/go +ADDON_PATH=$PWD + +export GOPATH=$GOPATH:$GOCA_PATH:$ADDON_PATH + +cd src/docker_machine/ +make build \ No newline at end of file diff --git a/src/docker_machine/src/docker_machine/Gopkg.lock b/src/docker_machine/src/docker_machine/Gopkg.lock new file mode 100644 index 0000000000..d136ff8421 --- /dev/null +++ b/src/docker_machine/src/docker_machine/Gopkg.lock @@ -0,0 +1,95 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/Azure/go-ansiterm" + packages = [ + ".", + "winterm" + ] + revision = "d6e3b3328b783f23731bc4d058875b0371ff8109" + +[[projects]] + branch = "master" + name = "github.com/docker/docker" + packages = [ + "pkg/term", + "pkg/term/windows" + ] + revision = "0c1006f1abc1af7aa6b9847754370d054dfa6c68" + +[[projects]] + name = "github.com/docker/machine" + packages = [ + "libmachine/drivers", + "libmachine/drivers/plugin", + "libmachine/drivers/plugin/localbinary", + "libmachine/drivers/rpc", + "libmachine/log", + "libmachine/mcnflag", + "libmachine/mcnutils", + "libmachine/ssh", + "libmachine/state", + "libmachine/version", + "version" + ] + revision = "89b833253d9412716a0291cbdccc94454c33d1b5" + version = "v0.14.0" + +[[projects]] + branch = "master" + name = "github.com/kolo/xmlrpc" + packages = ["."] + revision = "0826b98aaa29c0766956cb40d45cf7482a597671" + +[[projects]] + name = "github.com/sirupsen/logrus" + packages = ["."] + revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" + version = "v1.0.5" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = [ + "curve25519", + "ed25519", + "ed25519/internal/edwards25519", + "internal/chacha20", + "poly1305", + "ssh", + "ssh/terminal" + ] + revision = "c3a3ad6d03f7a915c0f7e194b7152974bb73d287" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = [ + "html", + "html/atom" + ] + revision = "6078986fec03a1dcc236c34816c71b0e05018fda" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = [ + "unix", + "windows" + ] + revision = "7ceb54c8418b8f9cdf0177b511d5cbb06e9fae39" + +[[projects]] + branch = "v2" + name = "gopkg.in/xmlpath.v2" + packages = ["."] + revision = "860cbeca3ebcc600db0b213c0e83ad6ce91f5739" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "8e5052c0521dfead3dd5b8672b0d2f066c70177a5e5a3ba4abd1449d087b98aa" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/src/docker_machine/src/docker_machine/Gopkg.toml b/src/docker_machine/src/docker_machine/Gopkg.toml new file mode 100644 index 0000000000..b8d85d5130 --- /dev/null +++ b/src/docker_machine/src/docker_machine/Gopkg.toml @@ -0,0 +1,43 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + +required = ["github.com/kolo/xmlrpc", "gopkg.in/xmlpath.v2"] + +[[constraint]] + name = "github.com/docker/machine" + version = "0.14.0" + +[[constraint]] + branch = "master" + name = "github.com/kolo/xmlrpc" + +[[constraint]] + branch = "v2" + name = "gopkg.in/xmlpath.v2" + +[prune] + go-tests = true + unused-packages = true diff --git a/src/docker_machine/src/docker_machine/Makefile b/src/docker_machine/src/docker_machine/Makefile new file mode 100644 index 0000000000..a46745fe9e --- /dev/null +++ b/src/docker_machine/src/docker_machine/Makefile @@ -0,0 +1,17 @@ +GODEP_BIN := $(word 1, $(subst :, ,$(GOPATH)))/bin/dep + +default: build + +bin/docker-machine-driver-opennebula: + $(GODEP_BIN) ensure + mv vendor/* .. + go build -o ./bin/docker-machine-driver-opennebula ./bin +build: clean bin/docker-machine-driver-opennebula + +clean: + $(RM) bin/docker-machine-driver-opennebula + +install: bin/docker-machine-driver-opennebula + cp -f ./bin/docker-machine-driver-opennebula $(GOPATH)/bin/ + +.PHONY: clean build install diff --git a/src/docker_machine/src/docker_machine/bin/main.go b/src/docker_machine/src/docker_machine/bin/main.go new file mode 100644 index 0000000000..acbacf824a --- /dev/null +++ b/src/docker_machine/src/docker_machine/bin/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "docker_machine" + "github.com/docker/machine/libmachine/drivers/plugin" +) + +func main() { + plugin.RegisterDriver(opennebula.NewDriver("", "")) +} diff --git a/src/docker_machine/src/docker_machine/opennebula.go b/src/docker_machine/src/docker_machine/opennebula.go new file mode 100644 index 0000000000..6274b27ac4 --- /dev/null +++ b/src/docker_machine/src/docker_machine/opennebula.go @@ -0,0 +1,731 @@ +package opennebula + +import ( + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "strconv" + "time" + "goca" + + "github.com/docker/machine/libmachine/drivers" + "github.com/docker/machine/libmachine/log" + "github.com/docker/machine/libmachine/mcnflag" + "github.com/docker/machine/libmachine/ssh" + "github.com/docker/machine/libmachine/state" +) + +type Driver struct { + *drivers.BaseDriver + TemplateName string + TemplateID string + NetworkName string + NetworkOwner string + NetworkID string + ImageName string + ImageOwner string + ImageID string + CPU string + VCPU string + Memory string + DiskSize string + ImageDevPrefix string + B2DSize string + User string + Password string + Xmlrpcurl string + Config goca.OneConfig + DisableVNC bool +} + +const ( + defaultTimeout = 1 * time.Second + defaultSSHUser = "docker" + defaultCPU = "1" + defaultVCPU = "1" + defaultMemory = "1024" + + // This is the contextualization script that will be executed by OpenNebula + contextScript = `#!/bin/sh + +if [ -f /etc/boot2docker ]; then + USERNAME=docker + USER_HOME=/home/docker +else + USERNAME=$DOCKER_SSH_USER + GROUPNAME=$DOCKER_SSH_USER + + if ! getent group $GROUPNAME; then + groupadd $GROUPNAME + fi + + if ! getent passwd $USERNAME; then + USER_HOME=/var/lib/$DOCKER_SSH_USER + useradd -m -d $USER_HOME -g $USERNAME $GROUPNAME + else + USER_HOME=$(getent passwd $USERNAME | cut -d: -f 6) + fi + + # Write sudoers + if [ ! -f /etc/sudoers.d/$USERNAME ]; then + echo -n "Defaults:$USERNAME " >> /etc/sudoers.d/$USERNAME + echo '!requiretty' >> /etc/sudoers.d/$USERNAME + echo "$USERNAME ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers.d/$USERNAME + fi +fi + +# Add DOCKER_SSH_PUBLIC_KEY + +AUTH_DIR="${USER_HOME}/.ssh" +AUTH_FILE="${AUTH_DIR}/authorized_keys" + +mkdir -m0700 -p $AUTH_DIR + +echo "$DOCKER_SSH_PUBLIC_KEY" >> $AUTH_FILE + +chown "${USERNAME}": ${AUTH_DIR} ${AUTH_FILE} +chmod 600 $AUTH_FILE` +) + +func NewDriver(hostName, storePath string) *Driver { + return &Driver{ + BaseDriver: &drivers.BaseDriver{ + SSHUser: defaultSSHUser, + MachineName: hostName, + StorePath: storePath, + }, + } +} + +func (d *Driver) buildConfig() { + d.Config = goca.NewConfig(d.User, d.Password, d.Xmlrpcurl) +} + +func (d *Driver) setClient() error { + d.buildConfig() + return goca.SetClient(d.Config) +} + +// GetCreateFlags registers the flags this driver adds to +// "docker hosts create" +func (d *Driver) GetCreateFlags() []mcnflag.Flag { + return []mcnflag.Flag{ + mcnflag.StringFlag{ + Name: "opennebula-cpu", + Usage: fmt.Sprintf("CPU value for the VM. Default: %d", defaultCPU), + EnvVar: "ONE_CPU", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-vcpu", + Usage: fmt.Sprintf("VCPUs for the VM. Default: %d", defaultVCPU), + EnvVar: "ONE_VCPU", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-memory", + Usage: fmt.Sprintf("Size of memory for VM in MB. Default: %d", defaultMemory), + EnvVar: "ONE_MEMORY", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-template-name", + Usage: "Template to use", + EnvVar: "ONE_TEMPLATE_NAME", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-template-id", + Usage: "Template ID to use", + EnvVar: "ONE_TEMPLATE_ID", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-network-name", + Usage: "Network to connect the machine to", + EnvVar: "ONE_NETWORK_NAME", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-network-id", + Usage: "Network ID to connect the machine to", + EnvVar: "ONE_NETWORK_ID", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-network-owner", + Usage: "User ID of the Network to connect the machine to", + EnvVar: "ONE_NETWORK_OWNER", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-image-name", + Usage: "Image to use as the OS", + EnvVar: "ONE_IMAGE_NAME", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-image-id", + Usage: "Image ID to use as the OS", + EnvVar: "ONE_IMAGE_ID", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-image-owner", + Usage: "Owner of the image to use as the OS", + EnvVar: "ONE_IMAGE_OWNER", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-dev-prefix", + Usage: "Dev prefix to use for the images: 'vd', 'sd', 'hd', etc..", + EnvVar: "ONE_IMAGE_DEV_PREFIX", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-disk-resize", + Usage: "Size of disk for VM in MB", + EnvVar: "ONE_DISK_SIZE", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-b2d-size", + Usage: "Size of the Volatile disk in MB (only for b2d)", + EnvVar: "ONE_B2D_DATA_SIZE", + Value: "", + }, + mcnflag.StringFlag{ + Name: "opennebula-ssh-user", + Usage: "Set the name of the SSH user", + EnvVar: "ONE_SSH_USER", + Value: defaultSSHUser, + }, + mcnflag.BoolFlag{ + Name: "opennebula-disable-vnc", + Usage: "VNC is enabled by default. Disable it with this flag", + EnvVar: "ONE_DISABLE_VNC", + }, + mcnflag.StringFlag{ + Name: "opennebula-user", + Usage: "Set the user for authentication", + EnvVar: "ONE_USER", + }, + mcnflag.StringFlag{ + Name: "opennebula-password", + Usage: "Set the password for authentication", + EnvVar: "ONE_PASSWORD", + }, + mcnflag.StringFlag{ + Name: "opennebula-xmlrpcurl", + Usage: "Set the url for one xmlrpc server", + EnvVar: "ONE_XMLRPC", + }, + } +} + +func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { + d.SetSwarmConfigFromFlags(flags) + + // Authentication + d.User = flags.String("opennebula-user") + d.Password = flags.String("opennebula-password") + d.Xmlrpcurl = flags.String("opennebula-xmlrpcurl") + + // Capacity + d.CPU = flags.String("opennebula-cpu") + d.VCPU = flags.String("opennebula-vcpu") + d.Memory = flags.String("opennebula-memory") + + // Template + d.TemplateName = flags.String("opennebula-template-name") + d.TemplateID = flags.String("opennebula-template-id") + + // Network + d.NetworkName = flags.String("opennebula-network-name") + d.NetworkID = flags.String("opennebula-network-id") + d.NetworkOwner = flags.String("opennebula-network-owner") + + // Storage + d.ImageID = flags.String("opennebula-image-id") + d.ImageName = flags.String("opennebula-image-name") + d.ImageOwner = flags.String("opennebula-image-owner") + + d.ImageDevPrefix = flags.String("opennebula-dev-prefix") + d.DiskSize = flags.String("opennebula-disk-resize") + d.B2DSize = flags.String("opennebula-b2d-size") + + // Provision + d.SSHUser = flags.String("opennebula-ssh-user") + + // VNC + d.DisableVNC = flags.Bool("opennebula-disable-vnc") + + // Either TemplateName or TemplateID + if d.TemplateName != "" && d.TemplateID != "" { + return errors.New("specify only one of: --opennebula-template-name or --opennebula-template-id, not both") + } + + // Either NetworkName or NetworkID + if d.NetworkName != "" && d.NetworkID != "" { + return errors.New("specify only one of: --opennebula-network-name or --opennebula-network-id, not both") + } + + // Either ImageName or ImageID + if d.ImageName != "" && d.ImageID != "" { + return errors.New("specify only one of: --opennebula-image-name or --opennebula-image-id, not both") + } + + // Required and incompatible options for Template + if d.TemplateName != "" || d.TemplateID != "" { + // Template has been specified: + + // ImageName and ImageID are incompatible + if d.ImageName != "" || d.ImageID != "" { + return errors.New("options --opennebula-image-* are incompatible with --opennebula-template-*") + } + + // ImageDevPrefix is incompatible + if d.ImageDevPrefix != "" { + return errors.New("option: --opennebula-dev-prefix is incompatible with --opennebula-template-*") + } + // DiskSize is incompatible + if d.DiskSize != "" { + return errors.New("option: --opennebula-disk-resize is incompatible with --opennebula-template-*") + } + // B2DSize is incompatible + if d.B2DSize != "" { + return errors.New("option: --opennebula-disk-resize is incompatible with --opennebula-template-*") + } + // DisableVNC is incompatible + if d.DisableVNC { + return errors.New("option: --opennebula-disable-vnc is incompatible with --opennebula-template-*") + } + } else { + //Template has NOT been specified: + + // ImageName or ImageID is required + if d.ImageName == "" && d.ImageID == "" { + return errors.New("specify a image to use as the OS with --opennebula-image-name or --opennebula-image-id") + } + + // NetworkName or NetworkID is required + if d.NetworkName == "" && d.NetworkID == "" { + return errors.New("specify a network to connect to with --opennebula-network-name or --opennebula-network-id") + } + + // Assign default capacity values + if d.CPU == "" { + d.CPU = defaultCPU + } + + if d.VCPU == "" { + d.VCPU = defaultVCPU + } + + if d.Memory == "" { + d.Memory = defaultMemory + } + } + + return nil +} + +func (d *Driver) DriverName() string { + return "opennebula" +} + +func (d *Driver) GetSSHHostname() (string, error) { + return d.GetIP() +} + +func (d *Driver) GetSSHUsername() string { + return d.SSHUser +} + +func (d *Driver) PreCreateCheck() error { + return nil +} + +func (d *Driver) Create() error { + var ( + vector *goca.TemplateBuilderVector + vmtemplate *goca.Template + + err error + ) + + // build config and set the xmlrpc client + d.setClient() + + log.Infof("Creating SSH key..") + if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { + return err + } + + pubKey, err := ioutil.ReadFile(d.publicSSHKeyPath()) + if err != nil { + return err + } + + // Create template + template := goca.NewTemplateBuilder() + + if d.TemplateName != "" || d.TemplateID != "" { + // Template has been specified + } else { + // Template has NOT been specified + template.AddValue("NAME", d.MachineName) + + // OS Boot + vector = template.NewVector("OS") + vector.AddValue("BOOT", "disk0") + + // OS Disk + vector = template.NewVector("DISK") + + if d.ImageID != "" { + vector.AddValue("IMAGE_ID", d.ImageID) + } else { + vector.AddValue("IMAGE", d.ImageName) + if d.ImageOwner != "" { + vector.AddValue("IMAGE_UNAME", d.ImageOwner) + } + } + + if d.DiskSize != "" { + vector.AddValue("SIZE", d.DiskSize) + } + + if d.ImageDevPrefix != "" { + vector.AddValue("DEV_PREFIX", d.ImageDevPrefix) + } + + // Add a volatile disk for b2d + if d.B2DSize != "" { + vector = template.NewVector("DISK") + vector.AddValue("SIZE", d.B2DSize) + vector.AddValue("TYPE", "fs") + vector.AddValue("FORMAT", "raw") + } + + // VNC + if !d.DisableVNC { + vector = template.NewVector("GRAPHICS") + vector.AddValue("LISTEN", "0.0.0.0") + vector.AddValue("TYPE", "vnc") + } + } + + // Capacity + if d.CPU != "" { + template.AddValue("CPU", d.CPU) + } + + if d.Memory != "" { + template.AddValue("MEMORY", d.Memory) + } + + if d.VCPU != "" { + template.AddValue("VCPU", d.VCPU) + } + + // Network + if d.NetworkName != "" || d.NetworkID != "" { + vector = template.NewVector("NIC") + + if d.NetworkName != "" { + vector.AddValue("NETWORK", d.NetworkName) + if d.NetworkOwner != "" { + vector.AddValue("NETWORK_UNAME", d.NetworkOwner) + } + } + + if d.NetworkID != "" { + vector.AddValue("NETWORK_ID", d.NetworkID) + } + } + + // Context + vector = template.NewVector("CONTEXT") + vector.AddValue("NETWORK", "YES") + vector.AddValue("SSH_PUBLIC_KEY", "$USER[SSH_PUBLIC_KEY]") + vector.AddValue("DOCKER_SSH_USER", d.SSHUser) + vector.AddValue("DOCKER_SSH_PUBLIC_KEY", string(pubKey)) + contextScript64 := base64.StdEncoding.EncodeToString([]byte(contextScript)) + vector.AddValue("START_SCRIPT_BASE64", contextScript64) + + // Instantiate + log.Infof("Starting VM..") + + if d.TemplateName != "" || d.TemplateID != "" { + + if d.TemplateName != "" { + vmtemplate, err = goca.NewTemplateFromName(d.TemplateName) + if err != nil { + return err + } + } else { + templateID, err := strconv.Atoi(d.TemplateID) + if err != nil { + return err + } + vmtemplate = goca.NewTemplate(uint(templateID)) + } + + _, err = vmtemplate.Instantiate(d.MachineName, false, template.String()) + + } else { + _, err = goca.CreateVM(template.String(), false) + } + + if err != nil { + return err + } + + if d.IPAddress, err = d.GetIP(); err != nil { + return err + } + + return d.Start() +} + +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + return fmt.Sprintf("tcp://%s:2376", ip), nil +} + +func (d *Driver) GetIP() (string, error) { + d.setClient() + vm, err := goca.NewVMFromName(d.MachineName) + if err != nil { + return "", err + } + + err = vm.Info() + if err != nil { + return "", err + } + + if ip, ok := vm.XPath("/VM/TEMPLATE/NIC/IP"); ok { + d.IPAddress = ip + } + + if d.IPAddress == "" { + return "", fmt.Errorf("IP address is not set") + } + + return d.IPAddress, nil +} + +func (d *Driver) GetState() (state.State, error) { + d.setClient() + vm, err := goca.NewVMFromName(d.MachineName) + if err != nil { + return state.None, err + } + + err = vm.Info() + if err != nil { + return state.None, err + } + + vmState, lcmState, err := vm.StateString() + if err != nil { + return state.None, err + } + + switch vmState { + case "INIT", "PENDING", "HOLD", "CLONING": + return state.Starting, nil + case "ACTIVE": + switch lcmState { + case "RUNNING", + + // migration is considered running + "MIGRATE", + "SAVE_MIGRATE", + "PROLOG_MIGRATE", + "BOOT_MIGRATE", + + // recover --recreate is also considered running + "CLEANUP_RESUBMIT", + + // operation on the VMs + "HOTPLUG", + "HOTPLUG_SNAPSHOT", + "HOTPLUG_NIC", + "HOTPLUG_SAVEAS", + "DISK_SNAPSHOT", + "DISK_SNAPSHOT_DELETE": + + return state.Running, nil + + case "LCM_INIT", + "PROLOG", + "BOOT", + "PROLOG_RESUME", + "BOOT_UNKNOWN", + "BOOT_POWEROFF", + "BOOT_SUSPENDED", + "BOOT_STOPPED", + "PROLOG_UNDEPLOY", + "BOOT_UNDEPLOY", + "PROLOG_MIGRATE_UNKNOWN": + + return state.Starting, nil + + case "HOTPLUG_SAVEAS_POWEROFF", + "HOTPLUG_SAVEAS_SUSPENDED", + "HOTPLUG_PROLOG_POWEROFF", + "HOTPLUG_EPILOG_POWEROFF", + "PROLOG_MIGRATE_POWEROFF", + "PROLOG_MIGRATE_SUSPEND", + "DISK_SNAPSHOT_POWEROFF", + "DISK_SNAPSHOT_REVERT_POWEROFF", + "DISK_SNAPSHOT_DELETE_POWEROFF", + "DISK_SNAPSHOT_SUSPENDED", + "DISK_SNAPSHOT_REVERT_SUSPENDED", + "DISK_SNAPSHOT_DELETE_SUSPENDED": + + return state.Stopped, nil + + case "SAVE_SUSPEND", + "SAVE_STOP", + "EPILOG_STOP", + "EPILOG", + "SHUTDOWN", + "SHUTDOWN_POWEROFF", + "SHUTDOWN_UNDEPLOY", + "EPILOG_UNDEPLOY": + return state.Stopping, nil + + case "UNKNOWN", + "CLEANUP_DELETE", + "BOOT_FAILURE", + "BOOT_MIGRATE_FAILURE", + "PROLOG_MIGRATE_FAILURE", + "PROLOG_FAILURE", + "EPILOG_FAILURE", + "EPILOG_STOP_FAILURE", + "EPILOG_UNDEPLOY_FAILURE", + "PROLOG_MIGRATE_POWEROFF_FAILURE", + "PROLOG_MIGRATE_SUSPEND_FAILURE", + "BOOT_UNDEPLOY_FAILURE", + "BOOT_STOPPED_FAILURE", + "PROLOG_RESUME_FAILURE", + "PROLOG_UNDEPLOY_FAILURE", + "PROLOG_MIGRATE_UNKNOWN_FAILURE": + + return state.Error, nil + + } + + case "POWEROFF", "UNDEPLOYED": + return state.Stopped, nil + case "STOPPED", "SUSPENDED": + return state.Saved, nil + case "DONE", "FAILED", "CLONING_FAILURE": + return state.Error, nil + } + + return state.Error, nil +} + +func (d *Driver) Start() error { + d.setClient() + vm, err := goca.NewVMFromName(d.MachineName) + if err != nil { + return err + } + + vm.Resume() + + s := state.None + for retry := 0; retry < 50 && s != state.Running; retry++ { + s, err = d.GetState() + if err != nil { + return err + } + + switch s { + case state.Error: + return errors.New("vM in error state") + default: + time.Sleep(2 * time.Second) + } + } + + if d.IPAddress == "" { + if d.IPAddress, err = d.GetIP(); err != nil { + return err + } + } + + log.Infof("Waiting for SSH..") + + // Wait for SSH over NAT to be available before returning to user + return drivers.WaitForSSH(d) +} + +func (d *Driver) Stop() error { + d.setClient() + vm, err := goca.NewVMFromName(d.MachineName) + if err != nil { + return err + } + + err = vm.Poweroff() + if err != nil { + return err + } + + return nil +} + +func (d *Driver) Remove() error { + d.setClient() + vm, err := goca.NewVMFromName(d.MachineName) + if err != nil { + return err + } + + err = vm.TerminateHard() + if err != nil { + return err + } + + return nil +} + +func (d *Driver) Restart() error { + d.setClient() + vm, err := goca.NewVMFromName(d.MachineName) + if err != nil { + return err + } + + err = vm.Reboot() + if err != nil { + return err + } + + return nil +} + +func (d *Driver) Kill() error { + d.setClient() + vm, err := goca.NewVMFromName(d.MachineName) + if err != nil { + return err + } + + return vm.PoweroffHard() +} + +func (d *Driver) publicSSHKeyPath() string { + return d.GetSSHKeyPath() + ".pub" +} diff --git a/src/docker_machine/src/docker_machine/opennebula_test.go b/src/docker_machine/src/docker_machine/opennebula_test.go new file mode 100644 index 0000000000..40d09e2259 --- /dev/null +++ b/src/docker_machine/src/docker_machine/opennebula_test.go @@ -0,0 +1 @@ +package opennebula diff --git a/src/oca/go/src/goca/Makefile b/src/oca/go/src/goca/Makefile new file mode 100644 index 0000000000..060dd4d78f --- /dev/null +++ b/src/oca/go/src/goca/Makefile @@ -0,0 +1,18 @@ +.PHONY: build test help default + +default: test + +help: + @echo 'Management commands for goca:' + @echo + @echo 'Usage:' + @echo ' make test Run the tests.' + @echo ' make get-deps runs glide install, mostly used for ci.' + @echo + +test: + go test $(glide nv) + golint $(glide nv) + +get-deps: + glide install diff --git a/src/oca/go/src/goca/acl.go b/src/oca/go/src/goca/acl.go new file mode 100644 index 0000000000..e53a661b30 --- /dev/null +++ b/src/oca/go/src/goca/acl.go @@ -0,0 +1,38 @@ +package goca + +// ACLPool represents an OpenNebula ACL list pool +type ACLPool struct { + XMLResource +} + +// NewACLPool returns an acl pool. A connection to OpenNebula is +// performed. +func NewACLPool() (*ACLPool, error) { + response, err := client.Call("one.acl.info") + if err != nil { + return nil, err + } + + aclpool := &ACLPool{XMLResource{body: response.Body()}} + + return aclpool, err +} + +// CreateACLRule adds a new ACL rule. +// * user: User component of the new rule. A string containing a hex number. +// * resource: Resource component of the new rule. A string containing a hex number. +// * rights: Rights component of the new rule. A string containing a hex number. +func CreateACLRule(user, resource, rights string) (uint, error) { + response, err := client.Call("one.acl.addrule", user, resource, rights) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// DeleteACLRule deletes an ACL rule. +func DeleteACLRule(aclID uint) error { + _, err := client.Call("one.acl.delrule", int(aclID)) + return err +} diff --git a/src/oca/go/src/goca/cluster.go b/src/oca/go/src/goca/cluster.go new file mode 100644 index 0000000000..126698ed28 --- /dev/null +++ b/src/oca/go/src/goca/cluster.go @@ -0,0 +1,130 @@ +package goca + +// Cluster represents an OpenNebula Cluster +type Cluster struct { + XMLResource + ID uint + Name string +} + +// ClusterPool represents an OpenNebula ClusterPool +type ClusterPool struct { + XMLResource +} + +// NewClusterPool returns a cluster pool. A connection to OpenNebula is +// performed. +func NewClusterPool() (*ClusterPool, error) { + response, err := client.Call("one.clusterpool.info") + if err != nil { + return nil, err + } + + clusterpool := &ClusterPool{XMLResource{body: response.Body()}} + + return clusterpool, err + +} + +// NewCluster finds a cluster object by ID. No connection to OpenNebula. +func NewCluster(id uint) *Cluster { + return &Cluster{ID: id} +} + +// NewClusterFromName finds a cluster object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the cluster. +func NewClusterFromName(name string) (*Cluster, error) { + clusterPool, err := NewClusterPool() + if err != nil { + return nil, err + } + + id, err := clusterPool.GetIDFromName(name, "/CLUSTER_POOL/CLUSTER") + if err != nil { + return nil, err + } + + return NewCluster(id), nil +} + +// CreateCluster allocates a new cluster. It returns the new cluster ID. +func CreateCluster(name string) (uint, error) { + response, err := client.Call("one.cluster.allocate", name) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Delete deletes the given cluster from the pool. +func (cluster *Cluster) Delete() error { + _, err := client.Call("one.cluster.delete", cluster.ID) + return err +} + +// Update replaces the cluster cluster contents. +// * tpl: The new cluster contents. Syntax can be the usual attribute=value or +// XML. +// * appendCluster: Update type: 0: Replace the whole cluster. 1: Merge new +// cluster with the existing one. +func (cluster *Cluster) Update(tpl string, appendCluster int) error { + _, err := client.Call("one.cluster.update", cluster.ID, tpl, appendCluster) + return err +} + +// AddHost adds a host to the given cluster. +// * hostID: The host ID. +func (cluster *Cluster) AddHost(hostID uint) error { + _, err := client.Call("one.cluster.addhost", cluster.ID, int(hostID)) + return err +} + +// DelHost removes a host from the given cluster. +// * hostID: The host ID. +func (cluster *Cluster) DelHost(hostID uint) error { + _, err := client.Call("one.cluster.delhost", cluster.ID, int(hostID)) + return err +} + +// AddDatastore adds a datastore to the given cluster. +// * dsID: The datastore ID. +func (cluster *Cluster) AddDatastore(dsID uint) error { + _, err := client.Call("one.cluster.adddatastore", cluster.ID, int(dsID)) + return err +} + +// DelDatastore removes a datastore from the given cluster. +// * dsID: The datastore ID. +func (cluster *Cluster) DelDatastore(dsID uint) error { + _, err := client.Call("one.cluster.deldatastore", cluster.ID, int(dsID)) + return err +} + +// AddVnet adds a vnet to the given cluster. +// * vnetID: The vnet ID. +func (cluster *Cluster) AddVnet(vnetID uint) error { + _, err := client.Call("one.cluster.addvnet", cluster.ID, int(vnetID)) + return err +} + +// DelVnet removes a vnet from the given cluster. +// * vnetID: The vnet ID. +func (cluster *Cluster) DelVnet(vnetID uint) error { + _, err := client.Call("one.cluster.delvnet", cluster.ID, int(vnetID)) + return err +} + +// Rename renames a cluster. +// * newName: The new name. +func (cluster *Cluster) Rename(newName string) error { + _, err := client.Call("one.cluster.rename", cluster.ID, newName) + return err +} + +// Info retrieves information for the cluster. +func (cluster *Cluster) Info() error { + _, err := client.Call("one.cluster.info", cluster.ID) + return err +} diff --git a/src/oca/go/src/goca/datastore.go b/src/oca/go/src/goca/datastore.go new file mode 100644 index 0000000000..76d1eb8d14 --- /dev/null +++ b/src/oca/go/src/goca/datastore.go @@ -0,0 +1,117 @@ +package goca + +// Datastore represents an OpenNebula Datastore +type Datastore struct { + XMLResource + ID uint + Name string +} + +// DatastorePool represents an OpenNebula DatastorePool +type DatastorePool struct { + XMLResource +} + +// NewDatastorePool returns a datastore pool. A connection to OpenNebula is +// performed. +func NewDatastorePool() (*DatastorePool, error) { + response, err := client.Call("one.datastorepool.info") + if err != nil { + return nil, err + } + + datastorepool := &DatastorePool{XMLResource{body: response.Body()}} + + return datastorepool, err +} + +// NewDatastore finds a datastore object by ID. No connection to OpenNebula. +func NewDatastore(id uint) *Datastore { + return &Datastore{ID: id} +} + +// NewDatastoreFromName finds a datastore object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the datastore. +func NewDatastoreFromName(name string) (*Datastore, error) { + datastorePool, err := NewDatastorePool() + if err != nil { + return nil, err + } + + id, err := datastorePool.GetIDFromName(name, "/DATASTORE_POOL/DATASTORE") + if err != nil { + return nil, err + } + + return NewDatastore(id), nil +} + +// CreateDatastore allocates a new datastore. It returns the new datastore ID. +// * tpl: template of the datastore +// * clusterID: The cluster ID. If it is -1, the default one will be used. +func CreateDatastore(tpl string, clusterID int) (uint, error) { + response, err := client.Call("one.datastore.allocate", tpl, clusterID) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Delete deletes the given datastore from the pool. +func (datastore *Datastore) Delete() error { + _, err := client.Call("one.datastore.delete", datastore.ID) + return err +} + +// Update replaces the datastore template contents. +// * tpl: The new template contents. Syntax can be the usual attribute=value or XML. +// * appendTemplate: Update type: 0: Replace the whole template. 1: Merge new template with the existing one. +func (datastore *Datastore) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.datastore.update", datastore.ID, tpl, appendTemplate) + return err +} + +// Chmod changes the permission bits of a datastore. +// * uu: USER USE bit. If set to -1, it will not change. +// * um: USER MANAGE bit. If set to -1, it will not change. +// * ua: USER ADMIN bit. If set to -1, it will not change. +// * gu: GROUP USE bit. If set to -1, it will not change. +// * gm: GROUP MANAGE bit. If set to -1, it will not change. +// * ga: GROUP ADMIN bit. If set to -1, it will not change. +// * ou: OTHER USE bit. If set to -1, it will not change. +// * om: OTHER MANAGE bit. If set to -1, it will not change. +// * oa: OTHER ADMIN bit. If set to -1, it will not change. +func (datastore *Datastore) Chmod(uu, um, ua, gu, gm, ga, ou, om, oa int) error { + _, err := client.Call("one.datastore.chmod", datastore.ID, uu, um, ua, gu, gm, ga, ou, om, oa) + return err +} + +// Chown changes the ownership of a datastore. +// * userID: The User ID of the new owner. If set to -1, it will not change. +// * groupID: The Group ID of the new group. If set to -1, it will not change. +func (datastore *Datastore) Chown(userID, groupID uint) error { + _, err := client.Call("one.datastore.chown", datastore.ID, int(userID), int(groupID)) + return err +} + +// Rename renames a datastore. +// * newName: The new name. +func (datastore *Datastore) Rename(newName string) error { + _, err := client.Call("one.datastore.rename", datastore.ID, newName) + return err +} + +// Enable enables or disables a datastore. +// * enable: True for enabling +func (datastore *Datastore) Enable(enable bool) error { + _, err := client.Call("one.datastore.enable", datastore.ID, enable) + return err +} + +// Info retrieves information for the datastore. +func (datastore *Datastore) Info() error { + _, err := client.Call("one.datastore.info", datastore.ID) + return err +} diff --git a/src/oca/go/src/goca/document.go b/src/oca/go/src/goca/document.go new file mode 100644 index 0000000000..270c36a16b --- /dev/null +++ b/src/oca/go/src/goca/document.go @@ -0,0 +1,144 @@ +package goca + +import "errors" + +// Document represents an OpenNebula Document +type Document struct { + XMLResource + ID uint + Name string +} + +// DocumentPool represents an OpenNebula DocumentPool +type DocumentPool struct { + XMLResource +} + +// NewDocumentPool returns a document pool. A connection to OpenNebula is +// performed. +func NewDocumentPool(documentType int, args ...int) (*DocumentPool, error) { + var who, start, end int + + switch len(args) { + case 0: + who = PoolWhoMine + start = -1 + end = -1 + case 1: + who = args[0] + start = -1 + end = -1 + case 3: + who = args[0] + start = args[1] + end = args[2] + default: + return nil, errors.New("Wrong number of arguments") + } + + response, err := client.Call("one.documentpool.info", who, start, end, documentType) + if err != nil { + return nil, err + } + + documentpool := &DocumentPool{XMLResource{body: response.Body()}} + + return documentpool, err +} + +// NewDocument finds a document object by ID. No connection to OpenNebula. +func NewDocument(id uint) *Document { + return &Document{ID: id} +} + +// NewDocumentFromName finds a document object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the document. +func NewDocumentFromName(name string, documentType int) (*Document, error) { + documentPool, err := NewDocumentPool(documentType) + if err != nil { + return nil, err + } + + id, err := documentPool.GetIDFromName(name, "/DOCUMENT_POOL/DOCUMENT") + if err != nil { + return nil, err + } + + return NewDocument(id), nil +} + +// CreateDocument allocates a new document. It returns the new document ID. +func CreateDocument(tpl string, documentType int) (uint, error) { + response, err := client.Call("one.document.allocate", tpl, documentType) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Clone clones an existing document. +// * newName: Name for the new document. +func (document *Document) Clone(newName string) error { + _, err := client.Call("one.document.clone", document.ID, newName) + return err +} + +// Delete deletes the given document from the pool. +func (document *Document) Delete() error { + _, err := client.Call("one.document.delete", document.ID) + return err +} + +// Update replaces the document template contents. +// * tpl: The new document template contents. Syntax can be the usual attribute=value or XML. +// * appendTemplate: Update type: 0: Replace the whole template. 1: Merge new template with the existing one. +func (document *Document) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.document.update", document.ID, tpl, appendTemplate) + return err +} + +// Chmod changes the permission bits of a document. +// * uu: USER USE bit. If set to -1, it will not change. +// * um: USER MANAGE bit. If set to -1, it will not change. +// * ua: USER ADMIN bit. If set to -1, it will not change. +// * gu: GROUP USE bit. If set to -1, it will not change. +// * gm: GROUP MANAGE bit. If set to -1, it will not change. +// * ga: GROUP ADMIN bit. If set to -1, it will not change. +// * ou: OTHER USE bit. If set to -1, it will not change. +// * om: OTHER MANAGE bit. If set to -1, it will not change. +// * oa: OTHER ADMIN bit. If set to -1, it will not change. +func (document *Document) Chmod(uu, um, ua, gu, gm, ga, ou, om, oa int) error { + _, err := client.Call("one.document.chmod", document.ID, uu, um, ua, gu, gm, ga, ou, om, oa) + return err +} + +// Chown changes the ownership of a document. +// * userID: The User ID of the new owner. If set to -1, it will not change. +// * groupID: The Group ID of the new group. If set to -1, it will not change. +func (document *Document) Chown(userID, groupID uint) error { + _, err := client.Call("one.document.chown", document.ID, int(userID), int(groupID)) + return err +} + +// Rename renames a document. +// * newName: The new name. +func (document *Document) Rename(newName string) error { + _, err := client.Call("one.document.rename", document.ID, newName) + return err +} + +// Lock locks the document at the api level. The lock automatically expires after 2 minutes. +// * applicationName: String to identify the application requesting the lock. +func (document *Document) Lock(applicationName string) error { + _, err := client.Call("one.document.lock", document.ID, applicationName) + return err +} + +// Unlock unlocks the document at the api level. +// * applicationName: String to identify the application requesting the lock. +func (document *Document) Unlock(applicationName string) error { + _, err := client.Call("one.document.unlock", document.ID, applicationName) + return err +} diff --git a/src/oca/go/src/goca/glide.lock b/src/oca/go/src/goca/glide.lock new file mode 100644 index 0000000000..ac7c70d6e1 --- /dev/null +++ b/src/oca/go/src/goca/glide.lock @@ -0,0 +1,13 @@ +hash: ac9bb59552226f818b59a1f54831140c20901e060de067f144f54d4e100d0e12 +updated: 2017-11-28T12:03:57.539724958+01:00 +imports: +- name: github.com/kolo/xmlrpc + version: 0826b98aaa29c0766956cb40d45cf7482a597671 +- name: golang.org/x/net + version: fc492d2e106922eb3aa4bca863d55e882c087ae3 + subpackages: + - html + - html/atom +- name: gopkg.in/xmlpath.v2 + version: 860cbeca3ebcc600db0b213c0e83ad6ce91f5739 +testImports: [] diff --git a/src/oca/go/src/goca/glide.yaml b/src/oca/go/src/goca/glide.yaml new file mode 100644 index 0000000000..0660820cd8 --- /dev/null +++ b/src/oca/go/src/goca/glide.yaml @@ -0,0 +1,4 @@ +package: github.com/OpenNebula/goca +import: +- package: github.com/kolo/xmlrpc +- package: gopkg.in/xmlpath.v2 diff --git a/src/oca/go/src/goca/goca.go b/src/oca/go/src/goca/goca.go new file mode 100644 index 0000000000..88d6272245 --- /dev/null +++ b/src/oca/go/src/goca/goca.go @@ -0,0 +1,178 @@ +package goca + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/kolo/xmlrpc" +) + +var ( + client *oneClient +) + +// OneConfig contains the information to communicate with OpenNebula +type OneConfig struct { + // Token is the authentication string. In the format of : + Token string + + // XmlrpcURL contains OpenNebula's XML-RPC API endpoint. Defaults to + // http://localhost:2633/RPC2 + XmlrpcURL string +} + +type oneClient struct { + token string + xmlrpcClient *xmlrpc.Client + xmlrpcClientError error +} + +type response struct { + status bool + body string + bodyInt int +} + +// Resource implements an OpenNebula Resource methods. *XMLResource implements +// all these methods +type Resource interface { + Body() string + XPath(string) (string, bool) + XPathIter(string) *XMLIter + GetIDFromName(string, string) (uint, error) +} + +// Initializes the client variable, used as a singleton +func init() { + err := SetClient(NewConfig("", "", "")) + if err != nil { + log.Fatal(err) + } +} + +// NewConfig returns a new OneConfig object with the specified user, password, +// and xmlrpcURL +func NewConfig(user string, password string, xmlrpcURL string) OneConfig { + var authToken string + var oneAuthPath string + + oneXmlrpc := xmlrpcURL + + if user == "" && password == "" { + oneAuthPath = os.Getenv("ONE_AUTH") + if oneAuthPath == "" { + oneAuthPath = os.Getenv("HOME") + "/.one/one_auth" + } + + token, err := ioutil.ReadFile(oneAuthPath) + if err == nil { + authToken = strings.TrimSpace(string(token)) + } else { + authToken = "" + } + } else { + authToken = user + ":" + password + } + + if oneXmlrpc == "" { + oneXmlrpc = os.Getenv("ONE_XMLRPC") + if oneXmlrpc == "" { + oneXmlrpc = "http://localhost:2633/RPC2" + } + } + + config := OneConfig{ + Token: authToken, + XmlrpcURL: oneXmlrpc, + } + + return config +} + +// SetClient assigns a value to the client variable +func SetClient(conf OneConfig) error { + + xmlrpcClient, xmlrpcClientError := xmlrpc.NewClient(conf.XmlrpcURL, nil) + + client = &oneClient{ + token: conf.Token, + xmlrpcClient: xmlrpcClient, + xmlrpcClientError: xmlrpcClientError, + } + + return nil +} + +// SystemVersion returns the current OpenNebula Version +func SystemVersion() (string, error) { + response, err := client.Call("one.system.version") + if err != nil { + return "", err + } + + return response.Body(), nil +} + +// Call is an XML-RPC wrapper. It returns a pointer to response and an error. +func (c *oneClient) Call(method string, args ...interface{}) (*response, error) { + var ( + ok bool + + status bool + body string + bodyInt int64 + ) + + if c.xmlrpcClientError != nil { + return nil, fmt.Errorf("Unitialized client. Token: '%s', xmlrpcClient: '%s'", c.token, c.xmlrpcClientError) + } + + result := []interface{}{} + + xmlArgs := make([]interface{}, len(args)+1) + + xmlArgs[0] = c.token + copy(xmlArgs[1:], args[:]) + + err := c.xmlrpcClient.Call(method, xmlArgs, &result) + if err != nil { + log.Fatal(err) + } + + status, ok = result[0].(bool) + if ok == false { + log.Fatal("Unexpected XML-RPC response. Expected: Index 0 Boolean") + } + + body, ok = result[1].(string) + if ok == false { + bodyInt, ok = result[1].(int64) + if ok == false { + log.Fatal("Unexpected XML-RPC response. Expected: Index 0 Int or String") + } + } + + // TODO: errCode? result[2] + + r := &response{status, body, int(bodyInt)} + + if status == false { + err = errors.New(body) + } + + return r, err +} + +// Body accesses the body of the response +func (r *response) Body() string { + return r.body +} + +// BodyInt accesses the body of the response, if it's an int. +func (r *response) BodyInt() int { + return r.bodyInt +} diff --git a/src/oca/go/src/goca/group.go b/src/oca/go/src/goca/group.go new file mode 100644 index 0000000000..c358651660 --- /dev/null +++ b/src/oca/go/src/goca/group.go @@ -0,0 +1,99 @@ +package goca + +// Group represents an OpenNebula Group +type Group struct { + XMLResource + ID uint + Name string +} + +// GroupPool represents an OpenNebula GroupPool +type GroupPool struct { + XMLResource +} + +// NewGroupPool returns a group pool. A connection to OpenNebula is +// performed. +func NewGroupPool() (*GroupPool, error) { + response, err := client.Call("one.grouppool.info") + if err != nil { + return nil, err + } + + grouppool := &GroupPool{XMLResource{body: response.Body()}} + + return grouppool, err +} + +// NewGroup finds a group object by ID. No connection to OpenNebula. +func NewGroup(id uint) *Group { + return &Group{ID: id} +} + +// NewGroupFromName finds a group object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the group. +func NewGroupFromName(name string) (*Group, error) { + groupPool, err := NewGroupPool() + if err != nil { + return nil, err + } + + id, err := groupPool.GetIDFromName(name, "/GROUP_POOL/GROUP") + if err != nil { + return nil, err + } + + return NewGroup(id), nil +} + +// CreateGroup allocates a new group. It returns the new group ID. +func CreateGroup(name string) (uint, error) { + response, err := client.Call("one.group.allocate", name) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Delete deletes the given group from the pool. +func (group *Group) Delete() error { + _, err := client.Call("one.group.delete", group.ID) + return err +} + +// Info retrieves information for the group. +func (group *Group) Info() error { + _, err := client.Call("one.group.info", group.ID) + return err +} + +// Update replaces the group template contents. +// * tpl: The new template contents. Syntax can be the usual attribute=value or XML. +// * appendTemplate: Update type: 0: Replace the whole template. 1: Merge new template with the existing one. +func (group *Group) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.group.update", group.ID, tpl, appendTemplate) + return err +} + +// AddAdmin adds a User to the Group administrators set +// * userID: The user ID. +func (group *Group) AddAdmin(userID uint) error { + _, err := client.Call("one.group.addadmin", group.ID, int(userID)) + return err +} + +// DelAdmin removes a User from the Group administrators set +// * userID: The user ID. +func (group *Group) DelAdmin(userID uint) error { + _, err := client.Call("one.group.deladmin", group.ID, int(userID)) + return err +} + +// Quota sets the group quota limits. +// * tpl: The new quota template contents. Syntax can be the usual attribute=value or XML. +func (group *Group) Quota(tpl string) error { + _, err := client.Call("one.group.quota", group.ID, tpl) + return err +} diff --git a/src/oca/go/src/goca/helper_test.go b/src/oca/go/src/goca/helper_test.go new file mode 100644 index 0000000000..901f46e18a --- /dev/null +++ b/src/oca/go/src/goca/helper_test.go @@ -0,0 +1,45 @@ +package goca + +import ( + "crypto/md5" + "fmt" + "strconv" + "testing" + "time" +) + +// Extracts the ID of a resource +func GetID(t *testing.T, r Resource, s string) (uint, error) { + path := fmt.Sprintf("/%s/ID", s) + + sIDFromXML, ok := r.XPath(path) + if !ok { + t.Error("Could not find ID") + } + + idFromXML, err := strconv.ParseUint(sIDFromXML, 10, strconv.IntSize) + if err != nil { + t.Error(err) + } + + return uint(idFromXML), nil +} + +// Appends a random string to a name +func GenName(name string) string { + t := strconv.FormatInt(time.Now().UnixNano(), 10) + + d := []byte(t) + h := fmt.Sprintf("%x", md5.Sum(d))[:6] + return name + "-" + h +} + +func WaitResource(f func() bool) bool { + for i := 0; i < 20; i++ { + if f() { + return true + } + time.Sleep(2 * time.Second) + } + return false +} diff --git a/src/oca/go/src/goca/host.go b/src/oca/go/src/goca/host.go new file mode 100644 index 0000000000..9b7b7e2f3c --- /dev/null +++ b/src/oca/go/src/goca/host.go @@ -0,0 +1,102 @@ +package goca + +// Host represents an OpenNebula Host +type Host struct { + XMLResource + ID uint + Name string +} + +// HostPool represents an OpenNebula HostPool +type HostPool struct { + XMLResource +} + +// NewHostPool returns a host pool. A connection to OpenNebula is +// performed. +func NewHostPool() (*HostPool, error) { + response, err := client.Call("one.hostpool.info") + if err != nil { + return nil, err + } + + hostpool := &HostPool{XMLResource{body: response.Body()}} + + return hostpool, err +} + +// NewHost finds a host object by ID. No connection to OpenNebula. +func NewHost(id uint) *Host { + return &Host{ID: id} +} + +// NewHostFromName finds a host object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the host. +func NewHostFromName(name string) (*Host, error) { + hostPool, err := NewHostPool() + if err != nil { + return nil, err + } + + id, err := hostPool.GetIDFromName(name, "/HOST_POOL/HOST") + if err != nil { + return nil, err + } + + return NewHost(id), nil +} + +// CreateHost allocates a new host. It returns the new host ID. +// * name: name of the host +// * im: information driver for the host +// * vm: virtualization driver for the host +// * clusterID: The cluster ID. If it is -1, the default one will be used. +func CreateHost(name, im, vm string, clusterID int) (uint, error) { + response, err := client.Call("one.host.allocate", name, im, vm, clusterID) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Delete deletes the given host from the pool +func (host *Host) Delete() error { + _, err := client.Call("one.host.delete", host.ID) + return err +} + +// Status sets the status of the host +// * status: 0: ENABLED, 1: DISABLED, 2: OFFLINE +func (host *Host) Status(status int) error { + _, err := client.Call("one.host.status", host.ID, status) + return err +} + +// Update replaces the host’s template contents. +// * tpl: The new template contents. Syntax can be the usual attribute=value or XML. +// * appendTemplate: Update type: 0: Replace the whole template. 1: Merge new template with the existing one. +func (host *Host) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.host.update", host.ID, tpl, appendTemplate) + return err +} + +// Rename renames a host. +// * newName: The new name. +func (host *Host) Rename(newName string) error { + _, err := client.Call("one.host.rename", host.ID, newName) + return err +} + +// Info retrieves information for the host. +func (host *Host) Info() error { + _, err := client.Call("one.host.info", host.ID) + return err +} + +// Monitoring returns the host monitoring records. +func (host *Host) Monitoring() error { + _, err := client.Call("one.host.monitoring", host.ID) + return err +} diff --git a/src/oca/go/src/goca/image.go b/src/oca/go/src/goca/image.go new file mode 100644 index 0000000000..22a37e7ca3 --- /dev/null +++ b/src/oca/go/src/goca/image.go @@ -0,0 +1,168 @@ +package goca + +import ( + "errors" + "strconv" +) + +// Image represents an OpenNebula Image +type Image struct { + XMLResource + ID uint + Name string +} + +// ImagePool represents an OpenNebula Image pool +type ImagePool struct { + XMLResource +} + +// ImageState is the state of the Image +type ImageState int + +const ( + // ImageInit image is being initialized + ImageInit ImageState = iota + + // ImageReady image is ready to be used + ImageReady + + // ImageUsed image is in use + ImageUsed + + // ImageDisabled image is in disabled + ImageDisabled + + // ImageLocked image is locked + ImageLocked + + // ImageError image is in error state + ImageError + + // ImageClone image is in clone state + ImageClone + + // ImageDelete image is in delete state + ImageDelete + + // ImageLockUsed image is in locked state (non-persistent) + ImageLockUsed + + // ImageLockUsedPers image is in locked state (persistent) + ImageLockUsedPers +) + +// String returns the string version of the ImageState +func (s ImageState) String() string { + return [...]string{ + "INIT", + "READY", + "USED", + "DISABLED", + "LOCKED", + "ERROR", + "CLONE", + "DELETE", + "USED_PERS", + "LOCKED_USED", + "LOCKED_USED_PERS", + }[s] +} + +// CreateImage allocates a new image based on the template string provided. It +// returns the image ID. +func CreateImage(template string, dsid uint) (uint, error) { + response, err := client.Call("one.image.allocate", template, dsid) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// NewImagePool returns a new image pool. It accepts the scope of the query. It +// performs an OpenNebula connectio to fetch the information. +func NewImagePool(args ...int) (*ImagePool, error) { + var who, start, end int + + switch len(args) { + case 0: + who = PoolWhoMine + start = -1 + end = -1 + case 3: + who = args[0] + start = args[1] + end = args[2] + default: + return nil, errors.New("Wrong number of arguments") + } + + response, err := client.Call("one.imagepool.info", who, start, end) + if err != nil { + return nil, err + } + + imagepool := &ImagePool{XMLResource{body: response.Body()}} + + return imagepool, err + +} + +// NewImage finds an image by ID returns a new Image object. At this stage no +// connection to OpenNebula is performed. +func NewImage(id uint) *Image { + return &Image{ID: id} +} + +// NewImageFromName finds an image by name and returns Image object. It connects +// to OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the image. +func NewImageFromName(name string) (*Image, error) { + imagePool, err := NewImagePool() + if err != nil { + return nil, err + } + + id, err := imagePool.GetIDFromName(name, "/IMAGE_POOL/IMAGE") + if err != nil { + return nil, err + } + + return NewImage(id), nil +} + +// Info connects to OpenNebula and fetches the information of the Image +func (image *Image) Info() error { + response, err := client.Call("one.image.info", image.ID) + image.body = response.Body() + return err +} + +// State looks up the state of the image and returns the ImageState +func (image *Image) State() (ImageState, error) { + stateString, ok := image.XPath("/IMAGE/STATE") + if ok != true { + return -1, errors.New("Unable to parse Image State") + } + + state, _ := strconv.Atoi(stateString) + + return ImageState(state), nil +} + +// StateString returns the state in string format +func (image *Image) StateString() (string, error) { + state, err := image.State() + if err != nil { + return "", err + } + return ImageState(state).String(), nil +} + +// Delete will remove the image from OpenNebula, which will remove it from the +// backend. +func (image *Image) Delete() error { + _, err := client.Call("one.image.delete", image.ID) + return err +} diff --git a/src/oca/go/src/goca/template.go b/src/oca/go/src/goca/template.go new file mode 100644 index 0000000000..33a11654aa --- /dev/null +++ b/src/oca/go/src/goca/template.go @@ -0,0 +1,136 @@ +package goca + +import ( + "errors" +) + +// Template represents an OpenNebula Template +type Template struct { + XMLResource + ID uint + Name string +} + +// TemplatePool represents an OpenNebula TemplatePool +type TemplatePool struct { + XMLResource +} + +// NewTemplatePool returns a template pool. A connection to OpenNebula is +// performed. +func NewTemplatePool(args ...int) (*TemplatePool, error) { + var who, start, end int + + switch len(args) { + case 0: + who = PoolWhoMine + start = -1 + end = -1 + case 3: + who = args[0] + start = args[1] + end = args[2] + default: + return nil, errors.New("Wrong number of arguments") + } + + response, err := client.Call("one.templatepool.info", who, start, end) + if err != nil { + return nil, err + } + + templatepool := &TemplatePool{XMLResource{body: response.Body()}} + + return templatepool, err + +} + +// NewTemplate finds a template object by ID. No connection to OpenNebula. +func NewTemplate(id uint) *Template { + return &Template{ID: id} +} + +// NewTemplateFromName finds a template object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the template. +func NewTemplateFromName(name string) (*Template, error) { + templatePool, err := NewTemplatePool() + if err != nil { + return nil, err + } + + id, err := templatePool.GetIDFromName(name, "/VMTEMPLATE_POOL/VMTEMPLATE") + if err != nil { + return nil, err + } + + return NewTemplate(id), nil +} + +// CreateTemplate allocates a new template. It returns the new template ID. +func CreateTemplate(template string) (uint, error) { + response, err := client.Call("one.template.allocate", template) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Info connects to OpenNebula and fetches the information of the Template +func (template *Template) Info() error { + response, err := client.Call("one.template.info", template.ID) + template.body = response.Body() + return err +} + +// Update will modify the template. If appendTemplate is 0, it will +// replace the whole template. If its 1, it will merge. +func (template *Template) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.template.update", template.ID, tpl, appendTemplate) + return err +} + +// Chown changes the owner/group of a template. If uid or gid is -1 it will not +// change +func (template *Template) Chown(uid, gid int) error { + _, err := client.Call("one.template.chown", template.ID, uid, gid) + return err +} + +// Chmod changes the permissions of a template. If any perm is -1 it will not +// change +func (template *Template) Chmod(uu, um, ua, gu, gm, ga, ou, om, oa int) error { + _, err := client.Call("one.template.chmod", template.ID, uu, um, ua, gu, gm, ga, ou, om, oa) + return err +} + +// Rename changes the name of template +func (template *Template) Rename(newName string) error { + _, err := client.Call("one.template.rename", template.ID, newName) + return err +} + +// Delete will remove the template from OpenNebula. +func (template *Template) Delete() error { + _, err := client.Call("one.template.delete", template.ID) + return err +} + +// Instantiate will instantiate the template +func (template *Template) Instantiate(name string, pending bool, extra string) (uint, error) { + response, err := client.Call("one.template.instantiate", template.ID, name, pending, extra) + + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Clone an existing template. If recursive is true it will clone the template +// plus any image defined in DISK. The new IMAGE_ID is set into each DISK. +func (template *Template) Clone(name string, recursive bool) error { + _, err := client.Call("one.template.clone", template.ID, name, recursive) + return err +} diff --git a/src/oca/go/src/goca/template_builder.go b/src/oca/go/src/goca/template_builder.go new file mode 100644 index 0000000000..1fb878fa66 --- /dev/null +++ b/src/oca/go/src/goca/template_builder.go @@ -0,0 +1,117 @@ +package goca + +import ( + "errors" + "fmt" + "strings" +) + +// TemplateBuilder represents an OpenNebula syntax template +type TemplateBuilder struct { + elements []TemplateBuilderElement +} + +// TemplateBuilderElement is an interface that must implement the String +// function +type TemplateBuilderElement interface { + String() string +} + +// TemplateBuilderPair is a key / value pair +type TemplateBuilderPair struct { + key string + value string +} + +// TemplateBuilderVector contains an array of keyvalue pairs +type TemplateBuilderVector struct { + key string + pairs []TemplateBuilderPair +} + +// NewTemplateBuilder returns a new TemplateBuilder object +func NewTemplateBuilder() *TemplateBuilder { + return &TemplateBuilder{} +} + +// NewVector creates a new vector in the template +func (t *TemplateBuilder) NewVector(key string) *TemplateBuilderVector { + vector := &TemplateBuilderVector{key: key} + t.elements = append(t.elements, vector) + return vector +} + +// String prints the TemplateBuilder in OpenNebula syntax +func (t *TemplateBuilder) String() string { + s := "" + endToken := "\n" + + for i, element := range t.elements { + if i == len(t.elements)-1 { + endToken = "" + } + s += element.String() + endToken + } + + return s +} + +// String prints a TemplateBuilderPair in OpenNebula syntax +func (t *TemplateBuilderPair) String() string { + return fmt.Sprintf("%s=\"%s\"", t.key, t.value) +} + +func (t *TemplateBuilderVector) String() string { + s := fmt.Sprintf("%s=[\n", strings.ToUpper(t.key)) + + endToken := ",\n" + for i, pair := range t.pairs { + if i == len(t.pairs)-1 { + endToken = "" + } + + s += fmt.Sprintf(" %s%s", pair.String(), endToken) + + } + s += " ]" + + return s +} + +// AddValue adds a new pair to a TemplateBuilder objects +func (t *TemplateBuilder) AddValue(key string, v interface{}) error { + var val string + + switch v := v.(type) { + default: + return errors.New("Unexpected type") + case int, uint: + val = fmt.Sprintf("%d", v) + case string: + val = v + } + + pair := &TemplateBuilderPair{strings.ToUpper(key), val} + t.elements = append(t.elements, pair) + + return nil +} + +// AddValue adds a new pair to a TemplateBuilderVector +func (t *TemplateBuilderVector) AddValue(key string, v interface{}) error { + var val string + + switch v := v.(type) { + default: + return errors.New("Unexpected type") + case int, uint: + val = fmt.Sprintf("%d", v) + case string: + val = v + } + + pair := TemplateBuilderPair{strings.ToUpper(key), val} + t.pairs = append(t.pairs, pair) + + return nil +} diff --git a/src/oca/go/src/goca/template_builder_test.go b/src/oca/go/src/goca/template_builder_test.go new file mode 100644 index 0000000000..9652402d08 --- /dev/null +++ b/src/oca/go/src/goca/template_builder_test.go @@ -0,0 +1,36 @@ +package goca + +import ( + "fmt" +) + +func Example() { + template := NewTemplateBuilder() + + // Main + template.AddValue("cpu", 1) + template.AddValue("memory", "64") + template.AddValue("vcpu", "2") + + // Disk + vector := template.NewVector("disk") + vector.AddValue("image_id", "119") + vector.AddValue("dev_prefix", "vd") + + // NIC + vector = template.NewVector("nic") + vector.AddValue("network_id", "3") + vector.AddValue("model", "virtio") + + fmt.Println(template) + // Output: + // CPU="1" + // MEMORY="64" + // VCPU="2" + // DISK=[ + // IMAGE_ID="119", + // DEV_PREFIX="vd" ] + // NIC=[ + // NETWORK_ID="3", + // MODEL="virtio" ] +} diff --git a/src/oca/go/src/goca/template_test.go b/src/oca/go/src/goca/template_test.go new file mode 100644 index 0000000000..a52ad94038 --- /dev/null +++ b/src/oca/go/src/goca/template_test.go @@ -0,0 +1,132 @@ +package goca + +import ( + "testing" +) + +// Helper to create a template +func createTemplate(t *testing.T) *Template { + templateName := GenName("template") + + // Create template + tpl := NewTemplateBuilder() + + tpl.AddValue("name", templateName) + tpl.AddValue("cpu", 1) + tpl.AddValue("memory", "64") + + id, err := CreateTemplate(tpl.String()) + if err != nil { + t.Error(err) + } + + // Get template by ID + template := NewTemplate(id) + + err = template.Info() + if err != nil { + t.Error(err) + } + + return template +} + +func TestTemplateCreateAndDelete(t *testing.T) { + template := createTemplate(t) + + idParse, err := GetID(t, template, "VMTEMPLATE") + if err != nil { + t.Error(err) + } + + if idParse != template.ID { + t.Errorf("Template ID does not match") + } + + // Get template by Name + templateName, ok := template.XPath("/VMTEMPLATE/NAME") + if !ok { + t.Errorf("Could not get name") + } + + template, err = NewTemplateFromName(templateName) + if err != nil { + t.Error(err) + } + + err = template.Info() + if err != nil { + t.Error(err) + } + + idParse, err = GetID(t, template, "VMTEMPLATE") + + if idParse != template.ID { + t.Errorf("Template ID does not match") + } + + // Delete template + err = template.Delete() + if err != nil { + t.Error(err) + } +} + +func TestTemplateInstantiate(t *testing.T) { + templateName := GenName("template") + + // Create template + tpl := NewTemplateBuilder() + + tpl.AddValue("name", templateName) + tpl.AddValue("cpu", 1) + tpl.AddValue("memory", "64") + + id, err := CreateTemplate(tpl.String()) + if err != nil { + t.Error(err) + } + + // Get template by ID + template := NewTemplate(id) + + // Instantiate(name string, pending bool, extra string) (uint, error) + vmid, err := template.Instantiate("", false, "") + if err != nil { + t.Error(err) + } + + vm := NewVM(vmid) + vm.Terminate() + + // Delete template + err = template.Delete() + if err != nil { + t.Error(err) + } +} + +func TestTemplateUpdate(t *testing.T) { + template := createTemplate(t) + + tpl := NewTemplateBuilder() + tpl.AddValue("A", "B") + + // Update + template.Update(tpl.String(), 1) + + err := template.Info() + if err != nil { + t.Error(err) + } + + if val, ok := template.XPath("/VMTEMPLATE/TEMPLATE/A"); !ok || val != "B" { + t.Errorf("Expecting A=B") + } + + // Delete template + err = template.Delete() + if err != nil { + t.Error(err) + } +} diff --git a/src/oca/go/src/goca/user.go b/src/oca/go/src/goca/user.go new file mode 100644 index 0000000000..1104065192 --- /dev/null +++ b/src/oca/go/src/goca/user.go @@ -0,0 +1,134 @@ +package goca + +// User represents an OpenNebula User +type User struct { + XMLResource + ID uint + Name string +} + +// UserPool represents an OpenNebula UserPool +type UserPool struct { + XMLResource +} + +// NewUserPool returns a user pool. A connection to OpenNebula is +// performed. +func NewUserPool() (*UserPool, error) { + response, err := client.Call("one.userpool.info") + if err != nil { + return nil, err + } + + userpool := &UserPool{XMLResource{body: response.Body()}} + + return userpool, err +} + +// NewUser finds a user object by ID. No connection to OpenNebula. +func NewUser(id uint) *User { + return &User{ID: id} +} + +// NewUserFromName finds a user object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the user. +func NewUserFromName(name string) (*User, error) { + userPool, err := NewUserPool() + if err != nil { + return nil, err + } + + id, err := userPool.GetIDFromName(name, "/USER_POOL/USER") + if err != nil { + return nil, err + } + + return NewUser(id), nil +} + +// CreateUser allocates a new user. It returns the new user ID. +// * name: name of the user +// * password: password of the user +// * authDriver: auth driver +// * groupIDs: array of groupIDs to add to the user +func CreateUser(name, password, authDriver string, groupIDs []uint) (uint, error) { + response, err := client.Call("one.user.allocate", name, password, authDriver, groupIDs) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Delete deletes the given user from the pool. +func (user *User) Delete() error { + _, err := client.Call("one.user.delete", user.ID) + return err +} + +// Passwd changes the password for the given user. +// * password: The new password +func (user *User) Passwd(password string) error { + _, err := client.Call("one.user.passwd", user.ID, password) + return err +} + +// Login generates or sets a login token. +// * token: The token +// * timeSeconds: Valid period in seconds; 0 reset the token and -1 for a non-expiring token. +// * effectiveGID: Effective GID to use with this token. To use the current GID and user groups set it to -1 +func (user *User) Login(token string, timeSeconds int, effectiveGID uint) error { + _, err := client.Call("one.user.login", user.ID, token, timeSeconds, int(effectiveGID)) + return err +} + +// Update replaces the user template contents. +// * tpl: The new template contents. Syntax can be the usual attribute=value or XML. +// * appendTemplate: Update type: 0: Replace the whole template. 1: Merge new template with the existing one. +func (user *User) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.user.update", user.ID, tpl, appendTemplate) + return err +} + +// Chauth changes the authentication driver and the password for the given user. +// * authDriver: The new authentication driver. +// * password: The new password. If it is an empty string +func (user *User) Chauth(authDriver, password string) error { + _, err := client.Call("one.user.chauth", user.ID, authDriver, password) + return err +} + +// Quota sets the user quota limits. +// * tpl: The new quota template contents. Syntax can be the usual attribute=value or XML. +func (user *User) Quota(tpl string) error { + _, err := client.Call("one.user.quota", user.ID, tpl) + return err +} + +// Chgrp changes the group of the given user. +// * groupID: The Group ID of the new group. +func (user *User) Chgrp(groupID uint) error { + _, err := client.Call("one.user.chgrp", user.ID, int(groupID)) + return err +} + +// AddGroup adds the User to a secondary group. +// * groupID: The Group ID of the new group. +func (user *User) AddGroup(groupID uint) error { + _, err := client.Call("one.user.addgroup", user.ID, int(groupID)) + return err +} + +// DelGroup removes the User from a secondary group +// * groupID: The Group ID. +func (user *User) DelGroup(groupID uint) error { + _, err := client.Call("one.user.delgroup", user.ID, int(groupID)) + return err +} + +// Info retrieves information for the user. +func (user *User) Info() error { + _, err := client.Call("one.user.info", user.ID) + return err +} diff --git a/src/oca/go/src/goca/vdc.go b/src/oca/go/src/goca/vdc.go new file mode 100644 index 0000000000..c3a4494d08 --- /dev/null +++ b/src/oca/go/src/goca/vdc.go @@ -0,0 +1,167 @@ +package goca + +// Vdc represents an OpenNebula Vdc +type Vdc struct { + XMLResource + ID uint + Name string +} + +// VdcPool represents an OpenNebula VdcPool +type VdcPool struct { + XMLResource +} + +// NewVdcPool returns a vdc pool. A connection to OpenNebula is +// performed. +func NewVdcPool() (*VdcPool, error) { + response, err := client.Call("one.vdcpool.info") + if err != nil { + return nil, err + } + + vdcpool := &VdcPool{XMLResource{body: response.Body()}} + + return vdcpool, err +} + +// NewVdc finds a vdc object by ID. No connection to OpenNebula. +func NewVdc(id uint) *Vdc { + return &Vdc{ID: id} +} + +// NewVdcFromName finds a vdc object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the vdc. +func NewVdcFromName(name string) (*Vdc, error) { + vdcPool, err := NewVdcPool() + if err != nil { + return nil, err + } + + id, err := vdcPool.GetIDFromName(name, "/VDC_POOL/VDC") + if err != nil { + return nil, err + } + + return NewVdc(id), nil +} + +// CreateVdc allocates a new vdc. It returns the new vdc ID. +// * tpl: A string containing the template of the VDC. Syntax can be the usual +// attribute=value or XML. +// * clusterID: The cluster ID. If it is -1, this virtual network won’t be added +// to any cluster +func CreateVdc(tpl string, clusterID int) (uint, error) { + response, err := client.Call("one.vdc.allocate", tpl, clusterID) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Delete deletes the given VDC from the pool. +func (vdc *Vdc) Delete() error { + _, err := client.Call("one.vdc.delete", vdc.ID) + return err +} + +// Update replaces the VDC template contents. +// * tpl: The new template contents. Syntax can be the usual attribute=value or XML. +// * appendTemplate: Update type: 0: Replace the whole template. 1: Merge new template with the existing one. +func (vdc *Vdc) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.vdc.update", vdc.ID, tpl, appendTemplate) + return err +} + +// Rename renames a VDC. +// * newName: The new name. +func (vdc *Vdc) Rename(newName string) error { + _, err := client.Call("one.vdc.rename", vdc.ID, newName) + return err +} + +// Info retrieves information for the VDC. +func (vdc *Vdc) Info() error { + _, err := client.Call("one.vdc.info", vdc.ID) + return err +} + +// AddGroup adds a group to the VDC +// * groupID: The group ID. +func (vdc *Vdc) AddGroup(groupID uint) error { + _, err := client.Call("one.vdc.addgroup", vdc.ID, int(groupID)) + return err +} + +// DelGroup deletes a group from the VDC +// * groupID: The group ID. +func (vdc *Vdc) DelGroup(groupID uint) error { + _, err := client.Call("one.vdc.delgroup", vdc.ID, int(groupID)) + return err +} + +// AddCluster adds a cluster to the VDC +// * zoneID: The Zone ID. +// * clusterID: The Cluster ID. +func (vdc *Vdc) AddCluster(zoneID, clusterID uint) error { + _, err := client.Call("one.vdc.addcluster", vdc.ID, int(zoneID), int(clusterID)) + return err +} + +// DelCluster deletes a cluster from the VDC +// * zoneID: The Zone ID. +// * clusterID: The Cluster ID. +func (vdc *Vdc) DelCluster(zoneID, clusterID uint) error { + _, err := client.Call("one.vdc.delcluster", vdc.ID, int(zoneID), int(clusterID)) + return err +} + +// AddHost adds a host to the VDC +// * zoneID: The Zone ID. +// * hostID: The Host ID. +func (vdc *Vdc) AddHost(zoneID, hostID uint) error { + _, err := client.Call("one.vdc.addhost", vdc.ID, int(zoneID), int(hostID)) + return err +} + +// DelHost deletes a host from the VDC +// * zoneID: The Zone ID. +// * hostID: The Host ID. +func (vdc *Vdc) DelHost(zoneID, hostID uint) error { + _, err := client.Call("one.vdc.delhost", vdc.ID, int(zoneID), int(hostID)) + return err +} + +// AddDatastore adds a datastore to the VDC +// * zoneID: The Zone ID. +// * dsID: The Datastore ID. +func (vdc *Vdc) AddDatastore(zoneID, dsID uint) error { + _, err := client.Call("one.vdc.adddatastore", vdc.ID, int(zoneID), int(dsID)) + return err +} + +// DelDatastore deletes a datastore from the VDC +// * zoneID: The Zone ID. +// * dsID: The Datastore ID. +func (vdc *Vdc) DelDatastore(zoneID, dsID uint) error { + _, err := client.Call("one.vdc.deldatastore", vdc.ID, int(zoneID), int(dsID)) + return err +} + +// AddVnet adds a vnet to the VDC +// * zoneID: The Zone ID. +// * vnetID: The Vnet ID. +func (vdc *Vdc) AddVnet(zoneID, vnetID uint) error { + _, err := client.Call("one.vdc.addvnet", vdc.ID, int(zoneID), int(vnetID)) + return err +} + +// DelVnet deletes a vnet from the VDC +// * zoneID: The Zone ID. +// * vnetID: The Vnet ID. +func (vdc *Vdc) DelVnet(zoneID, vnetID uint) error { + _, err := client.Call("one.vdc.delvnet", vdc.ID, int(zoneID), int(vnetID)) + return err +} diff --git a/src/oca/go/src/goca/version.go b/src/oca/go/src/goca/version.go new file mode 100644 index 0000000000..ec7fc05cee --- /dev/null +++ b/src/oca/go/src/goca/version.go @@ -0,0 +1,13 @@ +package goca + +// GitCommit is the git commit that was compiled. This will be filled in by the +// compiler. +var GitCommit string + +// Version is the main version number that is being run at the moment. +const Version = "0.1.0" + +// VersionPrerelease is the pre-release marker for the version. If this is "" +// (empty string) then it means that it is a final release. Otherwise, this is a +// pre-release such as "dev" (in development) +var VersionPrerelease = "" diff --git a/src/oca/go/src/goca/virtualnetwork.go b/src/oca/go/src/goca/virtualnetwork.go new file mode 100644 index 0000000000..7b0f603c97 --- /dev/null +++ b/src/oca/go/src/goca/virtualnetwork.go @@ -0,0 +1,180 @@ +package goca + +import "errors" + +// VirtualNetwork represents an OpenNebula VirtualNetwork +type VirtualNetwork struct { + XMLResource + ID uint + Name string +} + +// VirtualNetworkPool represents an OpenNebula VirtualNetworkPool +type VirtualNetworkPool struct { + XMLResource +} + +// NewVirtualNetworkPool returns a virtualnetwork pool. A connection to OpenNebula is +// performed. +func NewVirtualNetworkPool(args ...int) (*VirtualNetworkPool, error) { + var who, start, end int + + switch len(args) { + case 0: + who = PoolWhoMine + start = -1 + end = -1 + case 1: + who = args[0] + start = -1 + end = -1 + case 3: + who = args[0] + start = args[1] + end = args[2] + default: + return nil, errors.New("Wrong number of arguments") + } + + response, err := client.Call("one.vnpool.info", who, start, end) + if err != nil { + return nil, err + } + + vnpool := &VirtualNetworkPool{XMLResource{body: response.Body()}} + + return vnpool, err +} + +// NewVirtualNetwork finds a virtualnetwork object by ID. No connection to OpenNebula. +func NewVirtualNetwork(id uint) *VirtualNetwork { + return &VirtualNetwork{ID: id} +} + +// NewVirtualNetworkFromName finds a virtualnetwork object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the virtualnetwork. +func NewVirtualNetworkFromName(name string) (*VirtualNetwork, error) { + virtualnetworkPool, err := NewVirtualNetworkPool() + if err != nil { + return nil, err + } + + id, err := virtualnetworkPool.GetIDFromName(name, "/VNET_POOL/VNET") + if err != nil { + return nil, err + } + + return NewVirtualNetwork(id), nil +} + +// CreateVirtualnetwork allocates a new virtualnetwork. It returns the new virtualnetwork ID. +// * tpl: template of the virtualnetwork +// * clusterID: The cluster ID. If it is -1, the default one will be used. +func CreateVirtualnetwork(tpl string, clusterID int) (uint, error) { + response, err := client.Call("one.vn.allocate", tpl, clusterID) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Delete deletes the given virtual network from the pool. +func (vn *VirtualNetwork) Delete() error { + _, err := client.Call("one.vn.delete", vn.ID) + return err +} + +// AddAr adds address ranges to a virtual network. +// * tpl: template of the address ranges to add. Syntax can be the usual attribute=value or XML +func (vn *VirtualNetwork) AddAr(tpl string) error { + _, err := client.Call("one.vn.add_ar", vn.ID, tpl) + return err +} + +// RmAr removes an address range from a virtual network. +// * arID: ID of the address range to remove. +func (vn *VirtualNetwork) RmAr(arID int) error { + _, err := client.Call("one.vn.rm_ar", vn.ID, arID) + return err +} + +// UpdateAr updates the attributes of an address range. +// * tpl: template of the address ranges to update. Syntax can be the usual attribute=value or XML +func (vn *VirtualNetwork) UpdateAr(tpl string) error { + _, err := client.Call("one.vn.update_ar", vn.ID, tpl) + return err +} + +// Reserve reserve network addresses. +// * tpl: Template +func (vn *VirtualNetwork) Reserve(tpl string) error { + _, err := client.Call("one.vn.reserve", vn.ID, tpl) + return err +} + +// FreeAr frees a reserved address range from a virtual network. +// * arID: ID of the address range to free. +func (vn *VirtualNetwork) FreeAr(arID int) error { + _, err := client.Call("one.vn.free_ar", vn.ID, arID) + return err +} + +// Hold holds a virtual network Lease as used. +// * tpl: template of the lease to hold +func (vn *VirtualNetwork) Hold(tpl string) error { + _, err := client.Call("one.vn.hold", vn.ID, tpl) + return err +} + +// Release releases a virtual network Lease on hold. +// * tpl: template of the lease to release +func (vn *VirtualNetwork) Release(tpl string) error { + _, err := client.Call("one.vn.release", vn.ID, tpl) + return err +} + +// Update replaces the virtual network template contents. +// * tpl: The new template contents. Syntax can be the usual attribute=value or XML. +// * appendTemplate: Update type: 0: Replace the whole template. 1: Merge new template with the existing one. +func (vn *VirtualNetwork) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.vn.update", vn.ID, tpl, appendTemplate) + return err +} + +// Chmod changes the permission bits of a virtual network. +// * uu: USER USE bit. If set to -1, it will not change. +// * um: USER MANAGE bit. If set to -1, it will not change. +// * ua: USER ADMIN bit. If set to -1, it will not change. +// * gu: GROUP USE bit. If set to -1, it will not change. +// * gm: GROUP MANAGE bit. If set to -1, it will not change. +// * ga: GROUP ADMIN bit. If set to -1, it will not change. +// * ou: OTHER USE bit. If set to -1, it will not change. +// * om: OTHER MANAGE bit. If set to -1, it will not change. +// * oa: OTHER ADMIN bit. If set to -1, it will not change. +func (vn *VirtualNetwork) Chmod(uu, um, ua, gu, gm, ga, ou, om, oa int) error { + _, err := client.Call("one.vn.chmod", vn.ID, uu, um, ua, gu, gm, ga, ou, om, oa) + return err +} + +// Chown changes the ownership of a virtual network. +// * userID: The User ID of the new owner. If set to -1, it will not change. +// * groupID: The Group ID of the new group. If set to -1, it will not change. +func (vn *VirtualNetwork) Chown(userID, groupID uint) error { + _, err := client.Call("one.vn.chown", vn.ID, int(userID), int(groupID)) + return err +} + +// Rename renames a virtual network. +// * newName: The new name. +func (vn *VirtualNetwork) Rename(newName string) error { + _, err := client.Call("one.vn.rename", vn.ID, newName) + return err +} + +// Info retrieves information for the virtual network. +func (vn *VirtualNetwork) Info() error { + _, err := client.Call("one.vn.info", vn.ID) + return err +} diff --git a/src/oca/go/src/goca/vm.go b/src/oca/go/src/goca/vm.go new file mode 100644 index 0000000000..174d0b369b --- /dev/null +++ b/src/oca/go/src/goca/vm.go @@ -0,0 +1,840 @@ +package goca + +import ( + "errors" + "strconv" +) + +// VM represents an OpenNebula Virtual Machine +type VM struct { + XMLResource + ID uint + Name string +} + +// VMPool represents an OpenNebula Virtual Machine pool +type VMPool struct { + XMLResource +} + +// VMState is the state of the Virtual Machine +type VMState int + +const ( + // Init state + Init VMState = 0 + + // Pending state + Pending VMState = 1 + + // Hold state + Hold VMState = 2 + + // Active state + Active VMState = 3 + + // Stopped state + Stopped VMState = 4 + + // Suspended state + Suspended VMState = 5 + + // Done state + Done VMState = 6 + + // Deprecated + // Failed VMState = 7 + + // Poweroff state + Poweroff VMState = 8 + + // Undeployed state + Undeployed VMState = 9 + + // Cloning state + Cloning VMState = 10 + + // CloningFailure state + CloningFailure VMState = 11 +) + +func (s VMState) String() string { + switch s { + case Init: + return "INIT" + case Pending: + return "PENDING" + case Hold: + return "HOLD" + case Active: + return "ACTIVE" + case Stopped: + return "STOPPED" + case Suspended: + return "SUSPENDED" + case Done: + return "DONE" + case Poweroff: + return "POWEROFF" + case Undeployed: + return "UNDEPLOYED" + case Cloning: + return "CLONING" + case CloningFailure: + return "CLONINGFAILURE" + default: + return "" + } +} + +// LCMState is the life-cycle manager state of the virtual machine. It is used +// only when the VM's state is active, otherwise it's LcmInit +type LCMState int + +const ( + // LcmInit lcm state + LcmInit LCMState = 0 + + // Prolog lcm state + Prolog LCMState = 1 + + // Boot lcm state + Boot LCMState = 2 + + // Running lcm state + Running LCMState = 3 + + // Migrate lcm state + Migrate LCMState = 4 + + // SaveStop lcm state + SaveStop LCMState = 5 + + // SaveSuspend lcm state + SaveSuspend LCMState = 6 + + // SaveMigrate lcm state + SaveMigrate LCMState = 7 + + // PrologMigrate lcm state + PrologMigrate LCMState = 8 + + // PrologResume lcm state + PrologResume LCMState = 9 + + // EpilogStop lcm state + EpilogStop LCMState = 10 + + // Epilog lcm state + Epilog LCMState = 11 + + // Shutdown lcm state + Shutdown LCMState = 12 + + // Deprecated + // Cancel LCMState = 13 + // Failure LCMState = 14 + + // CleanupResubmit lcm state + CleanupResubmit LCMState = 15 + + // Unknown lcm state + Unknown LCMState = 16 + + // Hotplug lcm state + Hotplug LCMState = 17 + + // ShutdownPoweroff lcm state + ShutdownPoweroff LCMState = 18 + + // BootUnknown lcm state + BootUnknown LCMState = 19 + + // BootPoweroff lcm state + BootPoweroff LCMState = 20 + + // BootSuspended lcm state + BootSuspended LCMState = 21 + + // BootStopped lcm state + BootStopped LCMState = 22 + + // CleanupDelete lcm state + CleanupDelete LCMState = 23 + + // HotplugSnapshot lcm state + HotplugSnapshot LCMState = 24 + + // HotplugNic lcm state + HotplugNic LCMState = 25 + + // HotplugSaveas lcm state + HotplugSaveas LCMState = 26 + + // HotplugSaveasPoweroff lcm state + HotplugSaveasPoweroff LCMState = 27 + + // HotplugSaveasSuspended lcm state + HotplugSaveasSuspended LCMState = 28 + + // ShutdownUndeploy lcm state + ShutdownUndeploy LCMState = 29 + + // EpilogUndeploy lcm state + EpilogUndeploy LCMState = 30 + + // PrologUndeploy lcm state + PrologUndeploy LCMState = 31 + + // BootUndeploy lcm state + BootUndeploy LCMState = 32 + + // HotplugPrologPoweroff lcm state + HotplugPrologPoweroff LCMState = 33 + + // HotplugEpilogPoweroff lcm state + HotplugEpilogPoweroff LCMState = 34 + + // BootMigrate lcm state + BootMigrate LCMState = 35 + + // BootFailure lcm state + BootFailure LCMState = 36 + + // BootMigrateFailure lcm state + BootMigrateFailure LCMState = 37 + + // PrologMigrateFailure lcm state + PrologMigrateFailure LCMState = 38 + + // PrologFailure lcm state + PrologFailure LCMState = 39 + + // EpilogFailure lcm state + EpilogFailure LCMState = 40 + + // EpilogStopFailure lcm state + EpilogStopFailure LCMState = 41 + + // EpilogUndeployFailure lcm state + EpilogUndeployFailure LCMState = 42 + + // PrologMigratePoweroff lcm state + PrologMigratePoweroff LCMState = 43 + + // PrologMigratePoweroffFailure lcm state + PrologMigratePoweroffFailure LCMState = 44 + + // PrologMigrateSuspend lcm state + PrologMigrateSuspend LCMState = 45 + + // PrologMigrateSuspendFailure lcm state + PrologMigrateSuspendFailure LCMState = 46 + + // BootUndeployFailure lcm state + BootUndeployFailure LCMState = 47 + + // BootStoppedFailure lcm state + BootStoppedFailure LCMState = 48 + + // PrologResumeFailure lcm state + PrologResumeFailure LCMState = 49 + + // PrologUndeployFailure lcm state + PrologUndeployFailure LCMState = 50 + + // DiskSnapshotPoweroff lcm state + DiskSnapshotPoweroff LCMState = 51 + + // DiskSnapshotRevertPoweroff lcm state + DiskSnapshotRevertPoweroff LCMState = 52 + + // DiskSnapshotDeletePoweroff lcm state + DiskSnapshotDeletePoweroff LCMState = 53 + + // DiskSnapshotSuspended lcm state + DiskSnapshotSuspended LCMState = 54 + + // DiskSnapshotRevertSuspended lcm state + DiskSnapshotRevertSuspended LCMState = 55 + + // DiskSnapshotDeleteSuspended lcm state + DiskSnapshotDeleteSuspended LCMState = 56 + + // DiskSnapshot lcm state + DiskSnapshot LCMState = 57 + + // Deprecated + // DiskSnapshotRevert LCMState = 58 + + // DiskSnapshotDelete lcm state + DiskSnapshotDelete LCMState = 59 + + // PrologMigrateUnknown lcm state + PrologMigrateUnknown LCMState = 60 + + // PrologMigrateUnknownFailure lcm state + PrologMigrateUnknownFailure LCMState = 61 + + // DiskResize lcm state + DiskResize LCMState = 62 + + // DiskResizePoweroff lcm state + DiskResizePoweroff LCMState = 63 + + // DiskResizeUndeployed lcm state + DiskResizeUndeployed LCMState = 64 +) + +func (l LCMState) String() string { + switch l { + case LcmInit: + return "LCM_INIT" + case Prolog: + return "PROLOG" + case Boot: + return "BOOT" + case Running: + return "RUNNING" + case Migrate: + return "MIGRATE" + case SaveStop: + return "SAVE_STOP" + case SaveSuspend: + return "SAVESuspend" + case SaveMigrate: + return "SAVE_MIGRATE" + case PrologMigrate: + return "PROLOG_MIGRATE" + case PrologResume: + return "PROLOG_RESUME" + case EpilogStop: + return "EPILOG_STOP" + case Epilog: + return "EPILOG" + case Shutdown: + return "SHUTDOWN" + case CleanupResubmit: + return "CLEANUP_RESUBMIT" + case Unknown: + return "UNKNOWN" + case Hotplug: + return "HOTPLUG" + case ShutdownPoweroff: + return "SHUTDOWN_POWEROFF" + case BootUnknown: + return "BOOT_UNKNOWN" + case BootPoweroff: + return "BOOT_POWEROFF" + case BootSuspended: + return "BOOTSuspendED" + case BootStopped: + return "BOOT_STOPPED" + case CleanupDelete: + return "CLEANUP_DELETE" + case HotplugSnapshot: + return "HOTPLUG_SNAPSHOT" + case HotplugNic: + return "HOTPLUG_NIC" + case HotplugSaveas: + return "HOTPLUG_SAVEAS" + case HotplugSaveasPoweroff: + return "HOTPLUG_SAVEAS_POWEROFF" + case HotplugSaveasSuspended: + return "HOTPLUG_SAVEASSuspendED" + case ShutdownUndeploy: + return "SHUTDOWN_UNDEPLOY" + case EpilogUndeploy: + return "EPILOG_UNDEPLOY" + case PrologUndeploy: + return "PROLOG_UNDEPLOY" + case BootUndeploy: + return "BOOT_UNDEPLOY" + case HotplugPrologPoweroff: + return "HOTPLUG_PROLOG_POWEROFF" + case HotplugEpilogPoweroff: + return "HOTPLUG_EPILOG_POWEROFF" + case BootMigrate: + return "BOOT_MIGRATE" + case BootFailure: + return "BOOT_FAILURE" + case BootMigrateFailure: + return "BOOT_MIGRATE_FAILURE" + case PrologMigrateFailure: + return "PROLOG_MIGRATE_FAILURE" + case PrologFailure: + return "PROLOG_FAILURE" + case EpilogFailure: + return "EPILOG_FAILURE" + case EpilogStopFailure: + return "EPILOG_STOP_FAILURE" + case EpilogUndeployFailure: + return "EPILOG_UNDEPLOY_FAILURE" + case PrologMigratePoweroff: + return "PROLOG_MIGRATE_POWEROFF" + case PrologMigratePoweroffFailure: + return "PROLOG_MIGRATE_POWEROFF_FAILURE" + case PrologMigrateSuspend: + return "PROLOG_MIGRATESuspend" + case PrologMigrateSuspendFailure: + return "PROLOG_MIGRATESuspend_FAILURE" + case BootUndeployFailure: + return "BOOT_UNDEPLOY_FAILURE" + case BootStoppedFailure: + return "BOOT_STOPPED_FAILURE" + case PrologResumeFailure: + return "PROLOG_RESUME_FAILURE" + case PrologUndeployFailure: + return "PROLOG_UNDEPLOY_FAILURE" + case DiskSnapshotPoweroff: + return "DISK_SNAPSHOT_POWEROFF" + case DiskSnapshotRevertPoweroff: + return "DISK_SNAPSHOT_REVERT_POWEROFF" + case DiskSnapshotDeletePoweroff: + return "DISK_SNAPSHOT_DELETE_POWEROFF" + case DiskSnapshotSuspended: + return "DISK_SNAPSHOTSuspendED" + case DiskSnapshotRevertSuspended: + return "DISK_SNAPSHOT_REVERTSuspendED" + case DiskSnapshotDeleteSuspended: + return "DISK_SNAPSHOT_DELETESuspendED" + case DiskSnapshot: + return "DISK_SNAPSHOT" + case DiskSnapshotDelete: + return "DISK_SNAPSHOT_DELETE" + case PrologMigrateUnknown: + return "PROLOG_MIGRATE_UNKNOWN" + case PrologMigrateUnknownFailure: + return "PROLOG_MIGRATE_UNKNOWN_FAILURE" + case DiskResize: + return "DISK_RESIZE" + case DiskResizePoweroff: + return "DISK_RESIZE_POWEROFF" + case DiskResizeUndeployed: + return "DISK_RESIZE_UNDEPLOYED" + default: + return "" + } +} + +// NewVMPool returns a new image pool. It accepts the scope of the query. +func NewVMPool(args ...int) (*VMPool, error) { + var who, start, end, state int + + switch len(args) { + case 0: + who = PoolWhoMine + start = -1 + end = -1 + state = -1 + case 1: + who = args[0] + start = -1 + end = -1 + state = -1 + case 3: + who = args[0] + start = args[1] + end = args[2] + state = -1 + case 4: + who = args[0] + start = args[1] + end = args[2] + state = args[3] + default: + return nil, errors.New("Wrong number of arguments") + } + + response, err := client.Call("one.vmpool.info", who, start, end, state) + if err != nil { + return nil, err + } + + vmpool := &VMPool{XMLResource{body: response.Body()}} + + return vmpool, err + +} + +// Monitoring returns all the virtual machine monitorin records +// filter flag: +// -4: Resources belonging to the user's primary group +// -3: Resources belonging to the user +// -2: All resources +// -1: Resources belonging to the user and any of his groups +// >= 0: UID User's Resources +func (vmpool *VMPool) Monitoring(filter int) error { + _, err := client.Call("one.vmpool.monitoring", filter) + return err +} + +// Accounting returns the virtual machine history records +// filter flag: +// -4: Resources belonging to the user's primary group +// -3: Resources belonging to the user +// -2: All resources +// -1: Resources belonging to the user and any of his groups +// >= 0: UID User's Resources +// if startTime and/or endTime are -1 it means no limit +func (vmpool *VMPool) Accounting(filter, startTime, endTime int) error { + _, err := client.Call("one.vmpool.accounting", filter) + return err +} + +// Showback returns the virtual machine showback records +// filter flag +// <= -3: Connected user's resources +// -2: All resources +// -1: Connected user's and his group's resources +// >= 0: UID User's Resources +// firstMonth: January is 1. Can be -1, in which case the time interval won't have +// a left boundary. +// firstYear: Can be -1, in which case the time interval won't have a left +// boundary. +// lastMonth: January is 1. Can be -1, in which case the time interval won't have +// a right boundary. +// lastYear: Can be -1, in which case the time interval won't have a right +// boundary. +func (vmpool *VMPool) Showback(filter, firstMonth, firstYear, lastMonth, lastYear int) error { + _, err := client.Call("one.vmpool.showback", filter, firstMonth, firstYear, lastMonth, lastYear) + return err +} + +// CalculateShowback processes all the history records, and stores the monthly cost for each VM +// firstMonth: January is 1. Can be -1, in which case the time interval won't have +// a left boundary. +// firstYear: Can be -1, in which case the time interval won't have a left +// boundary. +// lastMonth: January is 1. Can be -1, in which case the time interval won't have +// a right boundary. +// lastYear: Can be -1, in which case the time interval won't have a right +// boundary. +func (vmpool *VMPool) CalculateShowback(firstMonth, firstYear, lastMonth, lastYear int) error { + _, err := client.Call("one.vmpool.calculateshowback", firstMonth, firstYear, lastMonth, lastYear) + return err +} + +// CreateVM allocates a new VM based on the template string provided. It +// returns the image ID +func CreateVM(template string, pending bool) (uint, error) { + response, err := client.Call("one.vm.allocate", template, pending) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// NewVM finds an VM by ID returns a new VM object. At this stage no +// connection to OpenNebula is performed. +func NewVM(id uint) *VM { + return &VM{ID: id} +} + +// NewVMFromName finds the VM by name and returns a VM object. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the VM. +func NewVMFromName(name string) (*VM, error) { + vmpool, err := NewVMPool() + if err != nil { + return nil, err + } + + id, err := vmpool.GetIDFromName(name, "/VM_POOL/VM") + if err != nil { + return nil, err + } + + return NewVM(id), nil +} + +// State returns the VMState and LCMState +func (vm *VM) State() (VMState, LCMState, error) { + vmStateString, ok := vm.XPath("/VM/STATE") + if ok != true { + return -1, -1, errors.New("Unable to parse VM State") + } + + lcmStateString, ok := vm.XPath("/VM/LCM_STATE") + if ok != true { + return -1, -1, errors.New("Unable to parse LCM State") + } + + vmState, _ := strconv.Atoi(vmStateString) + lcmState, _ := strconv.Atoi(lcmStateString) + + return VMState(vmState), LCMState(lcmState), nil +} + +// StateString returns the VMState and LCMState as strings +func (vm *VM) StateString() (string, string, error) { + vmState, lcmState, err := vm.State() + if err != nil { + return "", "", err + } + return VMState(vmState).String(), LCMState(lcmState).String(), nil +} + +// Action is the generic method to run any action on the VM +func (vm *VM) Action(action string) error { + _, err := client.Call("one.vm.action", action, vm.ID) + return err +} + +// Info connects to OpenNebula and fetches the information of the VM +func (vm *VM) Info() error { + response, err := client.Call("one.vm.info", vm.ID) + vm.body = response.Body() + return err +} + +// Update will modify the VM's template. If appendTemplate is 0, it will +// replace the whole template. If its 1, it will merge. +func (vm *VM) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.vm.update", vm.ID, tpl, appendTemplate) + return err +} + +// UpdateConf updates (appends) a set of supported configuration attributes in +// the VM template +func (vm *VM) UpdateConf(tpl string) error { + _, err := client.Call("one.vm.updateconf", vm.ID, tpl) + return err +} + +// Monitoring Returns the virtual machine monitoring records +func (vm *VM) Monitoring() error { + _, err := client.Call("one.vm.monitoring", vm.ID) + return err +} + +// Chown changes the owner/group of a VM. If uid or gid is -1 it will not +// change +func (vm *VM) Chown(uid, gid int) error { + _, err := client.Call("one.vm.chown", vm.ID, uid, gid) + return err +} + +// Chmod changes the permissions of a VM. If any perm is -1 it will not +// change +func (vm *VM) Chmod(uu, um, ua, gu, gm, ga, ou, om, oa int) error { + _, err := client.Call("one.vm.chmod", vm.ID, uu, um, ua, gu, gm, ga, ou, om, oa) + return err +} + +// Rename changes the name of a VM +func (vm *VM) Rename(newName string) error { + _, err := client.Call("one.vm.rename", vm.ID, newName) + return err +} + +// Delete will remove the VM from OpenNebula +func (vm *VM) Delete() error { + _, err := client.Call("one.vm.delete", vm.ID) + return err +} + +// Deploy in the selected hostID and/or dsID. Enforce to return error in case of +// overcommitment. Enforce is automatically enabled for non-oneadmin users. +func (vm *VM) Deploy(hostID uint, enforce bool, dsID uint) error { + _, err := client.Call("one.vm.deploy", vm.ID, int(hostID), enforce, int(dsID)) + return err +} + +// Resize changes the capacity of the virtual machine +func (vm *VM) Resize(template string, enforce bool) error { + _, err := client.Call("one.vm.resize", vm.ID, template, enforce) + return err +} + +// DiskSaveas exports a disk to an image. If imageType is empty the default one +// will be used. If snapID is -1 the current image state will be exported +func (vm *VM) DiskSaveas(diskID int, imageName, imageType string, snapID int) error { + _, err := client.Call("one.vm.disksaveas", vm.ID, diskID, imageName, imageType, snapID) + return err +} + +// DiskSnapshotCreate will create a snapshot of the disk image +func (vm *VM) DiskSnapshotCreate(diskID int, description string) error { + _, err := client.Call("one.vm.disksnapshotcreate", vm.ID, diskID, description) + return err +} + +// DiskSnapshotDelete will delete a snapshot +func (vm *VM) DiskSnapshotDelete(diskID, snapID int) error { + _, err := client.Call("one.vm.disksnapshotdelete", vm.ID, diskID, snapID) + return err +} + +// DiskSnapshotRevert will revert disk state to a previously taken snapshot +func (vm *VM) DiskSnapshotRevert(diskID, snapID int) error { + _, err := client.Call("one.vm.disksnapshotrevert", vm.ID, diskID, snapID) + return err +} + +// SnapshotCreate creates a new virtual machine snapshot. name can be empty +func (vm *VM) SnapshotCreate(name string) error { + _, err := client.Call("one.vm.snapshotcreate", vm.ID, name) + return err +} + +// SnapshotDelete deletes a virtual machine snapshot +func (vm *VM) SnapshotDelete(snapID int) error { + _, err := client.Call("one.vm.snapshotdelete", vm.ID, snapID) + return err +} + +// SnapshotRevert reverts a virtual machine to a snapshot +func (vm *VM) SnapshotRevert(snapID int) error { + _, err := client.Call("one.vm.snapshotrevert", vm.ID, snapID) + return err +} + +// Attach a new disk to the virtual machine. diskTemplate is a string containing +// a single DISK vector attribute. Syntax can be the usual attribute=value or +// XML +func (vm *VM) Attach(diskTemplate string) error { + _, err := client.Call("one.vm.attach", vm.ID, diskTemplate) + return err +} + +// Detach a disk from a virtual machine +func (vm *VM) Detach(diskID int) error { + _, err := client.Call("one.vm.detach", vm.ID, diskID) + return err +} + +// DiskResize a disk of a virtual machine +func (vm *VM) DiskResize(diskID int, size string) error { + _, err := client.Call("one.vm.diskresize", vm.ID, diskID, size) + return err +} + +// Migrate a VM to a target host and/or to another ds +func (vm *VM) Migrate(hostID uint, live, enforce bool, dsID uint) error { + _, err := client.Call("one.vm.migrate", int(hostID), live, enforce, int(dsID)) + return err +} + +// AttachNic attaches new network interface to the virtual machine +func (vm *VM) AttachNic(tpl string) error { + _, err := client.Call("one.vm.attachnic", vm.ID, tpl) + return err +} + +// DetachNic detaches a network interface from the virtual machine +func (vm *VM) DetachNic(nicID string) error { + _, err := client.Call("one.vm.detachnic", vm.ID, nicID) + return err +} + +// VM Actions + +// TerminateHard action on the VM +func (vm *VM) TerminateHard() error { + return vm.Action("terminate-hard") +} + +// Terminate action on the VM +func (vm *VM) Terminate() error { + return vm.Action("terminate") +} + +// UndeployHard action on the VM +func (vm *VM) UndeployHard() error { + return vm.Action("undeploy-hard") +} + +// Undeploy action on the VM +func (vm *VM) Undeploy() error { + return vm.Action("undeploy") +} + +// PoweroffHard action on the VM +func (vm *VM) PoweroffHard() error { + return vm.Action("poweroff-hard") +} + +// Poweroff action on the VM +func (vm *VM) Poweroff() error { + return vm.Action("poweroff") +} + +// RebootHard action on the VM +func (vm *VM) RebootHard() error { + return vm.Action("reboot-hard") +} + +// Reboot action on the VM +func (vm *VM) Reboot() error { + return vm.Action("reboot") +} + +// Hold action on the VM +func (vm *VM) Hold() error { + return vm.Action("hold") +} + +// Release action on the VM +func (vm *VM) Release() error { + return vm.Action("release") +} + +// Stop action on the VM +func (vm *VM) Stop() error { + return vm.Action("stop") +} + +// Suspend action on the VM +func (vm *VM) Suspend() error { + return vm.Action("suspend") +} + +// Resume action on the VM +func (vm *VM) Resume() error { + return vm.Action("resume") +} + +// Resched action on the VM +func (vm *VM) Resched() error { + return vm.Action("resched") +} + +// Unresched action on the VM +func (vm *VM) Unresched() error { + return vm.Action("unresched") +} + +// End actions + +// Recover recovers a stuck VM that is waiting for a driver operation +func (vm *VM) Recover(op int) error { + _, err := client.Call("one.vm.recover", vm.ID, op) + return err +} + +// RecoverSuccess forces a success +func (vm *VM) RecoverSuccess() error { + return vm.Recover(1) +} + +// RecoverFailure forces a success +func (vm *VM) RecoverFailure() error { + return vm.Recover(0) +} + +// RecoverRetry forces a success +func (vm *VM) RecoverRetry() error { + return vm.Recover(2) +} + +// RecoverDelete forces a delete +func (vm *VM) RecoverDelete() error { + return vm.Recover(3) +} + +// RecoverDeleteRecreate forces a delete +func (vm *VM) RecoverDeleteRecreate() error { + return vm.Recover(4) +} diff --git a/src/oca/go/src/goca/vm_test.go b/src/oca/go/src/goca/vm_test.go new file mode 100644 index 0000000000..736de28d5f --- /dev/null +++ b/src/oca/go/src/goca/vm_test.go @@ -0,0 +1,204 @@ +package goca + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +// Hook up gocheck into the "go test" runner. +func Test(t *testing.T) { TestingT(t) } + +type VMSuite struct { + templateID uint + vmID uint +} + +var _ = Suite(&VMSuite{}) + +func (s *VMSuite) SetUpSuite(c *C) { + // Create template + tpl := NewTemplateBuilder() + + tpl.AddValue("NAME", GenName("VMSuite-template")) + tpl.AddValue("CPU", 1) + tpl.AddValue("MEMORY", "64") + + templateID, err := CreateTemplate(tpl.String()) + c.Assert(err, IsNil) + + s.templateID = templateID +} + +func (s *VMSuite) SetUpTest(c *C) { + template := NewTemplate(s.templateID) + + vmID, err := template.Instantiate("", true, "") + c.Assert(err, IsNil) + s.vmID = vmID +} + +func (s *VMSuite) TearDownTest(c *C) { + vm := NewVM(s.vmID) + + err := vm.TerminateHard() + if err != nil { + err = vm.RecoverDelete() + } + c.Assert(err, IsNil) +} + +func (s *VMSuite) TearDownSuite(c *C) { + template := NewTemplate(s.templateID) + template.Delete() +} + +//////////////////////////////////////////////////////////////////////////////// + +func VMExpectState(c *C, vm *VM, state, lcmState string) func() bool { + return func() bool { + vm.Info() + + s, l, err := vm.StateString() + if err != nil { + return false + } + + if lcmState != "" && l == lcmState { + return true + } + + if state != "" && s == state { + return true + } + + c.Logf("VM: %d. Expecting: %s/%s, Got: %s/%s", vm.ID, state, lcmState, s, l) + + return false + } +} + +//////////////////////////////////////////////////////////////////////////////// + +func (s *VMSuite) TestVMDeploy(c *C) { + vm := NewVM(s.vmID) + err := vm.Release() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "ACTIVE", "RUNNING")), Equals, true) +} + +func (s *VMSuite) TestVMHoldRelease(c *C) { + vm := NewVM(s.vmID) + c.Assert(WaitResource(VMExpectState(c, vm, "HOLD", "")), Equals, true) + + err := vm.Release() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "PENDING", "")), Equals, true) +} + +func (s *VMSuite) TestVMUpdate(c *C) { + vm := NewVM(s.vmID) + err := vm.Update("A=B", 1) + c.Assert(err, IsNil) + + err = vm.Info() + c.Assert(err, IsNil) + + val, ok := vm.XPath("/VM/USER_TEMPLATE/A") + c.Assert(ok, Equals, true) + c.Assert(val, Equals, "B") +} + +// TODO: Hosts +// func (s *VMSuite) TestVMMigrate(c *C) { +// } +// func (s *VMSuite) TestVMLiveMigrate(c *C) { +// } + +func (s *VMSuite) TestVMTerminate(c *C) { + vm := NewVM(s.vmID) + err := vm.Release() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "ACTIVE", "RUNNING")), Equals, true) + + err = vm.Terminate() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "DONE", "")), Equals, true) +} + +func (s *VMSuite) TestVMTerminateHard(c *C) { + vm := NewVM(s.vmID) + err := vm.Release() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "ACTIVE", "RUNNING")), Equals, true) + + vm.TerminateHard() + c.Assert(WaitResource(VMExpectState(c, vm, "DONE", "")), Equals, true) +} + +func (s *VMSuite) TestVMStop(c *C) { + vm := NewVM(s.vmID) + err := vm.Release() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "ACTIVE", "RUNNING")), Equals, true) + + err = vm.Stop() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "STOPPED", "")), Equals, true) +} + +func (s *VMSuite) TestVMSuspend(c *C) { + vm := NewVM(s.vmID) + err := vm.Release() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "ACTIVE", "RUNNING")), Equals, true) + + err = vm.Suspend() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "SUSPENDED", "")), Equals, true) +} + +func (s *VMSuite) TestVMResume(c *C) { + vm := NewVM(s.vmID) + err := vm.Release() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "ACTIVE", "RUNNING")), Equals, true) + + err = vm.Suspend() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "SUSPENDED", "")), Equals, true) + + err = vm.Resume() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "ACTIVE", "RUNNING")), Equals, true) +} + +func (s *VMSuite) TestVMResize(c *C) { + vm := NewVM(s.vmID) + err := vm.Release() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "ACTIVE", "RUNNING")), Equals, true) + + err = vm.Poweroff() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "POWEROFF", "")), Equals, true) + + err = vm.Resize("CPU=2.5\nMEMORY=512", false) + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "POWEROFF", "")), Equals, true) + + err = vm.Resume() + c.Assert(err, IsNil) + c.Assert(WaitResource(VMExpectState(c, vm, "ACTIVE", "RUNNING")), Equals, true) + + err = vm.Info() + c.Assert(err, IsNil) + + cpu, ok := vm.XPath("/VM/TEMPLATE/CPU") + c.Assert(ok, Equals, true) + c.Assert(cpu, Equals, "2.5") + + memory, ok := vm.XPath("/VM/TEMPLATE/MEMORY") + c.Assert(ok, Equals, true) + c.Assert(memory, Equals, "512") +} diff --git a/src/oca/go/src/goca/xmlresource.go b/src/oca/go/src/goca/xmlresource.go new file mode 100644 index 0000000000..2a908f4fbf --- /dev/null +++ b/src/oca/go/src/goca/xmlresource.go @@ -0,0 +1,112 @@ +package goca + +import ( + "bytes" + "errors" + "strconv" + + "gopkg.in/xmlpath.v2" +) + +const ( + // PoolWhoPrimaryGroup resources belonging to the user’s primary group. + PoolWhoPrimaryGroup = -4 + + // PoolWhoMine to list resources that belong to the user that performs the + // query. + PoolWhoMine = -3 + + // PoolWhoAll to list all the resources seen by the user that performs the + // query. + PoolWhoAll = -2 + + // PoolWhoGroup to list all the resources that belong to the group that performs + // the query. + PoolWhoGroup = -1 +) + +// XMLResource contains an XML body field. All the resources in OpenNebula are +// of this kind. +type XMLResource struct { + body string +} + +// XMLIter is used to iterate over XML xpaths in an object. +type XMLIter struct { + iter *xmlpath.Iter +} + +// XMLNode represent an XML node. +type XMLNode struct { + node *xmlpath.Node +} + +// Body accesses the body of an XMLResource +func (r *XMLResource) Body() string { + return r.body +} + +// XPath returns the string pointed at by xpath, for an XMLResource +func (r *XMLResource) XPath(xpath string) (string, bool) { + path := xmlpath.MustCompile(xpath) + b := bytes.NewBufferString(r.Body()) + + root, _ := xmlpath.Parse(b) + + return path.String(root) +} + +// XPathIter returns an XMLIter object pointed at by the xpath +func (r *XMLResource) XPathIter(xpath string) *XMLIter { + path := xmlpath.MustCompile(xpath) + b := bytes.NewBufferString(string(r.Body())) + + root, _ := xmlpath.Parse(b) + + return &XMLIter{iter: path.Iter(root)} +} + +// GetIDFromName finds the a resource by ID by looking at an xpath contained +// in that resource +func (r *XMLResource) GetIDFromName(name string, xpath string) (uint, error) { + var id int + var match = false + + iter := r.XPathIter(xpath) + for iter.Next() { + node := iter.Node() + + n, _ := node.XPath("NAME") + if n == name { + if match { + return 0, errors.New("multiple resources with that name") + } + + idString, _ := node.XPath("ID") + id, _ = strconv.Atoi(idString) + match = true + } + } + + if !match { + return 0, errors.New("resource not found") + } + + return uint(id), nil +} + +// Next moves on to the next resource +func (i *XMLIter) Next() bool { + return i.iter.Next() +} + +// Node returns the XMLNode +func (i *XMLIter) Node() *XMLNode { + return &XMLNode{node: i.iter.Node()} +} + +// XPath returns an XMLNode pointed at by xpath +func (n *XMLNode) XPath(xpath string) (string, bool) { + path := xmlpath.MustCompile(xpath) + return path.String(n.node) +} diff --git a/src/oca/go/src/goca/zone.go b/src/oca/go/src/goca/zone.go new file mode 100644 index 0000000000..4463c6a519 --- /dev/null +++ b/src/oca/go/src/goca/zone.go @@ -0,0 +1,88 @@ +package goca + +// Zone represents an OpenNebula Zone +type Zone struct { + XMLResource + ID uint + Name string +} + +// ZonePool represents an OpenNebula ZonePool +type ZonePool struct { + XMLResource +} + +// NewZonePool returns a zone pool. A connection to OpenNebula is +// performed. +func NewZonePool() (*ZonePool, error) { + response, err := client.Call("one.zonepool.info") + if err != nil { + return nil, err + } + + zonepool := &ZonePool{XMLResource{body: response.Body()}} + + return zonepool, err +} + +// NewZone finds a zone object by ID. No connection to OpenNebula. +func NewZone(id uint) *Zone { + return &Zone{ID: id} +} + +// NewZoneFromName finds a zone object by name. It connects to +// OpenNebula to retrieve the pool, but doesn't perform the Info() call to +// retrieve the attributes of the zone. +func NewZoneFromName(name string) (*Zone, error) { + zonePool, err := NewZonePool() + if err != nil { + return nil, err + } + + id, err := zonePool.GetIDFromName(name, "/ZONE_POOL/ZONE") + if err != nil { + return nil, err + } + + return NewZone(id), nil +} + +// CreateZone allocates a new zone. It returns the new zone ID. +// * tpl: A string containing the template of the ZONE. Syntax can be the usual +// attribute=value or XML. +// * clusterID: The id of the cluster. If -1, the default one will be used +func CreateZone(tpl string, clusterID int) (uint, error) { + response, err := client.Call("one.zone.allocate", tpl, clusterID) + if err != nil { + return 0, err + } + + return uint(response.BodyInt()), nil +} + +// Delete deletes the given zone from the pool. +func (zone *Zone) Delete() error { + _, err := client.Call("one.zone.delete", zone.ID) + return err +} + +// Update replaces the zone template contents. +// * tpl: The new template contents. Syntax can be the usual attribute=value or XML. +// * appendTemplate: Update type: 0: Replace the whole template. 1: Merge new template with the existing one. +func (zone *Zone) Update(tpl string, appendTemplate int) error { + _, err := client.Call("one.zone.update", zone.ID, tpl, appendTemplate) + return err +} + +// Rename renames a zone. +// * newName: The new name. +func (zone *Zone) Rename(newName string) error { + _, err := client.Call("one.zone.rename", zone.ID, newName) + return err +} + +// Info retrieves information for the zone. +func (zone *Zone) Info() error { + _, err := client.Call("one.zone.info", zone.ID) + return err +}