mirror of https://github.com/docker/docs.git
Add a Rackspace Cloud backend based on Gophercloud https://github.com/rackspace/gophercloud.
Signed-off-by: John Hopper john.hopper@jpserver.net [solomon@docker.com: manually resolved conflicts] Signed-off-by: Solomon Hykes <solomon@docker.com>
This commit is contained in:
parent
032972586b
commit
b44dba9051
|
@ -0,0 +1,848 @@
|
||||||
|
//
|
||||||
|
// Copyright (C) 2014 Rackspace Hosting Ltd.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package backends
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
"github.com/dotcloud/docker/engine"
|
||||||
|
"github.com/dotcloud/docker/runconfig"
|
||||||
|
"github.com/rackspace/gophercloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DASS_TARGET_PREFIX = "rdas_target_"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
nilOptions = gophercloud.AuthOptions{}
|
||||||
|
|
||||||
|
// ErrNoAuthUrl errors occur when the value of the OS_AUTH_URL environment variable cannot be determined.
|
||||||
|
ErrNoAuthUrl = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.")
|
||||||
|
|
||||||
|
// ErrNoUsername errors occur when the value of the OS_USERNAME environment variable cannot be determined.
|
||||||
|
ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.")
|
||||||
|
|
||||||
|
// ErrNoPassword errors occur when the value of the OS_PASSWORD environment variable cannot be determined.
|
||||||
|
ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD or OS_API_KEY needs to be set.")
|
||||||
|
)
|
||||||
|
|
||||||
|
// On status callback for when waiting on cloud actions
|
||||||
|
type onStatus func(details *gophercloud.Server) error
|
||||||
|
|
||||||
|
// As the name implies, this function dumbly waits for the desired host status
|
||||||
|
func doNothing(details *gophercloud.Server) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shamelessly taken from docker core
|
||||||
|
func RandomString() string {
|
||||||
|
id := make([]byte, 32)
|
||||||
|
_, err := io.ReadFull(rand.Reader, id)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // This shouldn't happen
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the user's auth options from the openstack env variables
|
||||||
|
func getAuthOptions() (string, gophercloud.AuthOptions, error) {
|
||||||
|
provider := os.Getenv("OS_AUTH_URL")
|
||||||
|
username := os.Getenv("OS_USERNAME")
|
||||||
|
apiKey := os.Getenv("OS_API_KEY")
|
||||||
|
password := os.Getenv("OS_PASSWORD")
|
||||||
|
tenantId := os.Getenv("OS_TENANT_ID")
|
||||||
|
tenantName := os.Getenv("OS_TENANT_NAME")
|
||||||
|
|
||||||
|
if provider == "" {
|
||||||
|
return "", nilOptions, ErrNoAuthUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
if username == "" {
|
||||||
|
return "", nilOptions, ErrNoUsername
|
||||||
|
}
|
||||||
|
|
||||||
|
if password == "" && apiKey == "" {
|
||||||
|
return "", nilOptions, ErrNoPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
ao := gophercloud.AuthOptions{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
ApiKey: apiKey,
|
||||||
|
TenantId: tenantId,
|
||||||
|
TenantName: tenantName,
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider, ao, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP Client with some syntax sugar
|
||||||
|
type HttpClient interface {
|
||||||
|
Call(method, path, body string) (*http.Response, error)
|
||||||
|
Get(path, data string) (resp *http.Response, err error)
|
||||||
|
Delete(path, data string) (resp *http.Response, err error)
|
||||||
|
Post(path, data string) (resp *http.Response, err error)
|
||||||
|
Put(path, data string) (resp *http.Response, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerHttpClient struct {
|
||||||
|
Url *url.URL
|
||||||
|
proto string
|
||||||
|
addr string
|
||||||
|
dockerVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a new docker http client
|
||||||
|
func newDockerHttpClient(peer, dockerVersion string) (client HttpClient, err error) {
|
||||||
|
targetUrl, err := url.Parse(peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUrl.Scheme = "http"
|
||||||
|
|
||||||
|
return &DockerHttpClient{
|
||||||
|
Url: targetUrl,
|
||||||
|
dockerVersion: dockerVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *DockerHttpClient) Call(method, path, body string) (*http.Response, error) {
|
||||||
|
targetUrl, err := url.Parse(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUrl.Host = client.Url.Host
|
||||||
|
targetUrl.Scheme = client.Url.Scheme
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, targetUrl.String(), strings.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *DockerHttpClient) Get(path, data string) (resp *http.Response, err error) {
|
||||||
|
return client.Call("GET", path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *DockerHttpClient) Post(path, data string) (resp *http.Response, err error) {
|
||||||
|
return client.Call("POST", path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *DockerHttpClient) Put(path, data string) (resp *http.Response, err error) {
|
||||||
|
return client.Call("PUT", path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *DockerHttpClient) Delete(path, data string) (resp *http.Response, err error) {
|
||||||
|
return client.Call("DELETE", path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hostbound action is an action that is supplied with an active client to a desired cloud host
|
||||||
|
type hostbound func(client HttpClient) engine.Status
|
||||||
|
|
||||||
|
// A HostContext represents an active session with a cloud host
|
||||||
|
type HostContextCache struct {
|
||||||
|
contexts map[string]*HostContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHostContextCache() (contextCache *HostContextCache) {
|
||||||
|
return &HostContextCache{
|
||||||
|
contexts: make(map[string]*HostContext),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hcc *HostContextCache) Get(id, name string, rax *RaxCloud) (context *HostContext, err error) {
|
||||||
|
return NewHostContext(id, name, rax)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hcc *HostContextCache) GetCached(id, name string, rax *RaxCloud) (context *HostContext, err error) {
|
||||||
|
var found bool
|
||||||
|
if context, found = hcc.contexts[id]; !found {
|
||||||
|
if context, err = NewHostContext(id, name, rax); err == nil {
|
||||||
|
hcc.contexts[id] = context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hcc *HostContextCache) Close() {
|
||||||
|
for _, context := range hcc.contexts {
|
||||||
|
context.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostContext struct {
|
||||||
|
id string
|
||||||
|
name string
|
||||||
|
rax *RaxCloud
|
||||||
|
tunnel *os.Process
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHostContext(id, name string, rax *RaxCloud) (hc *HostContext, err error) {
|
||||||
|
if tunnelProcess, err := rax.openTunnel(id, 8000, 8000); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to tunnel to host %s(id:%s): %v", name, id, err)
|
||||||
|
} else {
|
||||||
|
return &HostContext{
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
rax: rax,
|
||||||
|
tunnel: tunnelProcess,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HostContext) Close() {
|
||||||
|
if hc.tunnel != nil {
|
||||||
|
hc.tunnel.Kill()
|
||||||
|
if state, err := hc.tunnel.Wait(); err != nil {
|
||||||
|
fmt.Printf("Wait result: state:%v, err:%s\n", state, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hc.tunnel = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HostContext) exec(job *engine.Job, action hostbound) (status engine.Status) {
|
||||||
|
if hc.tunnel == nil {
|
||||||
|
return job.Errorf("Tunnel not open to host %s(id:%s)", hc.name, hc.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if client, err := newDockerHttpClient("tcp://localhost:8000", "v1.10"); err == nil {
|
||||||
|
return action(client)
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Unable to init http client: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Rackspace impl of the cloud provider interface
|
||||||
|
type RaxCloud struct {
|
||||||
|
remoteUser string
|
||||||
|
keyFile string
|
||||||
|
regionOverride string
|
||||||
|
image string
|
||||||
|
flavor string
|
||||||
|
instance string
|
||||||
|
scalingStrategy string
|
||||||
|
hostCtxCache *HostContextCache
|
||||||
|
|
||||||
|
gopher gophercloud.CloudServersProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a pointer to a new RaxCloud instance
|
||||||
|
func RaxCloudBackend() *RaxCloud {
|
||||||
|
return &RaxCloud{
|
||||||
|
hostCtxCache: NewHostContextCache(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installs the RaxCloud backend into an engine
|
||||||
|
func (rax *RaxCloud) Install(eng *engine.Engine) (err error) {
|
||||||
|
eng.Register("rax", func(job *engine.Job) (status engine.Status) {
|
||||||
|
return rax.init(job)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes the plugin by handling job arguments
|
||||||
|
func (rax *RaxCloud) init(job *engine.Job) (status engine.Status) {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "rax"
|
||||||
|
app.Usage = "Deploy Docker containers onto your Rackspace Cloud account. Accepts environment variables as listed in: https://github.com/openstack/python-novaclient/#command-line-api"
|
||||||
|
app.Version = "0.0.1"
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{"instance", "", "Sets the server instance to target for specific commands such as version."},
|
||||||
|
cli.StringFlag{"region", "DFW", "Sets the Rackspace region to target. This overrides any environment variables for region."},
|
||||||
|
cli.StringFlag{"key", "/etc/swarmd/rax.id_rsa", "Sets SSH keys file to use when initializing secure connections."},
|
||||||
|
cli.StringFlag{"user", "root", "Username to use when initializing secure connections."},
|
||||||
|
cli.StringFlag{"image", "", "Image to use when provisioning new hosts."},
|
||||||
|
cli.StringFlag{"flavor", "", "Flavor to use when provisioning new hosts."},
|
||||||
|
cli.StringFlag{"auto-scaling", "none", "Flag that when set determines the mode used for auto-scaling."},
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
app.Action = func(ctx *cli.Context) {
|
||||||
|
err = rax.run(ctx, job.Eng)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(job.Args) > 1 {
|
||||||
|
app.Run(append([]string{job.Name}, job.Args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed to init?
|
||||||
|
if err != nil || rax.gopher == nil {
|
||||||
|
app.Run([]string{"rax", "help"})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return engine.StatusErr
|
||||||
|
} else {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) configure(ctx *cli.Context) (err error) {
|
||||||
|
if keyFile := ctx.String("key"); keyFile != "" {
|
||||||
|
rax.keyFile = keyFile
|
||||||
|
|
||||||
|
if regionOverride := ctx.String("region"); regionOverride != "" {
|
||||||
|
rax.regionOverride = regionOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
if remoteUser := ctx.String("user"); remoteUser != "" {
|
||||||
|
rax.remoteUser = remoteUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if instance := ctx.String("instance"); instance != "" {
|
||||||
|
rax.instance = instance
|
||||||
|
}
|
||||||
|
|
||||||
|
if image := ctx.String("image"); image != "" {
|
||||||
|
rax.image = image
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Please set an image ID with --image <id>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if flavor := ctx.String("flavor"); flavor != "" {
|
||||||
|
rax.flavor = flavor
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Please set a flavor ID with --flavor <id>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if scalingStrategy := ctx.String("auto-scaling"); scalingStrategy != "" {
|
||||||
|
rax.scalingStrategy = scalingStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
return rax.createGopherCtx()
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Please set a SSH key with --key <keyfile>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) run(ctx *cli.Context, eng *engine.Engine) (err error) {
|
||||||
|
if err = rax.configure(ctx); err == nil {
|
||||||
|
if _, name, err := rax.findTargetHost(); err == nil {
|
||||||
|
if name == "" {
|
||||||
|
rax.createHost(DASS_TARGET_PREFIX + RandomString()[:12])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
eng.Register("create", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := "/containers/create"
|
||||||
|
config := runconfig.ContainerConfigFromJob(job)
|
||||||
|
|
||||||
|
if data, err := json.Marshal(config); err != nil {
|
||||||
|
return job.Errorf("marshaling failure : %v", err)
|
||||||
|
} else if resp, err := client.Post(path, string(data)); err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
} else {
|
||||||
|
var container struct {
|
||||||
|
Id string
|
||||||
|
Warnings []string
|
||||||
|
}
|
||||||
|
|
||||||
|
if body, err := ioutil.ReadAll(resp.Body); err != nil {
|
||||||
|
return job.Errorf("Failed to copy response body: %v", err)
|
||||||
|
} else if err := json.Unmarshal([]byte(body), &container); err != nil {
|
||||||
|
return job.Errorf("Failed to read container info from body: %v", err)
|
||||||
|
} else {
|
||||||
|
job.Printf("%s\n", container.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("container_delete", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/containers/%s?force=%s", job.Args[0], url.QueryEscape(job.Getenv("forceRemove")))
|
||||||
|
|
||||||
|
if _, err := client.Delete(path, ""); err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("containers", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/containers/json?all=%s&limit=%s",
|
||||||
|
url.QueryEscape(job.Getenv("all")), url.QueryEscape(job.Getenv("limit")))
|
||||||
|
|
||||||
|
if resp, err := client.Get(path, ""); err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
} else {
|
||||||
|
created := engine.NewTable("Created", 0)
|
||||||
|
|
||||||
|
if body, err := ioutil.ReadAll(resp.Body); err != nil {
|
||||||
|
return job.Errorf("Failed to copy response body: %v", err)
|
||||||
|
} else if created.ReadListFrom(body); err != nil {
|
||||||
|
return job.Errorf("Failed to read list from body: %v", err)
|
||||||
|
} else {
|
||||||
|
created.WriteListTo(job.Stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("version", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := "/version"
|
||||||
|
|
||||||
|
if resp, err := client.Get(path, ""); err != nil {
|
||||||
|
return job.Errorf("Failed call %s(path:%s): %v", job.Name, path, err)
|
||||||
|
} else if _, err := io.Copy(job.Stdout, resp.Body); err != nil {
|
||||||
|
return job.Errorf("Failed to copy response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("start", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/containers/%s/start", job.Args[0])
|
||||||
|
config := runconfig.ContainerConfigFromJob(job)
|
||||||
|
|
||||||
|
if data, err := json.Marshal(config); err != nil {
|
||||||
|
return job.Errorf("marshaling failure : %v", err)
|
||||||
|
} else if _, err := client.Post(path, string(data)); err != nil {
|
||||||
|
return job.Errorf("Failed call %s(path:%s): %v", job.Name, path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("stop", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/containers/%s/stop?t=%s", job.Args[0], url.QueryEscape(job.Getenv("t")))
|
||||||
|
|
||||||
|
if _, err := client.Post(path, ""); err != nil {
|
||||||
|
return job.Errorf("Failed call %s(path:%s): %v", job.Name, path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("kill", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/containers/%s/kill?signal=%s", job.Args[0], job.Args[1])
|
||||||
|
|
||||||
|
if _, err := client.Post(path, ""); err != nil {
|
||||||
|
return job.Errorf("Failed call %s(path:%s): %v", job.Name, path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("restart", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/containers/%s/restart?t=%s", url.QueryEscape(job.Getenv("t")))
|
||||||
|
|
||||||
|
if _, err := client.Post(path, ""); err != nil {
|
||||||
|
return job.Errorf("Failed call %s(path:%s): %v", job.Name, path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("inspect", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/containers/%s/json", job.Args[0])
|
||||||
|
|
||||||
|
if resp, err := client.Post(path, ""); err != nil {
|
||||||
|
return job.Errorf("Failed call %s(path:%s): %v", job.Name, path, err)
|
||||||
|
} else if _, err := io.Copy(job.Stdout, resp.Body); err != nil {
|
||||||
|
return job.Errorf("Failed to copy response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("attach", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/containers/%s/attach?stream=%s&stdout=%s&stderr=%s", job.Args[0],
|
||||||
|
url.QueryEscape(job.Getenv("stream")),
|
||||||
|
url.QueryEscape(job.Getenv("stdout")),
|
||||||
|
url.QueryEscape(job.Getenv("stderr")))
|
||||||
|
|
||||||
|
if resp, err := client.Post(path, ""); err != nil {
|
||||||
|
return job.Errorf("Failed call %s(path:%s): %v", job.Name, path, err)
|
||||||
|
} else if _, err := io.Copy(job.Stdout, resp.Body); err != nil {
|
||||||
|
return job.Errorf("Failed to copy response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("pull", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/images/create?fromImage=%s&tag=%s", job.Args[0], url.QueryEscape(job.Getenv("tag")))
|
||||||
|
|
||||||
|
if resp, err := client.Post(path, ""); err != nil {
|
||||||
|
return job.Errorf("Failed call %s(path:%s): %v", job.Name, path, err)
|
||||||
|
} else if _, err := io.Copy(job.Stdout, resp.Body); err != nil {
|
||||||
|
return job.Errorf("Failed to copy response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.Register("logs", func(job *engine.Job) (status engine.Status) {
|
||||||
|
if ctx, err := rax.getHostContext(); err == nil {
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
return ctx.exec(job, func(client HttpClient) (status engine.Status) {
|
||||||
|
path := fmt.Sprintf("/containers/%s/logs?stdout=%s&stderr=%s", job.Args[0], url.QueryEscape(job.Getenv("stdout")), url.QueryEscape(job.Getenv("stderr")))
|
||||||
|
|
||||||
|
if resp, err := client.Get(path, ""); err != nil {
|
||||||
|
return job.Errorf("Failed call %s(path:%s): %v", job.Name, path, err)
|
||||||
|
} else if _, err := io.Copy(job.Stdout, resp.Body); err != nil {
|
||||||
|
return job.Errorf("Failed to copy response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return job.Errorf("Failed to create host context: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eng.RegisterCatchall(func(job *engine.Job) engine.Status {
|
||||||
|
log.Printf("[UNIMPLEMENTED] %s %#v %#v %#v", job.Name, *job, job.Env(), job.Args)
|
||||||
|
return engine.StatusOK
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) Close() {
|
||||||
|
rax.hostCtxCache.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) getHostContext() (hc *HostContext, err error) {
|
||||||
|
if id, name, err := rax.findTargetHost(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return rax.hostCtxCache.Get(id, name, rax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) addressFor(id string) (address string, err error) {
|
||||||
|
var server *gophercloud.Server
|
||||||
|
|
||||||
|
if server, err = rax.gopher.ServerById(id); err == nil {
|
||||||
|
address = server.AccessIPv4
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) createHost(name string) (err error) {
|
||||||
|
createServerInfo := gophercloud.NewServer{
|
||||||
|
Name: name,
|
||||||
|
ImageRef: rax.image,
|
||||||
|
FlavorRef: rax.flavor,
|
||||||
|
}
|
||||||
|
|
||||||
|
if newServer, err := rax.gopher.CreateServer(createServerInfo); err == nil {
|
||||||
|
log.Printf("Waiting for host build to complete: %s\n", name)
|
||||||
|
return rax.waitForStatusById(newServer.Id, "ACTIVE", doNothing)
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) openTunnel(id string, localPort, remotePort int) (*os.Process, error) {
|
||||||
|
ip, err := rax.addressFor(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []string{
|
||||||
|
"-o", "PasswordAuthentication=no",
|
||||||
|
"-o", "LogLevel=quiet",
|
||||||
|
"-o", "UserKnownHostsFile=/dev/null",
|
||||||
|
"-o", "CheckHostIP=no",
|
||||||
|
"-o", "StrictHostKeyChecking=no",
|
||||||
|
"-i", rax.keyFile,
|
||||||
|
"-A",
|
||||||
|
"-p", "22",
|
||||||
|
fmt.Sprintf("%s@%s", rax.remoteUser, ip),
|
||||||
|
"-N",
|
||||||
|
"-f",
|
||||||
|
"-L", fmt.Sprintf("%d:localhost:%d", localPort, remotePort),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("ssh", options...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Process, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) createGopherCtx() (err error) {
|
||||||
|
var (
|
||||||
|
provider string
|
||||||
|
authOptions gophercloud.AuthOptions
|
||||||
|
apiCriteria gophercloud.ApiCriteria
|
||||||
|
access *gophercloud.Access
|
||||||
|
csp gophercloud.CloudServersProvider
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create our auth options set by the user's environment
|
||||||
|
provider, authOptions, err = getAuthOptions()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set our API criteria
|
||||||
|
apiCriteria, err = gophercloud.PopulateApi("rackspace-us")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the region
|
||||||
|
apiCriteria.Type = "compute"
|
||||||
|
apiCriteria.Region = os.Getenv("OS_REGION_NAME")
|
||||||
|
|
||||||
|
if rax.regionOverride != "" {
|
||||||
|
log.Printf("Overriding region set in env with %s", rax.regionOverride)
|
||||||
|
apiCriteria.Region = rax.regionOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiCriteria.Region == "" {
|
||||||
|
return fmt.Errorf("No region set. Please set the OS_REGION_NAME environment variable or use the --region flag.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to authenticate
|
||||||
|
access, err = gophercloud.Authenticate(provider, authOptions)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ERROR: %v\n\n\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a CSP ref!
|
||||||
|
csp, err = gophercloud.ServersApi(access, apiCriteria)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rax.gopher = csp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) findDockerHosts() (ids map[string]string, err error) {
|
||||||
|
var servers []gophercloud.Server
|
||||||
|
|
||||||
|
if servers, err = rax.gopher.ListServersLinksOnly(); err == nil {
|
||||||
|
ids = make(map[string]string)
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
if strings.HasPrefix(server.Name, DASS_TARGET_PREFIX) {
|
||||||
|
ids[server.Id] = server.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) findDeploymentHosts() (ids map[string]string, err error) {
|
||||||
|
var servers map[string]string
|
||||||
|
|
||||||
|
if servers, err = rax.findDockerHosts(); err == nil {
|
||||||
|
ids = make(map[string]string)
|
||||||
|
|
||||||
|
for id, name := range servers {
|
||||||
|
var serverDetails *gophercloud.Server
|
||||||
|
|
||||||
|
if serverDetails, err = rax.gopher.ServerById(id); err == nil {
|
||||||
|
if serverDetails.Status == "ACTIVE" {
|
||||||
|
ids[id] = name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) findTargetHost() (id, name string, err error) {
|
||||||
|
var deploymentHosts map[string]string
|
||||||
|
|
||||||
|
if deploymentHosts, err = rax.findDeploymentHosts(); err == nil {
|
||||||
|
for id, name = range deploymentHosts {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) serverIdByName(name string) (string, error) {
|
||||||
|
if servers, err := rax.gopher.ListServersLinksOnly(); err == nil {
|
||||||
|
for _, server := range servers {
|
||||||
|
if server.Name == name {
|
||||||
|
return server.Id, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("Unable to locate server by name: %s", name)
|
||||||
|
} else {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) waitForStatusByName(name, status string, action onStatus) error {
|
||||||
|
id, err := rax.serverIdByName(name)
|
||||||
|
|
||||||
|
for err == nil {
|
||||||
|
return rax.waitForStatusById(id, status, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rax *RaxCloud) waitForStatusById(id, status string, action onStatus) (err error) {
|
||||||
|
var details *gophercloud.Server
|
||||||
|
for err == nil {
|
||||||
|
if details, err = rax.gopher.ServerById(id); err == nil {
|
||||||
|
log.Printf("Status: %s\n", details.Status)
|
||||||
|
|
||||||
|
if details.Status == status {
|
||||||
|
action(details)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -151,6 +151,10 @@
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/tutumcloud/go-tutum",
|
"ImportPath": "github.com/tutumcloud/go-tutum",
|
||||||
"Rev": "d826286d2e5882428c8163ab44261987bea85a44"
|
"Rev": "d826286d2e5882428c8163ab44261987bea85a44"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/rackspace/gophercloud",
|
||||||
|
"Rev": "2285a429874c1365ef6c6d3ceb08b1d428e26aca"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue