* Add Virtual Kubelet provider for VIC Initial virtual kubelet provider for VMware VIC. This provider currently handles creating and starting of a pod VM via the VIC portlayer and persona server. Image store handling via the VIC persona server. This provider currently requires the feature/wolfpack branch of VIC. * Added pod stop and delete. Also added node capacity. Added the ability to stop and delete pod VMs via VIC. Also retrieve node capacity information from the VCH. * Cleanup and readme file Some file clean up and added a Readme.md markdown file for the VIC provider. * Cleaned up errors, added function comments, moved operation code 1. Cleaned up error handling. Set standard for creating errors. 2. Added method prototype comments for all interface functions. 3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder. * Add mocking code and unit tests for podcache, podcreator, and podstarter Used the unit test framework used in VIC to handle assertions in the provider's unit test. Mocking code generated using OSS project mockery, which is compatible with the testify assertion framework. * Vendored packages for the VIC provider Requires feature/wolfpack branch of VIC and a few specific commit sha of projects used within VIC. * Implementation of POD Stopper and Deleter unit tests (#4) * Updated files for initial PR
423 lines
11 KiB
Go
423 lines
11 KiB
Go
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
|
//
|
|
// 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.
|
|
|
|
// +build !windows,!darwin
|
|
|
|
package tether
|
|
|
|
import (
|
|
"archive/tar"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
log "github.com/Sirupsen/logrus"
|
|
dar "github.com/docker/docker/pkg/archive"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"github.com/vmware/govmomi/toolbox"
|
|
"github.com/vmware/govmomi/toolbox/hgfs"
|
|
"github.com/vmware/govmomi/toolbox/vix"
|
|
"github.com/vmware/vic/lib/archive"
|
|
"github.com/vmware/vic/lib/tether/msgs"
|
|
"github.com/vmware/vic/lib/tether/shared"
|
|
"github.com/vmware/vic/pkg/trace"
|
|
)
|
|
|
|
// Toolbox is a tether extension that wraps toolbox.Service
|
|
type Toolbox struct {
|
|
*toolbox.Service
|
|
|
|
sess struct {
|
|
sync.Mutex
|
|
session *SessionConfig
|
|
}
|
|
|
|
// IDs that can be used to authenticate
|
|
authIDs map[string]struct{}
|
|
|
|
stop chan struct{}
|
|
}
|
|
|
|
var (
|
|
defaultArchiveHandler = hgfs.NewArchiveHandler().(*hgfs.ArchiveHandler)
|
|
baseOp = &BaseOperations{}
|
|
)
|
|
|
|
// NewToolbox returns a tether.Extension that wraps the vsphere/toolbox service
|
|
func NewToolbox() *Toolbox {
|
|
in := toolbox.NewBackdoorChannelIn()
|
|
out := toolbox.NewBackdoorChannelOut()
|
|
|
|
service := toolbox.NewService(in, out)
|
|
service.PrimaryIP = toolbox.DefaultIP
|
|
|
|
return &Toolbox{
|
|
Service: service,
|
|
authIDs: make(map[string]struct{}),
|
|
}
|
|
}
|
|
|
|
// Start implementation of the tether.Extension interface
|
|
func (t *Toolbox) Start() error {
|
|
t.stop = make(chan struct{})
|
|
on := make(chan struct{})
|
|
|
|
t.Service.Power.PowerOn.Handler = func() error {
|
|
log.Info("toolbox: service is ready (power on event received)")
|
|
close(on)
|
|
return nil
|
|
}
|
|
|
|
err := t.Service.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Wait for the vmx to send the OS_PowerOn message,
|
|
// at which point it will be ready to service vix command requests.
|
|
log.Info("toolbox: waiting for initialization")
|
|
|
|
select {
|
|
case <-on:
|
|
case <-time.After(time.Second):
|
|
log.Warn("toolbox: timeout waiting for power on event")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop implementation of the tether.Extension interface
|
|
func (t *Toolbox) Stop() error {
|
|
t.Service.Stop()
|
|
|
|
t.Service.Wait()
|
|
|
|
close(t.stop)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reload implementation of the tether.Extension interface
|
|
func (t *Toolbox) Reload(config *ExecutorConfig) error {
|
|
if config != nil && config.Sessions != nil {
|
|
t.sess.Lock()
|
|
defer t.sess.Unlock()
|
|
t.sess.session = config.Sessions[config.ID]
|
|
}
|
|
|
|
// we allow the primary session
|
|
t.authIDs[config.ID] = struct{}{}
|
|
// we also allow any device IDs that are attached
|
|
for _, mspec := range config.Mounts {
|
|
// mounstpect.source.path is the disk label for vmdks
|
|
// TODO: this is not the case for other volumes, eg nfs vols.
|
|
if mspec.Source.Scheme == "label" {
|
|
t.authIDs[mspec.Source.Path] = struct{}{}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InContainer configures the toolbox to run within a container VM
|
|
func (t *Toolbox) InContainer() *Toolbox {
|
|
t.Power.Halt.Handler = t.halt
|
|
|
|
cmd := t.Service.Command
|
|
cmd.Authenticate = t.containerAuthenticate
|
|
cmd.ProcessStartCommand = t.containerStartCommand
|
|
|
|
cmd.FileServer.RegisterFileHandler(hgfs.ArchiveScheme, &hgfs.ArchiveHandler{
|
|
Read: toolboxOverrideArchiveRead,
|
|
Write: toolboxOverrideArchiveWrite,
|
|
})
|
|
|
|
return t
|
|
}
|
|
|
|
func (t *Toolbox) session() *SessionConfig {
|
|
t.sess.Lock()
|
|
defer t.sess.Unlock()
|
|
return t.sess.session
|
|
}
|
|
|
|
func (t *Toolbox) kill(_ context.Context, name string) error {
|
|
session := t.session()
|
|
if session == nil {
|
|
return fmt.Errorf("failed to kill container: process not found")
|
|
}
|
|
|
|
session.Lock()
|
|
defer session.Unlock()
|
|
return t.killHelper(session, name)
|
|
}
|
|
|
|
func (t *Toolbox) killHelper(session *SessionConfig, name string) error {
|
|
if name == "" {
|
|
name = string(ssh.SIGTERM)
|
|
}
|
|
|
|
if session.Cmd.Process == nil {
|
|
return fmt.Errorf("the session %s hasn't launched yet", session.ID)
|
|
}
|
|
|
|
sig := new(msgs.SignalMsg)
|
|
err := sig.FromString(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
num := syscall.Signal(sig.Signum())
|
|
|
|
log.Infof("sending signal %s (%d) to process group for %s", sig.Signal, num, session.ID)
|
|
if err := syscall.Kill(-session.Cmd.Process.Pid, num); err != nil {
|
|
return fmt.Errorf("failed to signal %s group: %s", session.ID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *Toolbox) containerAuthenticate(_ vix.CommandRequestHeader, data []byte) error {
|
|
var c vix.UserCredentialNamePassword
|
|
if err := c.UnmarshalBinary(data); err != nil {
|
|
return err
|
|
}
|
|
|
|
session := t.session()
|
|
if session == nil {
|
|
return errors.New("not yet initialized")
|
|
}
|
|
|
|
session.Lock()
|
|
defer session.Unlock()
|
|
|
|
// no authentication yet, just using container ID and device IDs as a sanity check for now
|
|
if _, ok := t.authIDs[c.Name]; !ok {
|
|
return errors.New("failed to verify authentication ID")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *Toolbox) containerStartCommand(m *toolbox.ProcessManager, r *vix.StartProgramRequest) (int64, error) {
|
|
var p *toolbox.Process
|
|
|
|
switch r.ProgramPath {
|
|
case "kill":
|
|
p = toolbox.NewProcessFunc(t.kill)
|
|
case "reload":
|
|
p = toolbox.NewProcessFunc(func(_ context.Context, _ string) error {
|
|
return ReloadConfig()
|
|
})
|
|
default:
|
|
return -1, fmt.Errorf("unknown command %q", r.ProgramPath)
|
|
}
|
|
|
|
return m.Start(r, p)
|
|
}
|
|
|
|
func (t *Toolbox) halt() error {
|
|
session := t.session()
|
|
if session == nil {
|
|
return fmt.Errorf("failed to halt container: not initialized yet")
|
|
}
|
|
|
|
session.Lock()
|
|
defer session.Unlock()
|
|
|
|
if session.Cmd.Process == nil {
|
|
return fmt.Errorf("the session %s hasn't launched yet", session.ID)
|
|
}
|
|
|
|
log.Infof("stopping %s", session.ID)
|
|
|
|
if err := t.killHelper(session, session.StopSignal); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Killing the executor session in the container VM will stop the tether and its extensions.
|
|
// If that doesn't happen within the timeout, send a SIGKILL.
|
|
select {
|
|
case <-t.stop:
|
|
log.Infof("%s has stopped", session.ID)
|
|
return nil
|
|
case <-time.After(time.Second * 10):
|
|
}
|
|
|
|
log.Warnf("killing %s", session.ID)
|
|
|
|
return session.Cmd.Process.Kill()
|
|
}
|
|
|
|
// toolboxOverrideArchiveRead is the online DataSink Override Handler
|
|
func toolboxOverrideArchiveRead(u *url.URL, tr *tar.Reader) error {
|
|
|
|
// special behavior when using disk-labels and filterspec
|
|
diskLabel := u.Query().Get(shared.DiskLabelQueryName)
|
|
filterSpec := u.Query().Get(shared.FilterSpecQueryName)
|
|
if diskLabel != "" && filterSpec != "" {
|
|
op := trace.NewOperation(context.Background(), "ToolboxOnlineDataSink: %s", u.String())
|
|
op.Debugf("Reading from tar archive to path %s: %s", u.Path, u.String())
|
|
spec, err := archive.DecodeFilterSpec(op, &filterSpec)
|
|
if err != nil {
|
|
op.Errorf(err.Error())
|
|
return err
|
|
}
|
|
|
|
diskPath, err := mountDiskLabel(op, diskLabel)
|
|
if err != nil {
|
|
op.Errorf(err.Error())
|
|
return err
|
|
}
|
|
defer unmount(op, diskPath)
|
|
|
|
// no need to join on u.Path here. u.Path == spec.Rebase, but
|
|
// Unpack will rebase tar headers for us. :thumbsup:
|
|
err = archive.InvokeUnpack(op, tr, spec, diskPath)
|
|
if err != nil {
|
|
op.Errorf(err.Error())
|
|
}
|
|
op.Debugf("Finished reading from tar archive to path %s: %s", u.Path, u.String())
|
|
return err
|
|
}
|
|
return defaultArchiveHandler.Read(u, tr)
|
|
|
|
}
|
|
|
|
// toolboxOverrideArchiveWrite is the Online DataSource Override Handler
|
|
func toolboxOverrideArchiveWrite(u *url.URL, tw *tar.Writer) error {
|
|
|
|
// special behavior when using disk-labels and filterspec
|
|
diskLabel := u.Query().Get(shared.DiskLabelQueryName)
|
|
filterSpec := u.Query().Get(shared.FilterSpecQueryName)
|
|
|
|
skiprecurse, _ := strconv.ParseBool(u.Query().Get(shared.SkipRecurseQueryName))
|
|
skipdata, _ := strconv.ParseBool(u.Query().Get(shared.SkipDataQueryName))
|
|
|
|
if diskLabel != "" && filterSpec != "" {
|
|
op := trace.NewOperation(context.Background(), "ToolboxOnlineDataSource: %s", u.String())
|
|
op.Debugf("Writing to archive from %s: %s", u.Path, u.String())
|
|
|
|
spec, err := archive.DecodeFilterSpec(op, &filterSpec)
|
|
if err != nil {
|
|
op.Errorf(err.Error())
|
|
return err
|
|
}
|
|
|
|
// get the container fs mount
|
|
diskPath, err := mountDiskLabel(op, diskLabel)
|
|
if err != nil {
|
|
op.Errorf(err.Error())
|
|
return err
|
|
}
|
|
defer unmount(op, diskPath)
|
|
|
|
var rc io.ReadCloser
|
|
if skiprecurse {
|
|
// we only want a single file - this is a hack while we're abusing Diff, but
|
|
// accomplish this by generating a single entry ChangeSet
|
|
changes := []dar.Change{
|
|
{
|
|
Kind: dar.ChangeModify,
|
|
Path: u.Path,
|
|
},
|
|
}
|
|
|
|
rc, err = archive.Tar(op, diskPath, changes, spec, !skipdata, false)
|
|
} else {
|
|
rc, err = archive.Diff(op, diskPath, "", spec, !skipdata, false)
|
|
}
|
|
|
|
if err != nil {
|
|
op.Errorf(err.Error())
|
|
return err
|
|
}
|
|
|
|
tr := tar.NewReader(rc)
|
|
defer rc.Close()
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
op.Debugf("Finished writing to archive from %s: %s with error %#v", u.Path, u.String(), err)
|
|
break
|
|
}
|
|
if err != nil {
|
|
op.Errorf("error writing tar: %s", err.Error())
|
|
return err
|
|
}
|
|
op.Debugf("Writing header: %#s", *hdr)
|
|
err = tw.WriteHeader(hdr)
|
|
if err != nil {
|
|
op.Errorf("error writing tar header: %s", err.Error())
|
|
return err
|
|
}
|
|
_, err = io.Copy(tw, tr)
|
|
if err != nil {
|
|
op.Errorf("error writing tar contents: %s", err.Error())
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
return defaultArchiveHandler.Write(u, tw)
|
|
}
|
|
|
|
func mountDiskLabel(op trace.Operation, label string) (string, error) {
|
|
// We know the vmdk will always be attached at '/'
|
|
if label == "containerfs" {
|
|
return "/", nil
|
|
}
|
|
|
|
// otherwise, label represents a volume that needs to be mounted
|
|
tmpDir, err := ioutil.TempDir("", fmt.Sprintf("toolbox-%s", label))
|
|
if err != nil {
|
|
op.Errorf("failed to create mountpoint %s: %s", tmpDir, err)
|
|
return "", fmt.Errorf("failed to create mountpoint %s: %s", tmpDir, err)
|
|
}
|
|
|
|
err = baseOp.MountLabel(op, label, tmpDir)
|
|
if err != nil {
|
|
os.Remove(tmpDir)
|
|
op.Errorf("failed to mount label %s at %s: %s", label, tmpDir, err)
|
|
return "", fmt.Errorf("failed to mount label %s at %s: %s", label, tmpDir, err)
|
|
}
|
|
|
|
return tmpDir, nil
|
|
}
|
|
|
|
func unmount(op trace.Operation, unmountPath string) {
|
|
// don't unmount the root vmdk
|
|
if unmountPath == "/" {
|
|
return
|
|
}
|
|
|
|
// unmount the disk from the temporary directory
|
|
if err := Sys.Syscall.Unmount(unmountPath, syscall.MNT_DETACH); err != nil {
|
|
op.Errorf("failed to unmount %s: %s", unmountPath, err.Error())
|
|
}
|
|
|
|
// finally, remove the temporary directory
|
|
os.Remove(unmountPath)
|
|
}
|