Skip to content
Snippets Groups Projects
Select Git revision
21 results Searching

utils.spec.ts

Blame
  • driver.go 14.15 KiB
    package main
    
    import (
    	"context"
    	"fmt"
    	"io/ioutil"
    	"net"
    	"os"
    	"time"
    
    	"github.com/docker/machine/libmachine/drivers"
    	"github.com/docker/machine/libmachine/log"
    	"github.com/docker/machine/libmachine/mcnflag"
    	"github.com/docker/machine/libmachine/mcnutils"
    	mcnssh "github.com/docker/machine/libmachine/ssh"
    	"github.com/docker/machine/libmachine/state"
    	"github.com/hetznercloud/hcloud-go/hcloud"
    	"github.com/pkg/errors"
    	"golang.org/x/crypto/ssh"
    )
    
    type Driver struct {
    	*drivers.BaseDriver
    
    	AccessToken    string
    	Image          string
    	ImageID        int
    	cachedImage    *hcloud.Image
    	Type           string
    	cachedType     *hcloud.ServerType
    	Location       string
    	cachedLocation *hcloud.Location
    	KeyID          int
    	cachedKey      *hcloud.SSHKey
    	IsExistingKey  bool
    	originalKey    string
    	danglingKey    bool
    	ServerID       int
    	userData       string
    	cachedServer   *hcloud.Server
    }
    
    const (
    	defaultImage = "ubuntu-16.04"
    	defaultType  = "cx11"
    
    	flagAPIToken  = "hetzner-api-token"
    	flagImage     = "hetzner-image"
    	flagImageID   = "hetzner-image-id"
    	flagType      = "hetzner-server-type"
    	flagLocation  = "hetzner-server-location"
    	flagExKeyID   = "hetzner-existing-key-id"
    	flagExKeyPath = "hetzner-existing-key-path"
    	flagUserData  = "hetzner-user-data"
    )
    
    func NewDriver() *Driver {
    	return &Driver{
    		Image:         defaultImage,
    		Type:          defaultType,
    		IsExistingKey: false,
    		danglingKey:   false,
    		BaseDriver: &drivers.BaseDriver{
    			SSHUser: drivers.DefaultSSHUser,
    			SSHPort: drivers.DefaultSSHPort,
    		},
    	}
    }
    
    func (d *Driver) DriverName() string {
    	return "hetzner"
    }
    
    func (d *Driver) GetCreateFlags() []mcnflag.Flag {
    	return []mcnflag.Flag{
    		mcnflag.StringFlag{
    			EnvVar: "HETZNER_API_TOKEN",
    			Name:   flagAPIToken,
    			Usage:  "Project-specific Hetzner API token",
    			Value:  "",
    		},
    		mcnflag.StringFlag{
    			EnvVar: "HETZNER_IMAGE",
    			Name:   flagImage,
    			Usage:  "Image to use for server creation",
    			Value:  defaultImage,
    		},
    		mcnflag.IntFlag{
    			EnvVar: "HETZNER_IMAGE_ID",
    			Name:   flagImageID,
    			Usage:  "Image to use for server creation",
    		},
    		mcnflag.StringFlag{
    			EnvVar: "HETZNER_TYPE",
    			Name:   flagType,
    			Usage:  "Server type to create",
    			Value:  defaultType,
    		},
    		mcnflag.StringFlag{
    			EnvVar: "HETZNER_LOCATION",
    			Name:   flagLocation,
    			Usage:  "Location to create machine at",
    			Value:  "",
    		},
    		mcnflag.IntFlag{
    			EnvVar: "HETZNER_EXISTING_KEY_ID",
    			Name:   flagExKeyID,
    			Usage:  "Existing key ID to use for server; requires --hetzner-existing-key-path",
    			Value:  0,
    		},
    		mcnflag.StringFlag{
    			EnvVar: "HETZNER_EXISTING_KEY_PATH",
    			Name:   flagExKeyPath,
    			Usage:  "Path to existing key (new public key will be created unless --hetzner-existing-key-id is specified)",
    			Value:  "",
    		},
    		mcnflag.StringFlag{
    			EnvVar: "HETZNER_USER_DATA",
    			Name:   flagUserData,
    			Usage:  "Cloud-init based User data",
    			Value:  "",
    		},
    	}
    }
    
    func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error {
    	d.AccessToken = opts.String(flagAPIToken)
    	d.Image = opts.String(flagImage)
    	d.ImageID = opts.Int(flagImageID)
    	d.Location = opts.String(flagLocation)
    	d.Type = opts.String(flagType)
    	d.KeyID = opts.Int(flagExKeyID)
    	d.IsExistingKey = d.KeyID != 0
    	d.originalKey = opts.String(flagExKeyPath)
    	d.userData = opts.String(flagUserData)
    
    	d.SetSwarmConfigFromFlags(opts)
    
    	if d.AccessToken == "" {
    		return errors.Errorf("hetzner requires --%v to be set", flagAPIToken)
    	}
    
    	if d.ImageID != 0 && d.Image != defaultImage {
    		return errors.Errorf("--%v and --%v are mutually exclusive", flagImage, flagImageID)
    	}
    
    	return nil
    }
    
    func (d *Driver) PreCreateCheck() error {
    	if d.IsExistingKey {
    		if d.originalKey == "" {
    			return errors.New("specifying an existing key ID requires the existing key path to be set as well")
    		}
    
    		key, err := d.getKey()
    		if err != nil {
    			return errors.Wrap(err, "could not get key")
    		}
    
    		buf, err := ioutil.ReadFile(d.originalKey + ".pub")
    		if err != nil {
    			return errors.Wrap(err, "could not read public key")
    		}
    
    		// Will also parse `ssh-rsa w309jwf0e39jf asdf` public keys
    		pubk, _, _, _, err := ssh.ParseAuthorizedKey(buf)
    		if err != nil {
    			return errors.Wrap(err, "could not parse authorized key")
    		}
    
    		if key.Fingerprint != ssh.FingerprintLegacyMD5(pubk) &&
    			key.Fingerprint != ssh.FingerprintSHA256(pubk) {
    			return errors.Errorf("remote key %d does not match local key %s", d.KeyID, d.originalKey)
    		}
    	}
    
    	if _, err := d.getType(); err != nil {
    		return errors.Wrap(err, "could not get type")
    	}
    
    	if _, err := d.getImage(); err != nil {
    		return errors.Wrap(err, "could not get image")
    	}
    
    	if _, err := d.getLocation(); err != nil {
    		return errors.Wrap(err, "could not get location")
    	}
    
    	return nil
    }
    
    func (d *Driver) Create() error {
    	if d.originalKey != "" {
    		log.Debugf("Copying SSH key...")
    		if err := d.copySSHKeyPair(d.originalKey); err != nil {
    			return errors.Wrap(err, "could not copy ssh key pair")
    		}
    	} else {
    		log.Debugf("Generating SSH key...")
    		if err := mcnssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil {
    			return errors.Wrap(err, "could not generate ssh key")
    		}
    	}
    
    	if d.KeyID == 0 {
    		log.Infof("Creating SSH key...")
    
    		buf, err := ioutil.ReadFile(d.GetSSHKeyPath() + ".pub")
    		if err != nil {
    			return errors.Wrap(err, "could not read ssh public key")
    		}
    
    		keyopts := hcloud.SSHKeyCreateOpts{
    			Name:      d.GetMachineName(),
    			PublicKey: string(buf),
    		}
    
    		key, _, err := d.getClient().SSHKey.Create(context.Background(), keyopts)
    		if err != nil {
    			return errors.Wrap(err, "could not create ssh key")
    		}
    
    		d.KeyID = key.ID
    		d.danglingKey = true
    
    		defer d.destroyDanglingKey()
    	}
    
    	log.Infof("Creating Hetzner server...")
    
    	srvopts := hcloud.ServerCreateOpts{
    		Name:     d.GetMachineName(),
    		UserData: d.userData,
    	}
    
    	var err error
    	if srvopts.Location, err = d.getLocation(); err != nil {
    		return errors.Wrap(err, "could not get location")
    	}
    	if srvopts.ServerType, err = d.getType(); err != nil {
    		return errors.Wrap(err, "could not get type")
    	}
    	if srvopts.Image, err = d.getImage(); err != nil {
    		return errors.Wrap(err, "could not get image")
    	}
    	key, err := d.getKey()
    	if err != nil {
    		return errors.Wrap(err, "could not get ssh key")
    	}
    	srvopts.SSHKeys = append(srvopts.SSHKeys, key)
    
    	srv, _, err := d.getClient().Server.Create(context.Background(), srvopts)
    	if err != nil {
    		return errors.Wrap(err, "could not create server")
    	}
    
    	log.Infof(" -> Creating server %s[%d] in %s[%d]", srv.Server.Name, srv.Server.ID, srv.Action.Command, srv.Action.ID)
    	if err = d.waitForAction(srv.Action); err != nil {
    		return errors.Wrap(err, "could not wait for action")
    	}
    
    	d.ServerID = srv.Server.ID
    	log.Infof(" -> Server %s[%d]: Waiting to come up...", srv.Server.Name, srv.Server.ID)
    
    	for {
    		srvstate, err := d.GetState()
    		if err != nil {
    			return errors.Wrap(err, "could not get state")
    		}
    
    		if srvstate == state.Running {
    			break
    		}
    
    		time.Sleep(1 * time.Second)
    	}
    
    	log.Debugf(" -> Server %s[%d] ready", srv.Server.Name, srv.Server.ID)
    	d.IPAddress = srv.Server.PublicNet.IPv4.IP.String()
    
    	d.danglingKey = false
    
    	return nil
    }
    
    func (d *Driver) destroyDanglingKey() {
    	if d.danglingKey && !d.IsExistingKey && d.KeyID != 0 {
    		key, err := d.getKey()
    		if err != nil {
    			log.Errorf("could not get key: %v", err)
    			return
    		}
    
    		if _, err := d.getClient().SSHKey.Delete(context.Background(), key); err != nil {
    			log.Errorf("could not delete ssh key: %v", err)
    			return
    		}
    		d.KeyID = 0
    	}
    }
    
    func (d *Driver) GetSSHHostname() (string, error) {
    	return d.GetIP()
    }
    
    func (d *Driver) GetURL() (string, error) {
    	if err := drivers.MustBeRunning(d); err != nil {
    		return "", errors.Wrap(err, "could not execute drivers.MustBeRunning")
    	}
    
    	ip, err := d.GetIP()
    	if err != nil {
    		return "", errors.Wrap(err, "could not get IP")
    	}
    
    	return fmt.Sprintf("tcp://%s", net.JoinHostPort(ip, "2376")), nil
    }
    
    func (d *Driver) GetState() (state.State, error) {
    	srv, _, err := d.getClient().Server.GetByID(context.Background(), d.ServerID)
    	if err != nil {
    		return state.None, errors.Wrap(err, "could not get server by ID")
    	}
    	if srv == nil {
    		return state.None, errors.New("server not found")
    	}
    
    	switch srv.Status {
    	case hcloud.ServerStatusInitializing:
    		return state.Starting, nil
    	case hcloud.ServerStatusRunning:
    		return state.Running, nil
    	case hcloud.ServerStatusOff:
    		return state.Stopped, nil
    	}
    	return state.None, nil
    }
    
    func (d *Driver) Remove() error {
    	if d.ServerID != 0 {
    		srv, err := d.getServerHandle()
    		if err != nil {
    			return errors.Wrap(err, "could not get server handle")
    		}
    
    		if srv == nil {
    			log.Infof(" -> Server does not exist anymore")
    		} else {
    			log.Infof(" -> Destroying server %s[%d] in...", srv.Name, srv.ID)
    
    			if _, err := d.getClient().Server.Delete(context.Background(), srv); err != nil {
    				return errors.Wrap(err, "could not delete server")
    			}
    		}
    	}
    
    	if !d.IsExistingKey && d.KeyID != 0 {
    		key, err := d.getKey()
    		if err != nil {
    			return errors.Wrap(err, "could not get ssh key")
    		}
    		if key == nil {
    			log.Infof(" -> SSH key does not exist anymore")
    			return nil
    		}
    
    		log.Infof(" -> Destroying SSHKey %s[%d]...", key.Name, key.ID)
    
    		if _, err := d.getClient().SSHKey.Delete(context.Background(), key); err != nil {
    			return errors.Wrap(err, "could not delete ssh key")
    		}
    	}
    
    	return nil
    }
    
    func (d *Driver) Restart() error {
    	srv, err := d.getServerHandle()
    	if err != nil {
    		return errors.Wrap(err, "could not get server handle")
    	}
    	if srv == nil {
    		return errors.New("server not found")
    	}
    
    	act, _, err := d.getClient().Server.Reboot(context.Background(), srv)
    	if err != nil {
    		return errors.Wrap(err, "could not reboot server")
    	}
    
    	log.Infof(" -> Rebooting server %s[%d] in %s[%d]...", srv.Name, srv.ID, act.Command, act.ID)
    
    	return d.waitForAction(act)
    }
    
    func (d *Driver) Start() error {
    	srv, err := d.getServerHandle()
    	if err != nil {
    		return errors.Wrap(err, "could not get server handle")
    	}
    	if srv == nil {
    		return errors.New("server not found")
    	}
    
    	act, _, err := d.getClient().Server.Poweron(context.Background(), srv)
    	if err != nil {
    		return errors.Wrap(err, "could not power on server")
    	}
    
    	log.Infof(" -> Starting server %s[%d] in %s[%d]...", srv.Name, srv.ID, act.Command, act.ID)
    
    	return d.waitForAction(act)
    }
    
    func (d *Driver) Stop() error {
    	srv, err := d.getServerHandle()
    	if err != nil {
    		return errors.Wrap(err, "could not get server handle")
    	}
    	if srv == nil {
    		return errors.New("server not found")
    	}
    
    	act, _, err := d.getClient().Server.Shutdown(context.Background(), srv)
    	if err != nil {
    		return errors.Wrap(err, "could not shutdown server")
    	}
    
    	log.Infof(" -> Shutting down server %s[%d] in %s[%d]...", srv.Name, srv.ID, act.Command, act.ID)
    
    	return d.waitForAction(act)
    }
    
    func (d *Driver) Kill() error {
    	srv, err := d.getServerHandle()
    	if err != nil {
    		return errors.Wrap(err, "could not get server handle")
    	}
    	if srv == nil {
    		return errors.New("server not found")
    	}
    
    	act, _, err := d.getClient().Server.Poweroff(context.Background(), srv)
    	if err != nil {
    		return errors.Wrap(err, "could not poweroff server")
    	}
    
    	log.Infof(" -> Powering off server %s[%d] in %s[%d]...", srv.Name, srv.ID, act.Command, act.ID)
    
    	return d.waitForAction(act)
    }
    
    func (d *Driver) getClient() *hcloud.Client {
    	return hcloud.NewClient(hcloud.WithToken(d.AccessToken))
    }
    
    func (d *Driver) copySSHKeyPair(src string) error {
    	if err := mcnutils.CopyFile(src, d.GetSSHKeyPath()); err != nil {
    		return errors.Wrap(err, "could not copy ssh key")
    	}
    
    	if err := mcnutils.CopyFile(src+".pub", d.GetSSHKeyPath()+".pub"); err != nil {
    		return errors.Wrap(err, "could not copy ssh public key")
    	}
    
    	if err := os.Chmod(d.GetSSHKeyPath(), 0600); err != nil {
    		return errors.Wrap(err, "could not set permissions on the ssh key")
    	}
    
    	return nil
    }
    
    func (d *Driver) getLocation() (*hcloud.Location, error) {
    	if d.cachedLocation != nil {
    		return d.cachedLocation, nil
    	}
    
    	location, _, err := d.getClient().Location.GetByName(context.Background(), d.Location)
    	if err != nil {
    		return location, errors.Wrap(err, "could not get location by name")
    	}
    	d.cachedLocation = location
    	return location, nil
    }
    
    func (d *Driver) getType() (*hcloud.ServerType, error) {
    	if d.cachedType != nil {
    		return d.cachedType, nil
    	}
    
    	stype, _, err := d.getClient().ServerType.GetByName(context.Background(), d.Type)
    	if err != nil {
    		return stype, errors.Wrap(err, "could not get type by name")
    	}
    	d.cachedType = stype
    	return stype, nil
    }
    
    func (d *Driver) getImage() (*hcloud.Image, error) {
    	if d.cachedImage != nil {
    		return d.cachedImage, nil
    	}
    
    	var image *hcloud.Image
    	var err error
    
    	if d.ImageID != 0 {
    		image, _, err = d.getClient().Image.GetByID(context.Background(), d.ImageID)
    		if err != nil {
    			return image, errors.Wrap(err, fmt.Sprintf("could not get image by id %v", d.ImageID))
    		}
    	} else {
    		image, _, err = d.getClient().Image.GetByName(context.Background(), d.Image)
    		if err != nil {
    			return image, errors.Wrap(err, fmt.Sprintf("could not get image by name %v", d.Image))
    		}
    	}
    
    	d.cachedImage = image
    	return image, nil
    }
    
    func (d *Driver) getKey() (*hcloud.SSHKey, error) {
    	if d.cachedKey != nil {
    		return d.cachedKey, nil
    	}
    
    	stype, _, err := d.getClient().SSHKey.GetByID(context.Background(), d.KeyID)
    	if err != nil {
    		return stype, errors.Wrap(err, "could not get sshkey by ID")
    	}
    	d.cachedKey = stype
    	return stype, nil
    }
    
    func (d *Driver) getServerHandle() (*hcloud.Server, error) {
    	if d.cachedServer != nil {
    		return d.cachedServer, nil
    	}
    
    	if d.ServerID == 0 {
    		return nil, errors.New("server ID was 0")
    	}
    
    	srv, _, err := d.getClient().Server.GetByID(context.Background(), d.ServerID)
    	if err != nil {
    		return nil, errors.Wrap(err, "could not get client by ID")
    	}
    
    	d.cachedServer = srv
    	return srv, nil
    }
    
    func (d *Driver) waitForAction(a *hcloud.Action) error {
    	for {
    		act, _, err := d.getClient().Action.GetByID(context.Background(), a.ID)
    		if err != nil {
    			return errors.Wrap(err, "could not get client by ID")
    		}
    
    		if act.Status == hcloud.ActionStatusSuccess {
    			log.Debugf(" -> finished %s[%d]", act.Command, act.ID)
    			break
    		} else if act.Status == hcloud.ActionStatusRunning {
    			log.Debugf(" -> %s[%d]: %d %%", act.Command, act.ID, act.Progress)
    		} else if act.Status == hcloud.ActionStatusError {
    			return act.Error()
    		}
    
    		time.Sleep(1 * time.Second)
    	}
    	return nil
    }