VMware vSphere Integrated Containers provider (#206)

* 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
This commit is contained in:
Loc Nguyen
2018-06-04 15:41:32 -07:00
committed by Ria Bhatia
parent 98a111e8b7
commit 513cebe7b7
6296 changed files with 1123685 additions and 8 deletions

View File

@@ -0,0 +1,37 @@
// Copyright 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.
package attach
import (
"fmt"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/pkg/trace"
)
// Bind sets the *Connected fields of the VirtualSerialPort
func Bind(h interface{}, id string) (interface{}, error) {
defer trace.End(trace.Begin(""))
handle, ok := h.(*exec.Handle)
if !ok {
return nil, fmt.Errorf("Type assertion failed for %#+v", handle)
}
if handle.MigrationError != nil {
return nil, fmt.Errorf("Migration failed %s", handle.MigrationError)
}
return toggle(handle, id, true)
}

View File

@@ -0,0 +1,135 @@
// Copyright 2016 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.
package attach
import (
"fmt"
"net"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/migration/feature"
"github.com/vmware/vic/lib/portlayer/exec"
log "github.com/Sirupsen/logrus"
)
func lookupVCHIP() (net.IP, error) {
// FIXME: THERE MUST BE ANOTHER WAY
// following is from Create@exec.go
ips, err := net.LookupIP(constants.ManagementHostName)
if err != nil {
return nil, err
}
if len(ips) == 0 {
return nil, fmt.Errorf("No IP found on %s", constants.ManagementHostName)
}
if len(ips) > 1 {
return nil, fmt.Errorf("Multiple IPs found on %s: %#v", constants.ManagementHostName, ips)
}
return ips[0], nil
}
func toggle(handle *exec.Handle, id string, connected bool) (*exec.Handle, error) {
// check to see whether id is in Execs, if so set its RunBlock property to connected
session, ok := handle.ExecConfig.Execs[id]
if ok {
if err := compatible(handle); err != nil {
return nil, err
}
if session.Attach {
session.RunBlock = connected
}
}
// get the virtual device list
devices := object.VirtualDeviceList(handle.Config.Hardware.Device)
// select the virtual serial ports
serials := devices.SelectByBackingInfo((*types.VirtualSerialPortURIBackingInfo)(nil))
if len(serials) == 0 {
return nil, fmt.Errorf("Unable to find a device with desired backing")
}
if len(serials) > 1 {
return nil, fmt.Errorf("Multiple matches found with desired backing")
}
serial := serials[0]
ip, err := lookupVCHIP()
if err != nil {
return nil, err
}
log.Debugf("Found a device with desired backing: %#v", serial)
c := serial.GetVirtualDevice().Connectable
b := serial.GetVirtualDevice().Backing.(*types.VirtualSerialPortURIBackingInfo)
serviceURI := fmt.Sprintf("tcp://127.0.0.1:%d", constants.AttachServerPort)
proxyURI := fmt.Sprintf("telnet://%s:%d", ip, constants.SerialOverLANPort)
if b.ProxyURI == proxyURI && c.Connected == connected {
log.Debugf("Already in the desired state, (connected: %t, proxyURI: %s)", connected, proxyURI)
return handle, nil
}
// set the values
log.Debugf("Setting Connected to %t", connected)
c.Connected = connected
if connected && handle.ExecConfig.Sessions[handle.ExecConfig.ID].Attach {
log.Debugf("Setting the start connected state to %t", connected)
c.StartConnected = handle.ExecConfig.Sessions[handle.ExecConfig.ID].Attach
}
log.Debugf("Setting ServiceURI to %s", serviceURI)
b.ServiceURI = serviceURI
log.Debugf("Setting the ProxyURI to %s", proxyURI)
b.ProxyURI = proxyURI
config := &types.VirtualDeviceConfigSpec{
Device: serial,
Operation: types.VirtualDeviceConfigSpecOperationEdit,
}
handle.Spec.DeviceChange = append(handle.Spec.DeviceChange, config)
// check to see whether id is in Sessions, if so set its RunBlock property to connected
// if attach happens before start then this property will be persist in the vmx
// if attach happens after start then this propery will be thrown away by commit (one cannot change persistent extraconfig values if the vm is powered on)
session, ok = handle.ExecConfig.Sessions[id]
if ok {
if session.Attach {
session.RunBlock = connected
}
}
return handle, nil
}
func compatible(h interface{}) error {
if handle, ok := h.(*exec.Handle); ok {
if handle.DataVersion < feature.ExecSupportedVersion {
return fmt.Errorf("attaching exec tasks not supported for this container")
}
return nil
}
return fmt.Errorf("Type assertion failed for %#+v", h)
}

View File

@@ -0,0 +1,484 @@
// 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.
package communication
import (
"context"
"fmt"
"net"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vic/lib/tether/msgs"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/serial"
"github.com/vmware/vic/pkg/trace"
"golang.org/x/crypto/ssh"
"golang.org/x/sync/singleflight"
)
const (
VersionString = "SSH-2.0-VIC"
ClientTimeout = 10 * time.Second
)
// Connector defines the connection and interactions
type Connector struct {
mutex sync.RWMutex
cond *sync.Cond
interactions map[string]*LazySessionInteractor
listener net.Listener
// Quit channel for serve
done chan struct{}
// deduplication of incoming calls
fg singleflight.Group
// graceful shutdown
wg sync.WaitGroup
}
// NewConnector returns a new Connector
func NewConnector(listener net.Listener) *Connector {
defer trace.End(trace.Begin(""))
connector := &Connector{
interactions: make(map[string]*LazySessionInteractor),
listener: listener,
done: make(chan struct{}),
}
connector.cond = sync.NewCond(connector.mutex.RLocker())
return connector
}
// SessionIfAlive returns SessionInteractor or error
func (c *Connector) SessionIfAlive(ctx context.Context, id string) (SessionInteractor, error) {
c.mutex.RLock()
v, ok := c.interactions[id]
c.mutex.RUnlock()
if !ok {
return nil, fmt.Errorf("attach connector: no such connection in the map")
}
// we have an entry in the map, let's check its status
var conn SessionInteractor
var err error
conn, err = v.Initialize()
if err != nil {
goto Error
}
log.Debugf("attach connector: Pinging for %s", id)
if err = conn.Ping(); err != nil {
goto Error
}
log.Debugf("attach connector: Unblocking for %s", id)
if err = conn.Unblock(); err != nil {
goto Error
}
log.Debugf("attach connector: Unblocked %s, returning", id)
return conn, nil
Error:
log.Debugf("attach connector: liveness check failed, removing %s from connection map", id)
c.mutex.Lock()
delete(c.interactions, id)
c.mutex.Unlock()
return nil, err
}
// Interaction returns the interactor corresponding to the specified ID. If the connection doesn't exist
// the method will wait for the specified timeout, returning when the connection is created
// or the timeout expires, whichever occurs first
func (c *Connector) Interaction(ctx context.Context, id string) (SessionInteractor, error) {
defer trace.End(trace.Begin(id))
// make sure that we have only one call in-flight for each ID at any given time
si, err, shared := c.fg.Do(id, func() (interface{}, error) {
return c.interaction(ctx, id)
})
if err != nil {
c.fg.Forget(id)
return nil, err
}
if shared {
log.Debugf("Eliminated duplicated calls to Interaction for %s", id)
}
return si.(SessionInteractor), nil
}
func (c *Connector) interaction(ctx context.Context, id string) (SessionInteractor, error) {
defer trace.End(trace.Begin(id))
conn, err := c.SessionIfAlive(ctx, id)
if conn != nil && err == nil {
return conn, nil
}
if ctx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("attach connector: no such connection")
}
result := make(chan SessionInteractor, 1)
go func() {
ok := false
var v *LazySessionInteractor
c.mutex.RLock()
defer c.mutex.RUnlock()
for !ok && ctx.Err() == nil {
v, ok = c.interactions[id]
if ok {
conn, err := v.Initialize()
if conn != nil && err == nil {
// no need to test this connection as we just created it, unblock if needed
log.Debugf("attach connector: Unblocking for %s", id)
err = conn.Unblock()
if err == nil {
log.Debugf("attach connector: Unblocked %s, returning", id)
result <- conn
return
}
}
if err != nil {
log.Error(err)
}
ok = false
}
// block until cond is updated
log.Infof("attach connector: Connection not found yet for %s", id)
c.cond.Wait()
}
log.Debugf("attach connector: Giving up on connection for %s", id)
}()
select {
case client := <-result:
log.Debugf("attach connector: Found connection for %s: %p", id, client)
return client, nil
case <-ctx.Done():
err := fmt.Errorf("attach connector: Connection not found error for id:%s: %s", id, ctx.Err())
log.Error(err)
// wake up the result gofunc before returning
c.mutex.RLock()
c.cond.Broadcast()
c.mutex.RUnlock()
return nil, err
}
}
// RemoveInteraction removes the session the inteactions map
func (c *Connector) RemoveInteraction(id string) error {
defer trace.End(trace.Begin(id))
var err error
c.mutex.Lock()
v, ok := c.interactions[id]
if ok {
log.Debugf("attach connector: Removing %s from the connection map", id)
delete(c.interactions, id)
c.fg.Forget(id)
}
c.mutex.Unlock()
// the !ok case, but let's check the actual condition that impacts us
if v == nil {
return nil
}
conn := v.SessionInteractor()
if conn != nil {
err = conn.Close()
}
return err
}
// Start starts the connector
func (c *Connector) Start() {
defer trace.End(trace.Begin(""))
c.wg.Add(1)
go c.serve()
}
// Stop stops the connector
func (c *Connector) Stop() {
defer trace.End(trace.Begin(""))
c.listener.Close()
close(c.done)
c.wg.Wait()
}
// Starts the connector listening on the specified source
// TODO: should have mechanism for stopping this, and probably handing off the interactions to another
// routine to insert into the map
func (c *Connector) serve() {
defer c.wg.Done()
for {
if c.listener == nil {
log.Debugf("attach connector: listener closed")
break
}
// check to see whether we should stop accepting new connections and exit
select {
case <-c.done:
log.Debugf("attach connector: done closed")
return
default:
}
conn, err := c.listener.Accept()
if err != nil {
log.Errorf("Error waiting for incoming connection: %s", errors.ErrorStack(err))
continue
}
log.Debugf("attach connector: Received incoming connection")
go c.processIncoming(conn)
}
}
// takes the base connection, determines the ID of the source and stashes it in the map
func (c *Connector) processIncoming(conn net.Conn) {
var err error
defer func() {
if err != nil && conn != nil {
conn.Close()
}
}()
log.Debugf("Initiating ssh handshake with new connection attempt")
for {
if conn == nil {
log.Infof("attach connector: connection closed")
return
}
// TODO needs timeout handling. This could take 30s.
// Timeout for client handshake should be reasonably small.
// Server will try to drain a buffer and if the buffer doesn't contain
// 2 or more bytes it will just wait, so client should timeout.
// However, if timeout is too short, client will flood server with Syn requests.
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
deadline, ok := ctx.Deadline()
if ok {
conn.SetReadDeadline(deadline)
}
if err = serial.HandshakeClient(conn); err == nil {
conn.SetReadDeadline(time.Time{})
log.Debugf("HandshakeClient: connection handshake established")
cancel()
break
}
switch e := err.(type) {
case *serial.HandshakeError:
log.Debugf("HandshakeClient: %v", e)
continue
case *net.OpError:
if e.Temporary() || e.Timeout() {
// if it's a passing error or timeout then try again
continue
}
// if it's not a temporary condition, then treat it as a transport error
log.Errorf("HandshakeClient: transport op-error: %v", e)
conn.Close()
return
default: // includes the io.EOF case
// treat everything unknown as transport errror
log.Errorf("HandshakeClient: transport error: %v (%T)", e, e)
conn.Close()
return
}
}
callback := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
}
config := &ssh.ClientConfig{
User: "daemon",
HostKeyCallback: callback,
ClientVersion: VersionString,
Timeout: ClientTimeout,
}
// create the SSH connection
clientConn, chans, reqs, err := ssh.NewClientConn(conn, "", config)
if err != nil {
log.Errorf("SSH connection could not be established: %s", errors.ErrorStack(err))
return
}
// ask the IDs
ids, err := ContainerIDs(clientConn)
if err != nil {
log.Errorf("SSH connection could not be established: %s", errors.ErrorStack(err))
return
}
// Handle global requests
go c.reqs(reqs, clientConn, ids)
// Handle channel open messages
go c.chans(chans)
// create the connections
c.ids(clientConn, ids)
return
}
// ids iterates over the gived ids and
// - calls Ping for existing connections
// - calls NewSSHInteraction for new connections and fills the connection map
func (c *Connector) ids(conn ssh.Conn, ids []string) {
for _, id := range ids {
// needed for following closure - https://golang.org/doc/faq#closures_and_goroutines
id := id
c.mutex.RLock()
v, ok := c.interactions[id]
c.mutex.RUnlock()
if ok {
si, err := v.Initialize()
if si != nil && err == nil {
if err := si.Ping(); err == nil {
log.Debugf("Connection %s found and alive", id)
continue
}
}
log.Warnf("Connection found but it wasn't alive. Creating a new one")
}
// this is a new connection so learn the version
version, err := ContainerVersion(conn)
if err != nil {
log.Errorf("SSH version could not be learned (id=%s): %s", id, errors.ErrorStack(err))
return
}
lazy := &LazySessionInteractor{
fn: func() (SessionInteractor, error) {
defer trace.End(trace.Begin(id))
return NewSSHInteraction(conn, id, version)
},
}
log.Infof("Established connection with container VM: %s", id)
c.mutex.Lock()
c.interactions[id] = lazy
c.cond.Broadcast()
c.mutex.Unlock()
}
}
// reqs is the global request channel of the portlayer side of the connection
// we keep a list of sessions associated with this connection and drop them from the map when the global mux exits
func (c *Connector) reqs(reqs <-chan *ssh.Request, conn ssh.Conn, ids []string) {
defer trace.End(trace.Begin(""))
var pending func()
// list of session ids mux'ed on this connection
droplist := make(map[string]struct{})
// fill the map with the initial ids
for _, id := range ids {
droplist[id] = struct{}{}
}
for req := range reqs {
ok := true
log.Infof("received global request type %v", req.Type)
switch req.Type {
case msgs.ContainersReq:
pending = func() {
ids := msgs.ContainersMsg{}
if err := ids.Unmarshal(req.Payload); err != nil {
log.Errorf("Unmarshal failed with %s", err)
return
}
c.ids(conn, ids.IDs)
// drop the drop list to clear no longer active sessions from the map
droplist = make(map[string]struct{})
// fill the droplist with the latest info
for _, id := range ids.IDs {
droplist[id] = struct{}{}
}
}
default:
ok = false
}
// make sure that errors get send back if we failed
if req.WantReply {
log.Infof("Sending global request reply %t", ok)
if err := req.Reply(ok, nil); err != nil {
log.Warnf("Failed to reply a request back")
}
}
// run any pending work now that a reply has been sent
if pending != nil {
log.Debug("Invoking pending work for global mux")
go pending()
pending = nil
}
}
// global mux closed so it is time to do cleanup
for id := range droplist {
log.Infof("Droping %s from connection map", id)
c.RemoveInteraction(id)
}
}
// this is the channel mux for the ssh channel . It is configured to reject everything (required)
func (c *Connector) chans(chans <-chan ssh.NewChannel) {
defer trace.End(trace.Begin(""))
for ch := range chans {
ch.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %v", ch.ChannelType()))
}
}

View File

@@ -0,0 +1,253 @@
// Copyright 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.
package communication
import (
"fmt"
"io"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/matryer/resync"
"golang.org/x/crypto/ssh"
"github.com/vmware/vic/lib/migration/feature"
"github.com/vmware/vic/lib/tether/msgs"
"github.com/vmware/vic/pkg/trace"
)
const (
attachChannelType = "attach"
)
// SessionInteractor defines the interaction interface
type SessionInteractor interface {
// Send specific signal
Signal(signal ssh.Signal) error
// Stdout stream
Stdout() io.Reader
// Stderr stream
Stderr() io.Reader
// Stdin stream
Stdin() io.WriteCloser
Close() error
// Resize the terminal
Resize(cols, rows, widthpx, heightpx uint32) error
CloseStdin() error
Ping() error
Unblock() error
}
// interaction implements SessionInteractor using SSH
type interaction struct {
channel ssh.Channel
// to serialize unblock requests
mu sync.Mutex
// avoid spamming unblock messages
unblocked resync.Once
// current feature version that the container provides
version uint32
}
// ContainerVersion asks the version of the containers on the other hand and return them to the caller
func ContainerVersion(conn ssh.Conn) (uint32, error) {
defer trace.End(trace.Begin(""))
ok, reply, err := conn.SendRequest(msgs.VersionReq, true, nil)
if !ok && err == nil {
log.Warnf("VersionReq not supported by the container")
return 0, nil
}
if !ok || err != nil {
return 0, fmt.Errorf("failed to get container version from remote: %s", err)
}
version := msgs.VersionMsg{}
if err = version.Unmarshal(reply); err != nil {
return 0, fmt.Errorf("failed to unmarshal version from remote: %s", err)
}
return version.Version, nil
}
// ContainerIDs asks the ids of the containers on the other hand and return them to the caller
func ContainerIDs(conn ssh.Conn) ([]string, error) {
defer trace.End(trace.Begin(""))
ok, reply, err := conn.SendRequest(msgs.ContainersReq, true, nil)
if !ok || err != nil {
return nil, fmt.Errorf("failed to get container IDs from remote: %s", err)
}
ids := msgs.ContainersMsg{}
if err = ids.Unmarshal(reply); err != nil {
return nil, fmt.Errorf("failed to unmarshal ids from remote: %s", err)
}
return ids.IDs, nil
}
// NewSSHInteraction returns a stream connection to the requested session
// The ssh conn is assumed to be connected to the Executor hosting the session
func NewSSHInteraction(conn ssh.Conn, id string, version uint32) (SessionInteractor, error) {
defer trace.End(trace.Begin(id))
channel, _, err := conn.OpenChannel(attachChannelType, []byte(id))
if err != nil {
return nil, err
}
i := &interaction{
channel: channel,
version: version,
}
return i, nil
}
func (t *interaction) Signal(signal ssh.Signal) error {
defer trace.End(trace.Begin(""))
msg := msgs.SignalMsg{Signal: signal}
ok, err := t.channel.SendRequest(msgs.SignalReq, true, msg.Marshal())
if err == nil && !ok {
return fmt.Errorf("unknown error")
}
if err != nil {
return fmt.Errorf("signal error: %s", err)
}
return nil
}
func (t *interaction) CloseStdin() error {
defer trace.End(trace.Begin(""))
// configure remote to relay EOFs
ok, err := t.channel.SendRequest(msgs.CloseStdinReq, true, nil)
if err == nil && !ok {
return fmt.Errorf("unknown error closing stdin")
}
if err != nil {
return fmt.Errorf("close stdin request error: %s", err)
}
// send inline EOF on the stdin stream
err = t.channel.CloseWrite()
if err != nil {
return fmt.Errorf("close stdin error: %s", err)
}
return nil
}
func (t *interaction) Stdout() io.Reader {
defer trace.End(trace.Begin(""))
return t.channel
}
func (t *interaction) Stderr() io.Reader {
defer trace.End(trace.Begin(""))
return t.channel.Stderr()
}
func (t *interaction) Stdin() io.WriteCloser {
defer trace.End(trace.Begin(""))
return t.channel
}
func (t *interaction) Close() error {
defer trace.End(trace.Begin(""))
return t.channel.Close()
}
// Resize resizes the terminal.
func (t *interaction) Resize(cols, rows, widthpx, heightpx uint32) error {
defer trace.End(trace.Begin(""))
msg := msgs.WindowChangeMsg{
Columns: cols,
Rows: rows,
WidthPx: widthpx,
HeightPx: heightpx,
}
ok, err := t.channel.SendRequest(msgs.WindowChangeReq, true, msg.Marshal())
if err == nil && !ok {
return fmt.Errorf("unknown error")
}
if err != nil {
return fmt.Errorf("resize error: %s", err)
}
return nil
}
// Ping checks the liveliness of the connection
func (t *interaction) Ping() error {
defer trace.End(trace.Begin(""))
if t.version < feature.ExecSupportedVersion {
log.Warnf("Running container does not support Ping request, skipping.")
return nil
}
ok, err := t.channel.SendRequest(msgs.PingReq, true, []byte(msgs.PingMsg))
if !ok || err != nil {
return fmt.Errorf("failed to ping the other side: %s", err)
}
return nil
}
// Unblock sends an unblock msg
func (t *interaction) Unblock() error {
defer trace.End(trace.Begin(""))
var ok bool
var err error
var reset bool
if t.version < feature.ExecSupportedVersion {
log.Warnf("Running container does not support Unblock request, skipping.")
return nil
}
t.mu.Lock()
defer t.mu.Unlock()
t.unblocked.Do(func() {
if ok, err = t.channel.SendRequest(msgs.UnblockReq, true, []byte(msgs.UnblockMsg)); !ok || err != nil {
log.Errorf("failed to unblock the other side: %s", err)
// #5038: resync package is not reentrant so we need to call Reset after this
reset = true
}
})
if reset {
t.unblocked.Reset()
}
return err
}

View File

@@ -0,0 +1,60 @@
// 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.
package communication
import (
"sync"
)
// LazyInitializer defines the function that returns SessionInteractor
type LazyInitializer func() (SessionInteractor, error)
// LazySessionInteractor holds lazily initialized SessionInteractor
type LazySessionInteractor struct {
mu sync.Mutex
si SessionInteractor
fn LazyInitializer
}
// Initialize either returns either already initialized connection or returns the connection after initializing it
func (l *LazySessionInteractor) Initialize() (SessionInteractor, error) {
l.mu.Lock()
defer l.mu.Unlock()
if l.si != nil {
return l.si, nil
}
if l.si == nil && l.fn == nil {
panic("both si and fn are nil")
}
var err error
// l.si is nil but l.fn is not
l.si, err = l.fn()
if err != nil {
return nil, err
}
return l.si, nil
}
// SessionInteractor returns either an initialized connection, or nil if it was never initialized
func (l *LazySessionInteractor) SessionInteractor() SessionInteractor {
l.mu.Lock()
defer l.mu.Unlock()
return l.si
}

View File

@@ -0,0 +1,55 @@
// 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.
package communication
import (
"fmt"
"reflect"
"testing"
)
func TestLazySessionInteractor_Initialize(t *testing.T) {
type fields struct {
si SessionInteractor
fn LazyInitializer
}
tests := []struct {
name string
fields fields
want SessionInteractor
wantErr bool
}{
{"FnIsNil", fields{si: &interaction{}}, &interaction{}, false},
{"SiIsNil", fields{si: nil, fn: func() (SessionInteractor, error) { return &interaction{}, nil }}, &interaction{}, false},
{"FnAndSIAreNotNil", fields{si: &interaction{}, fn: func() (SessionInteractor, error) { return nil, fmt.Errorf("failure") }}, &interaction{}, false},
{"SiIsNilFnWillFail", fields{si: nil, fn: func() (SessionInteractor, error) { return nil, fmt.Errorf("failure") }}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &LazySessionInteractor{
si: tt.fields.si,
fn: tt.fields.fn,
}
got, err := l.Initialize()
if (err != nil) != tt.wantErr {
t.Errorf("LazySessionInteractor.Initialize() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("LazySessionInteractor.Initialize() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,120 @@
// 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.
package communication
import (
"context"
"fmt"
"net"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
)
// Server waits for TCP client connections on serialOverLANPort, then
// once connected, attempts to negotiate an SSH connection to the attached
// client. The client is the ssh server.
type Server struct {
port int
ip string
m sync.RWMutex
l *net.TCPListener
c *Connector
}
// NewServer returns a Server instance
func NewServer(ip string, port int) *Server {
defer trace.End(trace.Begin(""))
return &Server{
ip: ip,
port: port,
}
}
// Start starts the connector with given listener
func (n *Server) Start() error {
defer trace.End(trace.Begin(""))
n.m.Lock()
defer n.m.Unlock()
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", n.ip, n.port))
if err != nil {
return fmt.Errorf("Attach server error %s:%d: %s", n.ip, n.port, errors.ErrorStack(err))
}
n.l, err = net.ListenTCP("tcp", addr)
if err != nil {
return fmt.Errorf("Attach server error %s: %s", addr, errors.ErrorStack(err))
}
log.Infof("Attach server listening on %s:%d", n.ip, n.port)
// starts serving requests immediately
n.c = NewConnector(n.l)
n.c.Start()
return nil
}
// Stop stops the connector
func (n *Server) Stop() error {
defer trace.End(trace.Begin(""))
n.m.Lock()
defer n.m.Unlock()
err := n.l.Close()
n.c.Stop()
return err
}
// Addr returns the address of the underlying listener
func (n *Server) Addr() string {
defer trace.End(trace.Begin(""))
n.m.RLock()
defer n.m.RUnlock()
return n.l.Addr().String()
}
// Interaction returns the session interface for the given container. If the container
// cannot be found, this call will wait for the given timeout.
// id is ID of the container.
func (n *Server) Interaction(ctx context.Context, id string) (SessionInteractor, error) {
defer trace.End(trace.Begin(id))
n.m.RLock()
defer n.m.RUnlock()
return n.c.Interaction(ctx, id)
}
// RemoveInteraction removes the session interface from underlying connector
func (n *Server) RemoveInteraction(id string) error {
defer trace.End(trace.Begin(id))
n.m.Lock()
defer n.m.Unlock()
return n.c.RemoveInteraction(id)
}

View File

@@ -0,0 +1,185 @@
// Copyright 2016 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.
package communication
import (
"net"
"sync"
"testing"
"time"
"context"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/testdata"
log "github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/migration/feature"
"github.com/vmware/vic/lib/tether/msgs"
"github.com/vmware/vic/pkg/serial"
)
// Start the server, make 200 client connections, test they connect, then Stop.
func TestAttachStartStop(t *testing.T) {
log.SetLevel(log.InfoLevel)
if testing.Verbose() {
log.SetLevel(log.DebugLevel)
}
s := NewServer("localhost", 0)
var wg sync.WaitGroup
dial := func() {
defer wg.Done()
c, err := net.Dial("tcp", s.l.Addr().String())
if !assert.NoError(t, err) || !assert.NotNil(t, c) {
return
}
defer c.Close()
buf := make([]byte, 1)
c.SetReadDeadline(time.Now().Add(time.Second))
c.Read(buf)
// This will pass if the client has written a second syn packet by the time it's called. As such we set an
// unbounded readdeadline on the connection.
// We can assert behaviours that take a while, but cannot reliably assert behaviours that require fast scheduling
// of lots of threads on all systems running the CI.
c.SetReadDeadline(time.Time{})
if !assert.NoError(t, serial.HandshakeServer(c), "Expected handshake to succeed on 2nd syn packet from client") {
return
}
}
assert.NoError(t, s.Start())
for i := 0; i < 100; i++ {
wg.Add(1)
go dial()
}
done := make(chan bool)
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fail()
}
assert.NoError(t, s.Stop())
_, err := net.Dial("tcp", s.Addr())
assert.Error(t, err)
}
func TestAttachSshSession(t *testing.T) {
log.SetLevel(log.InfoLevel)
if testing.Verbose() {
log.SetLevel(log.DebugLevel)
}
s := NewServer("localhost", 0)
assert.NoError(t, s.Start())
defer s.Stop()
expectedID := "foo"
// This should block until the ssh server returns its container ID
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := s.c.Interaction(ctx, expectedID)
if !assert.NoError(t, err) {
return
}
}()
// Dial the attach server. This is a TCP client
networkClientCon, err := net.Dial("tcp", s.Addr())
if !assert.NoError(t, err) {
return
}
if !assert.NoError(t, serial.HandshakeServer(networkClientCon)) {
return
}
containerConfig := &ssh.ServerConfig{
NoClientAuth: true,
}
signer, err := ssh.ParsePrivateKey(testdata.PEMBytes["dsa"])
if !assert.NoError(t, err) {
return
}
containerConfig.AddHostKey(signer)
// create the SSH server on the client. The attach server will ssh connect to this.
sshConn, chans, reqs, err := ssh.NewServerConn(networkClientCon, containerConfig)
if !assert.NoError(t, err) {
return
}
defer sshConn.Close()
// Service the incoming Channel channel.
wg.Add(2)
go func() {
defer wg.Done()
exit := 0
for req := range reqs {
if req.Type == msgs.ContainersReq {
msg := msgs.ContainersMsg{IDs: []string{expectedID}}
req.Reply(true, msg.Marshal())
exit++
}
if req.Type == msgs.VersionReq {
msg := msgs.VersionMsg{Version: feature.MaxPluginVersion - 1}
req.Reply(true, msg.Marshal())
exit++
}
if exit == 2 {
break
}
}
}()
go func() {
defer wg.Done()
for ch := range chans {
assert.Equal(t, ch.ChannelType(), attachChannelType)
_, reqs, _ = ch.Accept()
for req := range reqs {
if req.Type == msgs.UnblockReq {
req.Reply(true, nil)
break
}
}
break
}
}()
wg.Wait()
}

View File

@@ -0,0 +1,61 @@
// Copyright 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.
package attach
import (
"fmt"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/pkg/trace"
)
// Join adds network backed serial port to the caller and configures them
func Join(h interface{}) (interface{}, error) {
defer trace.End(trace.Begin(""))
handle, ok := h.(*exec.Handle)
if !ok {
return nil, fmt.Errorf("Type assertion failed for %#+v", handle)
}
// Tether serial port - backed by network
serial := &types.VirtualSerialPort{
VirtualDevice: types.VirtualDevice{
Backing: &types.VirtualSerialPortURIBackingInfo{
VirtualDeviceURIBackingInfo: types.VirtualDeviceURIBackingInfo{
Direction: string(types.VirtualDeviceURIBackingOptionDirectionClient),
ProxyURI: fmt.Sprintf("telnet://0.0.0.0:%d", constants.SerialOverLANPort),
// Set it to 0.0.0.0 during Join call, VCH IP will be set when we call Bind
ServiceURI: fmt.Sprintf("tcp://127.0.0.1:%d", constants.AttachServerPort),
},
},
Connectable: &types.VirtualDeviceConnectInfo{
Connected: false,
StartConnected: false,
AllowGuestControl: true,
},
},
YieldOnPoll: true,
}
config := &types.VirtualDeviceConfigSpec{
Device: serial,
Operation: types.VirtualDeviceConfigSpecOperationAdd,
}
handle.Spec.DeviceChange = append(handle.Spec.DeviceChange, config)
return handle, nil
}

View File

@@ -0,0 +1,36 @@
// Copyright 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.
package attach
import (
"fmt"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/pkg/trace"
)
// Unbind unsets the *Connected fields of the VirtualSerialPort
func Unbind(h interface{}, id string) (interface{}, error) {
defer trace.End(trace.Begin(""))
handle, ok := h.(*exec.Handle)
if !ok {
return nil, fmt.Errorf("Type assertion failed for %#+v", handle)
}
if handle.MigrationError != nil {
return nil, fmt.Errorf("Migration failed %s", handle.MigrationError)
}
return toggle(handle, id, false)
}

View File

@@ -0,0 +1,40 @@
// Copyright 2016 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.
package collector
import (
"github.com/vmware/vic/lib/portlayer/event/events"
)
type Collector interface {
// AddMonitoredObject will add the object for event listening
AddMonitoredObject(ref string) error
// RemoveMonitoredObject will remove the object from event listening
RemoveMonitoredObject(ref string)
// Start listening for events and publish to function
Start() error
// Stop listening for events
Stop()
// Register a callback function
Register(func(events.Event))
// Name returns the collector name
Name() string
}

View File

@@ -0,0 +1,218 @@
// 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.
package vsphere
import (
"context"
"fmt"
"reflect"
"sync"
"github.com/vmware/vic/lib/portlayer/event/events"
vmwEvents "github.com/vmware/govmomi/event"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/types"
log "github.com/Sirupsen/logrus"
)
const (
name = "vSphere Event Collector"
)
type EventCollector struct {
vmwManager *vmwEvents.Manager
mos monitoredCache
callback func(events.Event)
lastProcessedID int32
}
type monitoredCache struct {
mu sync.RWMutex
mos map[string]types.ManagedObjectReference
}
func NewCollector(client *vim25.Client, objects ...string) *EventCollector {
ec := &EventCollector{
vmwManager: vmwEvents.NewManager(client),
mos: monitoredCache{mos: make(map[string]types.ManagedObjectReference)},
// initialize to an index that will not be present in a page
lastProcessedID: -1,
}
for i := range objects {
// #nosec: Errors unhandled.
ec.AddMonitoredObject(objects[i])
}
return ec
}
func (ec *EventCollector) Name() string {
return name
}
// Register an event manager callback with the collector
func (ec *EventCollector) Register(callback func(events.Event)) {
ec.callback = callback
}
func (ec *EventCollector) AddMonitoredObject(ref string) error {
ec.mos.mu.Lock()
defer ec.mos.mu.Unlock()
moRef := types.ManagedObjectReference{}
if !moRef.FromString(ref) {
return fmt.Errorf("%s received an invalid Object to monitor(%s)", name, ref)
}
ec.mos.mos[ref] = moRef
return nil
}
func (ec *EventCollector) RemoveMonitoredObject(ref string) {
ec.mos.mu.Lock()
defer ec.mos.mu.Unlock()
delete(ec.mos.mos, ref)
}
func (ec *EventCollector) monitoredObjects() []types.ManagedObjectReference {
ec.mos.mu.RLock()
defer ec.mos.mu.RUnlock()
refs := make([]types.ManagedObjectReference, 0, len(ec.mos.mos))
for k := range ec.mos.mos {
refs = append(refs, ec.mos.mos[k])
}
return refs
}
func (ec *EventCollector) Stop() {
_, err := ec.vmwManager.Destroy(context.Background())
if err != nil {
log.Warnf("%s failed to destroy the govmomi manager: %s", name, err.Error())
}
}
// eventTypes is used to filter the event collector so we only receive these event types.
var eventTypes []string
func init() {
events := []types.BaseEvent{
(*types.VmGuestShutdownEvent)(nil),
(*types.VmPoweredOnEvent)(nil),
(*types.DrsVmPoweredOnEvent)(nil),
(*types.VmPoweredOffEvent)(nil),
(*types.VmRemovedEvent)(nil),
(*types.VmSuspendedEvent)(nil),
(*types.VmMigratedEvent)(nil),
(*types.DrsVmMigratedEvent)(nil),
(*types.VmRelocatedEvent)(nil),
}
for _, event := range events {
eventTypes = append(eventTypes, reflect.TypeOf(event).Elem().Name())
}
}
// Start the event collector
func (ec *EventCollector) Start() error {
// array of managed objects
refs := ec.monitoredObjects()
// only continue if we have object to monitor
if len(refs) == 0 {
return fmt.Errorf("%s requires at least one Monitored Object: objects[%d]", name, 0)
}
log.Debugf("%s starting collection for %d managed objects", name, len(refs))
// we don't want the event listener to timeout
ctx := context.Background()
// pageSize is the number of events on the last page of the eventCollector
// as new events are added the oldest are removed. Originally this value
// was 1 and we encountered missed events due to them being evicted
// before being processed. We bumped to 25 but we still miss events during
// storms such as a host HA event.
// Setting pageSize to 1000 overwhelmed hostd via the task history and caused
// memory exhaustion. Setting pagesize to 200 while filtering for the specific
// types we require showed directly comparable memory overhead vs the 25 page
// size setting when running full ci. We may still have significantly higher
// memory usage in the scenario where we legitimately have events of interest
// at a rate of greater than 25 per page.
// This should eventually be replaced with a smaller maximum page size, a page
// cursor, and maybe a sliding window for the actual page size.
pageSize := int32(200)
// bool to follow the stream
followStream := true
// don't exceed the govmomi object limit
force := false
//TODO: need a proper way to handle failures / status
go func(pageSize int32, follow bool, ff bool, refs []types.ManagedObjectReference, ec *EventCollector) error {
// the govmomi event listener can only be configured once per session -- so if it's already listening it
// will be replaced
//
// the manager will be closed with the session
for {
err := ec.vmwManager.Events(ctx, refs, pageSize, followStream, force, func(_ types.ManagedObjectReference, page []types.BaseEvent) error {
evented(ec, page)
return nil
}, eventTypes...)
// TODO: this will disappear in the ether
if err != nil {
log.Debugf("Error configuring %s: %s", name, err.Error())
}
}
}(pageSize, followStream, force, refs, ec)
return nil
}
// evented will process the event and execute the registered callback
//
// Initial implmentation will only act on certain events -- future implementations
// may provide more flexibility
func evented(ec *EventCollector, page []types.BaseEvent) {
if ec.callback == nil {
log.Warn("No callback defined for EventManager")
return
}
if len(page) == 0 {
return
}
// skip events already seen
oldIndex := len(page)
for i := range page {
if page[i].GetEvent().Key == ec.lastProcessedID {
oldIndex = i
}
}
// events appear in page with most recent first - need to reverse for sane ordering
// we start from the first new event after the last one processed
for i := oldIndex - 1; i >= 0; i-- {
ec.callback(NewVMEvent(page[i]))
ec.lastProcessedID = page[i].GetEvent().Key
}
}

View File

@@ -0,0 +1,143 @@
// Copyright 2016 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.
package vsphere
import (
"strconv"
"testing"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/govmomi/vim25/types"
"github.com/stretchr/testify/assert"
)
const (
LifeCycle = iota
Reconfigure
Mixed
)
// used to test callbacks
var callcount int
func newVMMO() *types.ManagedObjectReference {
return &types.ManagedObjectReference{Value: "101", Type: "vm"}
}
func TestMonitoredObject(t *testing.T) {
mgr := newCollector()
mo := newVMMO()
mgr.AddMonitoredObject(mo.String())
mos := mgr.monitoredObjects()
assert.Equal(t, 1, len(mos))
mgr.RemoveMonitoredObject(mo.String())
mos = mgr.monitoredObjects()
assert.Equal(t, 0, len(mos))
}
func TestRegistration(t *testing.T) {
mgr := newCollector()
mgr.Register(callMe)
assert.NotNil(t, mgr.callback)
}
func TestEvented(t *testing.T) {
mgr := newCollector()
callcount = 0
// register local callback
mgr.Register(callMe)
// test lifecycle events
page := eventPage(3, LifeCycle)
evented(mgr, page)
assert.Equal(t, 3, callcount)
}
func TestName(t *testing.T) {
mgr := newCollector()
assert.NotNil(t, mgr.Name())
assert.Equal(t, name, mgr.Name())
}
func TestStart(t *testing.T) {
mgr := newCollector()
// start should fail as no objects registered
assert.Error(t, mgr.Start())
}
func TestEventTypes(t *testing.T) {
if len(eventTypes) != 9 {
t.Fatalf("eventTypes=%d", len(eventTypes))
}
f := types.TypeFunc()
for _, name := range eventTypes {
_, ok := f(name)
if !ok {
t.Errorf("unknown event type: %q", name)
}
}
}
func newCollector() *EventCollector {
return &EventCollector{mos: monitoredCache{mos: make(map[string]types.ManagedObjectReference)}, lastProcessedID: -1}
}
// simple callback counter
func callMe(vm events.Event) {
callcount++
}
// utility function to mock a vsphere event
//
// size is the number of events to create
// lifeCycle is true when we want to generate state events
// lifeCycle events == poweredOn, poweredOff, etc..
func eventPage(size int, eventType int) []types.BaseEvent {
page := make([]types.BaseEvent, 0, size)
moid := 100
for i := 0; i < size; i++ {
var eve types.BaseEvent
var eType int
moid++
vm := types.ManagedObjectReference{Value: strconv.Itoa(moid), Type: "vm"}
eType = eventType
if eType == Mixed {
if i%2 == 0 {
eType = LifeCycle
} else {
eType = Reconfigure
}
}
if eType == LifeCycle {
eve = types.BaseEvent(&types.VmPoweredOnEvent{VmEvent: types.VmEvent{Event: types.Event{Vm: &types.VmEventArgument{Vm: vm}}}})
} else {
eve = types.BaseEvent(&types.VmReconfiguredEvent{VmEvent: types.VmEvent{Event: types.Event{Vm: &types.VmEventArgument{Vm: vm}}}})
}
page = append(page, eve)
}
return page
}

View File

@@ -0,0 +1,71 @@
// 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.
package vsphere
import (
"strconv"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/portlayer/event/events"
)
type VMEvent struct {
*events.BaseEvent
}
func NewVMEvent(be types.BaseEvent) *VMEvent {
var ee string
// vm events that we care about
switch be.(type) {
case *types.VmPoweredOnEvent,
*types.DrsVmPoweredOnEvent:
ee = events.ContainerPoweredOn
case *types.VmPoweredOffEvent:
ee = events.ContainerPoweredOff
case *types.VmSuspendedEvent:
ee = events.ContainerSuspended
case *types.VmRemovedEvent:
ee = events.ContainerRemoved
case *types.VmGuestShutdownEvent:
ee = events.ContainerShutdown
case *types.VmMigratedEvent:
ee = events.ContainerMigrated
case *types.DrsVmMigratedEvent:
ee = events.ContainerMigratedByDrs
case *types.VmRelocatedEvent:
ee = events.ContainerRelocated
default:
panic("Unknown event")
}
e := be.GetEvent()
return &VMEvent{
&events.BaseEvent{
Event: ee,
ID: strconv.Itoa(int(e.Key)),
Detail: e.FullFormattedMessage,
Ref: e.Vm.Vm.String(),
CreatedTime: e.CreatedTime,
},
}
}
func (vme *VMEvent) Topic() string {
if vme.Type == "" {
vme.Type = events.NewEventType(vme)
}
return vme.Type.Topic()
}

View File

@@ -0,0 +1,43 @@
// Copyright 2016 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.
package vsphere
import (
"strconv"
"testing"
"time"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/stretchr/testify/assert"
)
func TestNewEvent(t *testing.T) {
vm := newVMMO()
k := 1
msg := "jojo the idiot circus boy"
tt := time.Now().UTC()
vmwEve := &types.VmPoweredOnEvent{VmEvent: types.VmEvent{Event: types.Event{CreatedTime: tt, FullFormattedMessage: msg, Key: int32(k), Vm: &types.VmEventArgument{Vm: *vm}}}}
vme := NewVMEvent(vmwEve)
assert.NotNil(t, vme)
assert.Equal(t, events.ContainerPoweredOn, vme.String())
assert.Equal(t, vm.String(), vme.Reference())
assert.Equal(t, strconv.Itoa(k), vme.EventID())
assert.Equal(t, msg, vme.Message())
assert.Equal(t, "vsphere.VMEvent", vme.Topic())
assert.Equal(t, tt, vme.Created())
}

View File

@@ -0,0 +1,39 @@
// Copyright 2016 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.
/*
Package event manages events via a simple pub / sub mechanism. Events could be created
by vic components or registered Collectors.
Basic Overview
The Event Manager provides basic pub / sub functionality. A subscription consists of a
topic (any defined Event), a subscription name (string) and a callback function. When an event is
published the event manager will determine the event type and check to see if any components
have registered a callback for that event type. For all subscriptions the event manager will
facilitate the callback. Publication of events can be accomplished by any component that has
a pointer to the event manager or via the registered collectors.
Collectors are responsible for collecting events or data from external systems and then publishing
relevant vic events to the event manager. In theory the collector could monitor anything and when
certain criteria are meet publish vic events to the manager. Collectors are registered with the
event manager which instructs the collector where to publish. Multiple collectors are allowed per
event manager, but each collector has a single publish target.
An example of a collector is the vSphere Event Collector which uses the vSphere EventHistoryCollector
to monitor the vSphere event stream and publish relevant events to vic. In the initial implementation
the vSphere Event Collector is focused on a subset of VM Events that are then transformed to vic Events
and published to the event manager.
*/
package event

View File

@@ -0,0 +1,45 @@
// Copyright 2016 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.
package event
import (
"github.com/vmware/vic/lib/portlayer/event/collector"
"github.com/vmware/vic/lib/portlayer/event/events"
)
// EventManager will provide a basic event pub/sub implementation
type EventManager interface {
// RegisterCollector a collector with the manager
RegisterCollector(collector.Collector)
// Collectors returns registered collectors
Collectors() map[string]collector.Collector
// Subscribe for event callbacks
Subscribe(eventTopic string, caller string, callback func(events.Event)) Subscriber
// Unsubscribe from event callbacks
Unsubscribe(eventTopic string, caller string)
// Subscribers will return the subscriber map
Subscribers() map[string]map[string]Subscriber
// Subscribed returns subscriber count
Subscribed() int
// Publish the event to the subscribers
Publish(e events.Event)
}

View File

@@ -0,0 +1,68 @@
// Copyright 2016 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.
package events
import (
"fmt"
"path"
"reflect"
"time"
)
type EventType string
type BaseEvent struct {
Type EventType
Event string
ID string
Detail string
Ref string
CreatedTime time.Time
}
func (be *BaseEvent) EventID() string {
return be.ID
}
// return event type / description
func (be *BaseEvent) String() string {
return be.Event
}
func (be *BaseEvent) Message() string {
return be.Detail
}
func (be *BaseEvent) Reference() string {
return be.Ref
}
func (be *BaseEvent) Created() time.Time {
return be.CreatedTime
}
// NewEventType utility function that uses reflection to return
// the event type
func NewEventType(kind interface{}) EventType {
t := reflect.TypeOf(kind)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
return EventType(fmt.Sprintf("%s.%s", path.Base(t.PkgPath()), t.Name()))
}
func (t EventType) Topic() string {
return string(t)
}

View File

@@ -0,0 +1,43 @@
// Copyright 2016 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.
package events
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewEventType(t *testing.T) {
topic := NewEventType(BaseEvent{})
assert.Contains(t, topic.Topic(), "events", ".", "BaseEvent")
topic = NewEventType(&BaseEvent{})
assert.Contains(t, topic.Topic(), "events", ".", "BaseEvent")
}
func TestBaseEvent(t *testing.T) {
be := &BaseEvent{
Event: "PoweredOn",
ID: "12",
Detail: "VM 123 PoweredOn",
Ref: "vm:12",
}
assert.Equal(t, "PoweredOn", be.String())
assert.Equal(t, "12", be.EventID())
assert.Equal(t, "VM 123 PoweredOn", be.Message())
assert.Equal(t, "vm:12", be.Reference())
}

View File

@@ -0,0 +1,43 @@
// 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.
package events
const (
ContainerCreated = "Created"
ContainerFailed = "Failed"
ContainerMigrated = "Migrated"
ContainerMigratedByDrs = "MigratedByDrs"
ContainerPoweredOff = "PoweredOff"
ContainerPoweredOn = "PoweredOn"
ContainerReconfigured = "Reconfigured"
ContainerRelocated = "Relocated"
ContainerRemoved = "Removed"
ContainerResumed = "Resumed"
ContainerShutdown = "Shutdown"
ContainerStarted = "Started"
ContainerStopped = "Stopped"
ContainerSuspended = "Suspended"
)
type ContainerEvent struct {
*BaseEvent
}
func (ce *ContainerEvent) Topic() string {
if ce.Type == "" {
ce.Type = NewEventType(ce)
}
return ce.Type.Topic()
}

View File

@@ -0,0 +1,37 @@
// Copyright 2016 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.
package events
import (
"time"
)
type Event interface {
EventTopic
// id of event
EventID() string
// event (PowerOn, PowerOff, etc)
String() string
// reference evented object
Reference() string
// event message
Message() string
Created() time.Time
}
type EventTopic interface {
Topic() string
}

View File

@@ -0,0 +1,61 @@
// Copyright 2018 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.
package vsphere
import (
"time"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/vic/pkg/trace"
)
type StateEvent struct {
*events.BaseEvent
}
func NewStateEvent(op trace.Operation, state types.VirtualMachinePowerState, ref types.ManagedObjectReference) *StateEvent {
var ee string
// vm power states that we care about
switch state {
case types.VirtualMachinePowerStatePoweredOn:
ee = events.ContainerPoweredOn
case types.VirtualMachinePowerStatePoweredOff:
ee = events.ContainerPoweredOff
case types.VirtualMachinePowerStateSuspended:
ee = events.ContainerSuspended
default:
panic("Unknown event")
}
return &StateEvent{
&events.BaseEvent{
Event: ee,
ID: op.ID(),
Detail: "Created from power state " + string(state),
Ref: ref.String(),
CreatedTime: time.Now(),
},
}
}
func (se *StateEvent) Topic() string {
if se.Type == "" {
se.Type = events.NewEventType(se)
}
return se.Type.Topic()
}

View File

@@ -0,0 +1,163 @@
// Copyright 2016 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.
package event
import (
"fmt"
"sync"
"github.com/vmware/vic/lib/portlayer/event/collector"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/vic/pkg/trace"
log "github.com/Sirupsen/logrus"
)
type Manager struct {
cos collectorCache
subs subscriberCache
eventQ chan events.Event
}
const eventQSize = 1000
type collectorCache struct {
mu sync.RWMutex
collectors map[string]collector.Collector
}
type subscriberCache struct {
mu sync.RWMutex
subscribers map[string]map[string]Subscriber
}
func NewEventManager(collectors ...collector.Collector) *Manager {
mgr := &Manager{
cos: collectorCache{
collectors: make(map[string]collector.Collector),
},
subs: subscriberCache{
subscribers: make(map[string]map[string]Subscriber),
},
eventQ: make(chan events.Event, eventQSize),
}
// register any collectors provided
for i := range collectors {
mgr.RegisterCollector(collectors[i])
}
// event processor routine
go func() {
for e := range mgr.eventQ {
// subscribers for this event
mgr.subs.mu.RLock()
subs := mgr.subs.subscribers[e.Topic()]
mgr.subs.mu.RUnlock()
log.Debugf("Found %d subscribers to %s: %s - %s", len(subs), e.EventID(), e.Topic(), e.Message())
for sub, s := range subs {
log.Debugf("Event manager calling back to %s for Event(%s): %s", sub, e.EventID(), e.Topic())
s.onEvent(e)
}
}
}()
return mgr
}
func (mgr *Manager) RegisterCollector(collector collector.Collector) {
if collector == nil {
return
}
mgr.cos.mu.Lock()
defer mgr.cos.mu.Unlock()
collector.Register(mgr.Publish)
mgr.cos.collectors[collector.Name()] = collector
}
func (mgr *Manager) Collectors() map[string]collector.Collector {
mgr.cos.mu.RLock()
defer mgr.cos.mu.RUnlock()
c := make(map[string]collector.Collector)
for name, collector := range mgr.cos.collectors {
c[name] = collector
}
return c
}
// Subscribe to the event manager for callback
func (mgr *Manager) Subscribe(eventTopic string, caller string, callback func(events.Event)) Subscriber {
defer trace.End(trace.Begin(fmt.Sprintf("%s:%s", eventTopic, caller)))
mgr.subs.mu.Lock()
defer mgr.subs.mu.Unlock()
if _, ok := mgr.subs.subscribers[eventTopic]; !ok {
mgr.subs.subscribers[eventTopic] = make(map[string]Subscriber)
}
s := newSubscriber(eventTopic, caller, callback)
mgr.subs.subscribers[eventTopic][caller] = s
return s
}
// Unsubscribe from callbacks
func (mgr *Manager) Unsubscribe(eventTopic string, caller string) {
defer trace.End(trace.Begin(fmt.Sprintf("%s:%s", eventTopic, caller)))
mgr.subs.mu.Lock()
defer mgr.subs.mu.Unlock()
if _, ok := mgr.subs.subscribers[eventTopic]; ok {
delete(mgr.subs.subscribers[eventTopic], caller)
}
}
func (mgr *Manager) Subscribers() map[string]map[string]Subscriber {
mgr.subs.mu.RLock()
defer mgr.subs.mu.RUnlock()
s := make(map[string]map[string]Subscriber)
for i, m := range mgr.subs.subscribers {
if _, ok := s[i]; !ok {
s[i] = make(map[string]Subscriber)
}
for k, v := range m {
s[i][k] = v
}
}
return s
}
// RegistryCount returns the callback count
func (mgr *Manager) Subscribed() int {
mgr.subs.mu.RLock()
defer mgr.subs.mu.RUnlock()
count := 0
for _, m := range mgr.subs.subscribers {
count += len(m)
}
return count
}
// Publish events to subscribers
func (mgr *Manager) Publish(e events.Event) {
mgr.eventQ <- e
}

View File

@@ -0,0 +1,98 @@
// Copyright 2016 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.
package event
import (
"testing"
"github.com/vmware/vic/lib/portlayer/event/collector/vsphere"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/govmomi/vim25/types"
"github.com/stretchr/testify/assert"
)
func TestNewManager(t *testing.T) {
mgr := NewEventManager()
assert.NotNil(t, mgr)
}
func TestTopic(t *testing.T) {
vmEvent := newVMEvent()
assert.Equal(t, vmEvent.Topic(), "vsphere.VMEvent")
}
func TestSubscribe(t *testing.T) {
mgr := NewEventManager()
topic := events.NewEventType(vsphere.VMEvent{}).Topic()
mgr.Subscribe(topic, "tester", callback)
subs := mgr.Subscribers()
assert.Equal(t, 1, len(subs))
assert.Equal(t, 1, mgr.Subscribed())
mgr.Subscribe(topic, "tester2", callback)
subs = mgr.Subscribers()
// should still have 1 topic
assert.Equal(t, 1, len(subs))
// now two subscribers for that topic
assert.Equal(t, 2, mgr.Subscribed())
mgr.Subscribe(events.NewEventType(&vsphere.VMEvent{}).Topic(), "tester3", callback)
subs = mgr.Subscribers()
// should still have 1 topic
assert.Equal(t, 1, len(subs))
// now two subscribers for that topic
assert.Equal(t, 3, mgr.Subscribed())
mgr.Unsubscribe(topic, "tester2")
subs = mgr.Subscribers()
// should still have 1 topic
assert.Equal(t, 1, len(subs))
// now two subscribers for that topic
assert.Equal(t, 2, mgr.Subscribed())
mgr.Unsubscribe(events.NewEventType(&vsphere.VMEvent{}).Topic(), "tester3")
subs = mgr.Subscribers()
// should still have 1 topic
assert.Equal(t, 1, len(subs))
// now one subscribers for that topic
assert.Equal(t, 1, mgr.Subscribed())
}
func TestRegisterCollector(t *testing.T) {
mgr := NewEventManager()
// register nil
mgr.RegisterCollector(nil)
assert.Equal(t, 0, len(mgr.Collectors()))
}
// utility methods
func newVMMO() *types.ManagedObjectReference {
return &types.ManagedObjectReference{Value: "101", Type: "vm"}
}
func newBaseEvent() types.BaseEvent {
vm := newVMMO()
return types.BaseEvent(&types.VmPoweredOnEvent{VmEvent: types.VmEvent{Event: types.Event{Vm: &types.VmEventArgument{Vm: *vm}}}})
}
func newVMEvent() *vsphere.VMEvent {
return vsphere.NewVMEvent(newBaseEvent())
}
func callback(e events.Event) {}

View File

@@ -0,0 +1,209 @@
// 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.
package event
import (
"sync/atomic"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vic/lib/portlayer/event/events"
)
const (
suspendDisabled int32 = iota
suspendDiscard
suspendQueue
)
type Subscriber interface {
// Topic returns the topic this subscriber is subscribed to
Topic() string
// Name returns the name of the subscriber
Name() string
// Suspend suspends processing events by the subscriber. If
// queueEvents is true, the events are queued until Resume()
// is called. If queueEvents is false, events passed into
// onEvent() after this call are discarded.
Suspend(queueEvents bool)
// Resume resumes processing of events by the subscriber.
// If Suspend() was called with queueEvents as true, any events
// that were passed to onEvent() after Suspend() returned are
// processed first.
Resume()
// IsSuspended returns true if the subscriber is suspended.
IsSuspended() bool
// Discarded returns the number of packets that were discarded by
// the subscriber as a result of Pause() being called with
// queueEvents as false.
Discarded() uint64
// Dropped returns the number of packets that were dropped when
// the event queue overflows. This only happens when Pause()
// is called with queueEvents as true.
Dropped() uint64
// onEvent is called by event.Manager to send an event to
// a subscriber
onEvent(events.Event)
}
type subscriber struct {
topic string
name string
callback func(e events.Event)
eventQ chan events.Event
suspendState int32
discarded, dropped uint64
suspend chan suspendCmd
}
type suspendCmd struct {
suspend bool
done chan struct{}
}
const maxEventQueueSize = 1000
// newSubscriber creates a new subscriber to topic
func newSubscriber(topic, name string, callback func(e events.Event)) Subscriber {
s := &subscriber{
topic: topic,
name: name,
callback: callback,
eventQ: make(chan events.Event, maxEventQueueSize),
suspend: make(chan suspendCmd),
}
go func() {
suspended := false
var done chan struct{}
for {
if done != nil {
done <- struct{}{}
done = nil
}
if suspended {
select {
case c := <-s.suspend:
suspended = c.suspend
done = c.done
}
continue
}
// not suspended
select {
case e := <-s.eventQ:
s.callback(e)
case c := <-s.suspend:
suspended = c.suspend
done = c.done
}
}
}()
return s
}
// Topic returns the topic this subscriber is subscribed to
func (s *subscriber) Topic() string {
return s.topic
}
// Name returns the name of the subscriber
func (s *subscriber) Name() string {
return s.name
}
// onEvent is called by event.Manager to send an event to
// a subscriber
func (s *subscriber) onEvent(e events.Event) {
switch atomic.LoadInt32(&s.suspendState) {
case suspendDisabled:
s.eventQ <- e
case suspendDiscard:
log.Warnf("discarding event %q", e)
atomic.AddUint64(&s.discarded, 1)
case suspendQueue:
done := false
for !done {
select {
case s.eventQ <- e:
done = true
default:
// make room; discard oldest
log.Warnf("dropping event %q", <-s.eventQ)
atomic.AddUint64(&s.dropped, 1)
}
}
}
}
// Suspend suspends processing events by the subscriber. If
// queueEvents is true, the events are queued until Resume()
// is called. If queueEvents is false, events passed into
// onEvent() after this call are discarded.
func (s *subscriber) Suspend(queueEvents bool) {
defer func() {
done := make(chan struct{})
s.suspend <- suspendCmd{suspend: true, done: done}
<-done
close(done)
}()
if queueEvents {
atomic.StoreInt32(&s.suspendState, suspendQueue)
return
}
atomic.StoreInt32(&s.suspendState, suspendDiscard)
}
// Resume resumes processing of events by the subscriber.
// If Suspend() was called with queueEvents as true, any events
// that were passed to onEvent() after Suspend() returned are
// processed first.
func (s *subscriber) Resume() {
defer func() {
done := make(chan struct{})
s.suspend <- suspendCmd{suspend: false, done: done}
<-done
close(done)
}()
atomic.StoreInt32(&s.suspendState, suspendDisabled)
}
// IsSuspended returns true if the subscriber is suspended.
func (s *subscriber) IsSuspended() bool {
return atomic.LoadInt32(&s.suspendState) != suspendDisabled
}
// Discarded returns the number of packets that were discarded by
// the subscriber as a result of Pause() being called with
// queueEvents as false.
func (s *subscriber) Discarded() uint64 {
return atomic.LoadUint64(&s.discarded)
}
// Dropped returns the number of packets that were dropped when
// the event queue overflows. This only happens when Pause()
// is called with queueEvents as true.
func (s *subscriber) Dropped() uint64 {
return atomic.LoadUint64(&s.dropped)
}

View File

@@ -0,0 +1,218 @@
// 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.
package event
import (
"strconv"
"testing"
"time"
log "github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/portlayer/event/events"
)
func init() {
log.SetLevel(log.DebugLevel)
}
type mockCollector struct {
c func(events.Event)
}
// AddMonitoredObject will add the object for event listening
func (m *mockCollector) AddMonitoredObject(_ string) error {
return nil
}
// RemoveMonitoredObject will remove the object from event listening
func (m *mockCollector) RemoveMonitoredObject(_ string) {
}
// Start listening for events and publish to function
func (m *mockCollector) Start() error {
return nil
}
// Stop listening for events
func (m *mockCollector) Stop() {}
// Register a callback function
func (m *mockCollector) Register(c func(events.Event)) {
m.c = c
}
// Name returns the collector name
func (m *mockCollector) Name() string {
return "mock"
}
type mockEvent struct {
id string
}
// id of event
func (e *mockEvent) EventID() string {
return e.id
}
// event (PowerOn, PowerOff, etc)
func (e *mockEvent) String() string {
return e.id
}
// reference evented object
func (e *mockEvent) Reference() string {
return ""
}
// event message
func (e *mockEvent) Message() string {
return ""
}
func (e *mockEvent) Created() time.Time {
return time.Now()
}
func (e *mockEvent) Topic() string {
return "test"
}
func TestSuspendQueue(t *testing.T) {
c := &mockCollector{}
m := NewEventManager(c)
var evs []events.Event
done := make(chan struct{})
s := m.Subscribe("test", "test", func(e events.Event) {
evs = append(evs, e)
if len(evs) == 100 {
close(done)
}
})
suspended := false
for i := 0; i < 100; i++ {
if !suspended && i >= 50 {
s.Suspend(true)
assert.True(t, s.IsSuspended())
suspended = true
}
c.c(&mockEvent{id: strconv.Itoa(i)})
}
select {
case <-done:
assert.Fail(t, "unexpectedly got all events despite suspend")
case <-time.After(2 * time.Second):
assert.Condition(t, func() bool { return len(evs) <= 50 })
}
s.Resume()
assert.False(t, s.IsSuspended())
select {
case <-done:
case <-time.After(2 * time.Second):
assert.Fail(t, "timed out waiting for suspended events")
}
// check for dups
for i := range evs {
for j := range evs {
if j == i {
continue
}
if evs[j].EventID() == evs[i].EventID() {
assert.Fail(t, "dup event found for id %d", evs[j].EventID())
}
}
}
}
func TestSuspendDiscard(t *testing.T) {
c := &mockCollector{}
m := NewEventManager(c)
var evs []events.Event
s := m.Subscribe("test", "test", func(e events.Event) {
assert.Fail(t, "got an event %q when expecting none", e)
})
// discard events
s.Suspend(false)
assert.True(t, s.IsSuspended())
for i := 0; i < 50; i++ {
c.c(&mockEvent{id: strconv.Itoa(i)})
}
<-time.After(5 * time.Second)
assert.Empty(t, evs)
assert.Equal(t, uint64(50), s.Discarded())
assert.Equal(t, uint64(0), s.Dropped())
}
func TestSuspendOverflow(t *testing.T) {
c := &mockCollector{}
m := NewEventManager(c)
var evs []events.Event
done := make(chan struct{})
s := m.Subscribe("test", "test", func(e events.Event) {
evs = append(evs, e)
if len(evs) == maxEventQueueSize {
close(done)
}
})
s.Suspend(true)
assert.True(t, s.IsSuspended())
for i := 0; i < maxEventQueueSize+1; i++ {
c.c(&mockEvent{id: strconv.Itoa(i)})
}
select {
case <-done:
assert.Fail(t, "unexpectedly got all events despite suspend")
case <-time.After(5 * time.Second):
}
s.Resume()
assert.False(t, s.IsSuspended())
select {
case <-done:
case <-time.After(5 * time.Second):
assert.Fail(t, "timed out waiting for sentinel event")
}
assert.Len(t, evs, maxEventQueueSize)
assert.Equal(t, uint64(1), s.Dropped())
assert.Equal(t, uint64(0), s.Discarded())
// check for dups
for i := range evs {
// should not have an event with id 0
assert.NotEqual(t, 0, evs[i].EventID(), "got event with event id 0")
for j := range evs {
if j == i {
continue
}
if evs[j].EventID() == evs[i].EventID() {
assert.Fail(t, "dup event found for id %d", evs[j].EventID())
}
}
}
}

440
vendor/github.com/vmware/vic/lib/portlayer/exec/base.go generated vendored Normal file
View File

@@ -0,0 +1,440 @@
// 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.
package exec
import (
"context"
"fmt"
"time"
"golang.org/x/crypto/ssh"
"github.com/vmware/govmomi/guest"
"github.com/vmware/govmomi/task"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/migration"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
"github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi"
"github.com/vmware/vic/pkg/vsphere/tasks"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// NotYetExistError is returned when a call that requires a VM exist is made
type NotYetExistError struct {
ID string
}
func (e NotYetExistError) Error() string {
return fmt.Sprintf("%s is not completely created", e.ID)
}
// containerBase holds fields common between Handle and Container. The fields and
// methods in containerBase should not require locking as they're primary use is:
// a. for read-only reference when used in Container
// b. single use/no-concurrent modification when used in Handle
type containerBase struct {
ExecConfig *executor.ExecutorConfig
// Migrated is used during in memory migration to assign whether an execConfig is viable for a commit phase
Migrated bool
// MigrationError means the errors happens during data migration, some operation might fail for we cannot extract the whole container configuration
MigrationError error
DataVersion int
// original - can be pointers so long as refreshes
// use different instances of the structures
Config *types.VirtualMachineConfigInfo
Runtime *types.VirtualMachineRuntimeInfo
// doesn't change so can be copied here
vm *vm.VirtualMachine
}
func newBase(vm *vm.VirtualMachine, c *types.VirtualMachineConfigInfo, r *types.VirtualMachineRuntimeInfo) *containerBase {
base := &containerBase{
ExecConfig: &executor.ExecutorConfig{},
Config: c,
Runtime: r,
vm: vm,
}
// construct a working copy of the exec config
if c != nil && c.ExtraConfig != nil {
var migratedConf map[string]string
containerExecKeyValues := vmomi.OptionValueMap(c.ExtraConfig)
// #nosec: Errors unhandled.
base.DataVersion, _ = migration.ContainerDataVersion(containerExecKeyValues)
migratedConf, base.Migrated, base.MigrationError = migration.MigrateContainerConfig(containerExecKeyValues)
extraconfig.Decode(extraconfig.MapSource(migratedConf), base.ExecConfig)
}
return base
}
// String returns the string representation of ContainerBase
func (c *containerBase) String() string {
return c.ExecConfig.ID
}
// VMReference will provide the vSphere vm managed object reference
func (c *containerBase) VMReference() types.ManagedObjectReference {
var moref types.ManagedObjectReference
if c.vm != nil {
moref = c.vm.Reference()
}
return moref
}
// unlocked refresh of container state
func (c *containerBase) refresh(op trace.Operation) error {
base, err := c.updates(op)
if err != nil {
op.Errorf("Update: unable to update container %s: %s", c, err)
return err
}
// copy over the new state
*c = *base
return nil
}
// updates acquires updates from the infrastructure without holding a lock
func (c *containerBase) updates(op trace.Operation) (*containerBase, error) {
defer trace.End(trace.Begin(c.ExecConfig.ID, op))
var o mo.VirtualMachine
// make sure we have vm
if c.vm == nil {
return nil, NotYetExistError{c.ExecConfig.ID}
}
if c.Config != nil {
op.Debugf("Update: for %s, refreshing from change version %s", c, c.Config.ChangeVersion)
}
if err := c.vm.Properties(op, c.vm.Reference(), []string{"config", "runtime"}, &o); err != nil {
return nil, err
}
base := &containerBase{
vm: c.vm,
Config: o.Config,
Runtime: &o.Runtime,
ExecConfig: &executor.ExecutorConfig{},
}
// Get the ExtraConfig
var migratedConf map[string]string
containerExecKeyValues := vmomi.OptionValueMap(o.Config.ExtraConfig)
if containerExecKeyValues["guestinfo.vice./common/id"] == "" {
return nil, fmt.Errorf("Update: change version %s failed assertion extraconfig id != nil", o.Config.ChangeVersion)
}
op.Debugf("Update: for %s, change version %s, extraconfig id: %+v", c, o.Config.ChangeVersion, containerExecKeyValues["guestinfo.vice./common/id"])
// #nosec: Errors unhandled.
base.DataVersion, _ = migration.ContainerDataVersion(containerExecKeyValues)
migratedConf, base.Migrated, base.MigrationError = migration.MigrateContainerConfig(containerExecKeyValues)
extraconfig.Decode(extraconfig.MapSource(migratedConf), base.ExecConfig)
return base, nil
}
func (c *containerBase) ReloadConfig(op trace.Operation) error {
defer trace.End(trace.Begin(c.ExecConfig.ID, op))
return c.startGuestProgram(op, "reload", "")
}
// WaitForExec waits exec'ed task to set started field or timeout
func (c *containerBase) WaitForExec(op trace.Operation, id string) error {
defer trace.End(trace.Begin(id, op))
return c.waitForExec(op, id)
}
// WaitForSession waits non-exec'ed task to set started field or timeout
func (c *containerBase) WaitForSession(ctx context.Context, id string) error {
defer trace.End(trace.Begin(id, ctx))
return c.waitForSession(ctx, id)
}
func (c *containerBase) startGuestProgram(op trace.Operation, name string, args string) error {
// make sure we have vm
if c.vm == nil {
return NotYetExistError{c.ExecConfig.ID}
}
defer trace.End(trace.Begin(c.ExecConfig.ID+":"+name, op))
o := guest.NewOperationsManager(c.vm.Client.Client, c.vm.Reference())
m, err := o.ProcessManager(op)
if err != nil {
return err
}
spec := types.GuestProgramSpec{
ProgramPath: name,
Arguments: args,
}
auth := types.NamePasswordAuthentication{
Username: c.ExecConfig.ID,
}
_, err = m.StartProgram(op, &auth, &spec)
return err
}
func (c *containerBase) start(op trace.Operation) error {
defer trace.End(trace.Begin(c.ExecConfig.ID, op))
// make sure we have vm
if c.vm == nil {
return NotYetExistError{c.ExecConfig.ID}
}
// Power on
_, err := c.vm.WaitForResult(op, func(op context.Context) (tasks.Task, error) {
return c.vm.PowerOn(op)
})
return err
}
func (c *containerBase) stop(op trace.Operation, waitTime *int32) error {
// make sure we have vm
if c.vm == nil {
return NotYetExistError{c.ExecConfig.ID}
}
// get existing state and set to stopping
// if there's a failure we'll revert to existing
err := c.shutdown(op, waitTime)
if err == nil {
return nil
}
op.Warnf("stopping %s via hard power off due to: %s", c, err.Error())
return c.poweroff(op)
}
func (c *containerBase) kill(op trace.Operation) error {
// make sure we have vm
if c.vm == nil {
return NotYetExistError{c.ExecConfig.ID}
}
wait := 10 * time.Second // default
timeout, cancel := trace.WithTimeout(&op, wait, "kill")
defer cancel()
sig := string(ssh.SIGKILL)
timeout.Infof("sending kill -%s %s", sig, c)
err := c.startGuestProgram(timeout, "kill", sig)
if err == nil && timeout.Err() != nil {
timeout.Warnf("timeout (%s) waiting for %s to power off via SIG%s", wait, c, sig)
}
if err != nil {
timeout.Warnf("killing %s attempt resulted in: %s", c, err.Error())
if isInvalidPowerStateError(err) {
return nil
}
}
// Even if startGuestProgram failed above, it may actually have executed. If the container came up and then
// we kill it before VC gets a chance to detect the toolbox, vSphere can execute the kill but report an
// error 3016 indicating the guest toolbox wasn't found. If we then try to poweroff, it may throw vSphere
// into an invalid transition and will need to recover. If we try to grab properties at this time, the
// power state may be incorrect. We work around this by waiting on the power state, regardless of error
// from startGuestProgram. https://github.com/vmware/vic/issues/5803
timeout.Infof("waiting %s for %s to power off", wait, c)
err = c.vm.WaitForPowerState(timeout, types.VirtualMachinePowerStatePoweredOff)
if err == nil {
return nil // VM has powered off
}
timeout.Warnf("killing %s via hard power off", c)
// stop wait time is not applied for the hard kill
return c.poweroff(op)
}
func (c *containerBase) shutdown(op trace.Operation, waitTime *int32) error {
// make sure we have vm
if c.vm == nil {
return NotYetExistError{c.ExecConfig.ID}
}
wait := 10 * time.Second // default
if waitTime != nil && *waitTime > 0 {
wait = time.Duration(*waitTime) * time.Second
}
cs := c.ExecConfig.Sessions[c.ExecConfig.ID]
stop := []string{cs.StopSignal, string(ssh.SIGKILL)}
if stop[0] == "" {
stop[0] = string(ssh.SIGTERM)
}
var killed bool
for _, sig := range stop {
msg := fmt.Sprintf("sending kill -%s %s", sig, c)
op.Infof(msg)
timeout, cancel := trace.WithTimeout(&op, wait, "shutdown")
defer cancel()
err := c.startGuestProgram(timeout, "kill", sig)
if err != nil {
// Just warn and proceed to waiting for power state per issue https://github.com/vmware/vic/issues/5803
// Description above in function kill()
timeout.Warnf("%s: %s", msg, err)
// If the error tells us "The attempted operation cannot be performed in the current state (Powered off)" (InvalidPowerState),
// we can avoid hard poweroff (issues #6236 and #6252). Here we wait for the power state changes instead of return
// immediately to avoid excess vSphere queries
if isInvalidPowerStateError(err) {
killed = true
}
}
timeout.Infof("waiting %s for %s to power off", wait, c)
err = c.vm.WaitForPowerState(timeout, types.VirtualMachinePowerStatePoweredOff)
if err == nil {
return nil // VM has powered off
}
if timeout.Err() == nil {
return err // error other than timeout
}
timeout.Warnf("timeout (%s) waiting for %s to power off via SIG%s", wait, c, sig)
if killed {
return nil
}
}
return fmt.Errorf("failed to shutdown %s via kill signals %s", c, stop)
}
func (c *containerBase) poweroff(op trace.Operation) error {
// make sure we have vm
if c.vm == nil {
return NotYetExistError{c.ExecConfig.ID}
}
_, err := c.vm.WaitForResult(op, func(op context.Context) (tasks.Task, error) {
return c.vm.PowerOff(op)
})
if err != nil {
// It is possible the VM has finally shutdown in between, ignore the error in that case
if terr, ok := err.(task.Error); ok {
switch terr := terr.Fault().(type) {
case *types.InvalidPowerState:
if terr.ExistingState == types.VirtualMachinePowerStatePoweredOff {
op.Warnf("power off %s task skipped (state was already %s)", c, terr.ExistingState)
return nil
}
op.Warnf("invalid power state during power off: %s", terr.ExistingState)
case *types.GenericVmConfigFault:
// Check if the poweroff task was canceled due to a concurrent guest shutdown
if len(terr.FaultMessage) > 0 {
k := terr.FaultMessage[0].Key
if k == vmNotSuspendedKey || k == vmPoweringOffKey {
op.Infof("power off %s task skipped due to guest shutdown", c)
return nil
}
}
op.Warnf("generic vm config fault during power off: %#v", terr)
default:
op.Warnf("hard power off failed due to: %#v", terr)
}
}
return err
}
return nil
}
func (c *containerBase) waitForPowerState(ctx context.Context, max time.Duration, state types.VirtualMachinePowerState) (bool, error) {
defer trace.End(trace.Begin(c.ExecConfig.ID, ctx))
timeout, cancel := context.WithTimeout(ctx, max)
defer cancel()
err := c.vm.WaitForPowerState(timeout, state)
if err != nil {
return timeout.Err() != nil, err
}
return false, nil
}
func (c *containerBase) waitForSession(ctx context.Context, id string) error {
defer trace.End(trace.Begin(id, ctx))
// guestinfo key that we want to wait for
key := extraconfig.CalculateKeys(c.ExecConfig, fmt.Sprintf("Sessions.%s.Started", id), "")[0]
return c.waitFor(ctx, key)
}
func (c *containerBase) waitForExec(op trace.Operation, id string) error {
defer trace.End(trace.Begin(id, op))
// guestinfo key that we want to wait for
key := extraconfig.CalculateKeys(c.ExecConfig, fmt.Sprintf("Execs.%s.Started", id), "")[0]
return c.waitFor(op, key)
}
func (c *containerBase) waitFor(ctx context.Context, key string) error {
detail, err := c.vm.WaitForKeyInExtraConfig(ctx, key)
if err != nil {
return fmt.Errorf("unable to wait for process launch status: %s", err)
}
if detail != "true" {
return fmt.Errorf("%s", detail)
}
return nil
}
// isInvalidPowerStateError verifies if an error is the InvalidPowerStateError
func isInvalidPowerStateError(err error) bool {
if soap.IsSoapFault(err) {
_, ok := soap.ToSoapFault(err).VimFault().(types.InvalidPowerState)
return ok
}
return false
}

View File

@@ -0,0 +1,255 @@
// 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.
package exec
import (
"context"
"errors"
"fmt"
"time"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/vic/pkg/retry"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/tasks"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// Commit executes the requires steps on the handle
func Commit(op trace.Operation, sess *session.Session, h *Handle, waitTime *int32) error {
defer trace.End(trace.Begin(h.ExecConfig.ID, op))
c := Containers.Container(h.ExecConfig.ID)
creation := h.vm == nil
if creation {
if h.Spec == nil {
return fmt.Errorf("a spec must be provided for create operations")
}
if sess == nil {
// session must not be nil
return fmt.Errorf("no session provided for create operations")
}
// the only permissible operation is to create a VM
if h.Spec == nil {
return fmt.Errorf("only create operations can be committed without an existing VM")
}
if c != nil {
return fmt.Errorf("a container already exists in the cache with this ID")
}
var res *types.TaskInfo
var err error
if sess.IsVC() && Config.VirtualApp.ResourcePool != nil {
// Create the vm
res, err = tasks.WaitForResult(op, func(op context.Context) (tasks.Task, error) {
return Config.VirtualApp.CreateChildVM(op, *h.Spec.Spec(), nil)
})
} else {
// Create the vm
res, err = tasks.WaitForResult(op, func(op context.Context) (tasks.Task, error) {
return sess.VMFolder.CreateVM(op, *h.Spec.Spec(), Config.ResourcePool, nil)
})
}
if err != nil {
op.Errorf("An error occurred while waiting for a creation operation to complete. Spec was %+v", *h.Spec.Spec())
return err
}
h.vm = vm.NewVirtualMachine(op, sess, res.Result.(types.ManagedObjectReference))
h.vm.DisableDestroy(op)
c = newContainer(&h.containerBase)
Containers.Put(c)
// inform of creation irrespective of remaining operations
publishContainerEvent(op, c.ExecConfig.ID, time.Now().UTC(), events.ContainerCreated)
// clear the spec as we've acted on it - this prevents a reconfigure from occurring in follow-on
// processing
h.Spec = nil
}
// if we're stopping the VM, do so before the reconfigure to preserve the extraconfig
if h.TargetState() == StateStopped {
if h.Runtime == nil {
op.Warnf("Commit called with incomplete runtime state for %s", h.ExecConfig.ID)
}
if h.Runtime != nil && h.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOff {
op.Infof("Dropping duplicate power off operation for %s", h.ExecConfig.ID)
} else {
// stop the container
if err := c.stop(op, waitTime); err != nil {
return err
}
// we must refresh now to get the new ChangeVersion - this is used to gate on powerstate in the reconfigure
// because we cannot set the ExtraConfig if the VM is powered on. There is still a race here unfortunately because
// tasks don't appear to contain the new ChangeVersion
h.refresh(op)
// inform of state change irrespective of remaining operations - but allow remaining operations to complete first
// to avoid data race on container config
defer publishContainerEvent(op, h.ExecConfig.ID, time.Now().UTC(), events.ContainerStopped)
}
}
// reconfigure operation
if h.Spec != nil {
if h.Runtime == nil {
op.Errorf("Refusing to perform reconfigure operation with incomplete runtime state for %s", h.ExecConfig.ID)
} else {
// ensure that our logic based on Runtime state remains valid
// NOTE: this inline refresh can be removed when switching away from guestinfo where we have non-persistence issues
// when updating ExtraConfig via the API with a powered on VM - we therefore have to be absolutely certain about the
// power state to decide if we can continue without nilifying extraconfig
//
// For the power off path this depends on handle.refresh() having been called to update the ChangeVersion
s := h.Spec.Spec()
op.Infof("Reconfigure: attempting update to %s with change version %q (%s)", h.ExecConfig.ID, s.ChangeVersion, h.Runtime.PowerState)
// nilify ExtraConfig if container configuration is migrated
// in this case, VCH and container are in different version. Migrated configuration cannot be written back to old container, to avoid data loss in old version's container
if h.Migrated {
op.Debugf("Reconfigure: dropping extraconfig as configuration of container %s is migrated", h.ExecConfig.ID)
s.ExtraConfig = nil
}
// address the race between power operation and refresh of config (and therefore ChangeVersion) in StateStopped block above
if s.ExtraConfig != nil && h.TargetState() == StateStopped && h.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOff {
detail := fmt.Sprintf("Reconfigure: collision of concurrent operations - expected power state poweredOff, found %s", h.Runtime.PowerState)
op.Warnf(detail)
// log out current vm power state and runtime power state got from refresh, to see if there is anything mismatch,
// cause in issue #6127, we see the runtime power state is not updated even after 1 minute
ps, _ := h.vm.PowerState(op)
op.Debugf("Container %s power state: %s, runtime power state: %s", h.ExecConfig.ID, ps, h.Runtime.PowerState)
// this should cause a second attempt at the power op. This could result repeated contention that fails to resolve, but the randomness in the backoff and the tight timing
// to hit this scenario should mean it will resolve in a reasonable timeframe.
return ConcurrentAccessError{errors.New(detail)}
}
_, err := h.vm.WaitForResult(op, func(op context.Context) (tasks.Task, error) {
return h.vm.Reconfigure(op, *s)
})
if err != nil {
op.Errorf("Reconfigure: failed update to %s with change version %s: %+v", h.ExecConfig.ID, s.ChangeVersion, err)
// Check whether we get ConcurrentAccess and wrap it if needed
if f, ok := err.(types.HasFault); ok {
switch f.Fault().(type) {
case *types.ConcurrentAccess:
op.Errorf("Reconfigure: failed update to %s due to ConcurrentAccess, our change version %s", h.ExecConfig.ID, s.ChangeVersion)
return ConcurrentAccessError{err}
}
}
return err
}
op.Infof("Reconfigure: committed update to %s with change version: %s", h.ExecConfig.ID, s.ChangeVersion)
// trigger a configuration reload in the container if needed
err = reloadConfig(op, h, c)
if err != nil {
return err
}
}
}
// best effort update of container cache using committed state - this will not reflect the power on below, however
// this is primarily for updating ExtraConfig state.
if !creation {
defer c.RefreshFromHandle(op, h)
}
if h.TargetState() == StateRunning {
if h.Runtime != nil && h.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOn {
op.Infof("Dropping duplicate power on operation for %s", h.ExecConfig.ID)
return nil
}
if h.Runtime == nil && !creation {
op.Warnf("Commit called with incomplete runtime state for %s", h.ExecConfig.ID)
}
// start the container
if err := c.start(op); err != nil {
// We observed that PowerOn_Task could get stuck on VC time to time even though the VM was starting fine on the host ESXi.
// Eventually the task was getting timed out (After 20 min.) and that was setting the container state back to Stopped.
// During that time VC was not generating any other event so the persona listener was getting nothing.
// This new event is for signaling the eventmonitor so that it can autoremove the container after this failure.
publishContainerEvent(op, h.ExecConfig.ID, time.Now().UTC(), events.ContainerFailed)
return err
}
// publish started event
publishContainerEvent(op, h.ExecConfig.ID, time.Now().UTC(), events.ContainerStarted)
}
return nil
}
// HELPER FUNCTIONS BELOW
// reloadConfig is responsible for triggering a guest_reconfigure in order to perform an operation on a running cVM
// this function needs to be resilient to intermittent config errors and task errors, but will pass concurrent
// modification issues back immediately.
func reloadConfig(op trace.Operation, h *Handle, c *Container) error {
op.Infof("Attempting to perform a guest reconfigure operation on (%s)", h.ExecConfig.ID)
retryFunc := func() error {
if h.reload && h.Runtime != nil && h.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOn {
err := c.ReloadConfig(op)
if err != nil {
op.Debugf("Error occurred during an attempt to reload the container config for an exec operation: (%s)", err)
// we will request the powerstate directly(this could be very costly without the vmomi gateway)
state, err := c.vm.PowerState(op)
if err != nil && state == types.VirtualMachinePowerStatePoweredOff {
// TODO: probably should make this error a specific type such as PowerOffDuringExecError( or a better name ofcourse)
return fmt.Errorf("container(%s) was powered down during the requested operation.", h.ExecConfig.ID)
}
return err
}
return nil
}
// nothing to be done.
return nil
}
err := retry.Do(retryFunc, isIntermittentFailure)
if err != nil {
op.Debugf("Failed an exec operation with err: %s", err)
return err
}
return nil
}
// TODO: refactor later, I need to test this and we need to unify the Task package and the retry Package(make task use retry imo)
// right now this just looks silly...
func isIntermittentFailure(err error) bool {
// in the future commit should be using the trace.operation for these calls and this function can act as a passthrough.
op := trace.NewOperation(context.TODO(), "")
return tasks.IsRetryError(op, err)
}

View File

@@ -0,0 +1,56 @@
// Copyright 2016 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.
package exec
import (
"net/url"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/config"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/portlayer/event"
)
var Config Configuration
// Configuration is a slice of the VCH config that is relevant to the exec part of the port layer
type Configuration struct {
// Turn on debug logging
DebugLevel int `vic:"0.1" scope:"read-only" key:"init/diagnostics/debug"`
SysLogConfig *executor.SysLogConfig `vic:"0.1" scope:"read-only" key:"init/diagnostics/syslog"`
// Port Layer - exec
config.Container `vic:"0.1" scope:"read-only" key:"container"`
// Resource pool is the working version of the compute resource config
ResourcePool *object.ResourcePool
// Parent resource will be a VirtualApp on VC
VirtualApp *object.VirtualApp
// For now throw the Event Manager here
EventManager event.EventManager
// Information about the VCH resource pool and about the real host that we want
// tol retrieve just once.
VCHMhz int64
VCHMemoryLimit int64
HostOS string
HostOSVersion string
HostProductName string //'VMware vCenter Server' or 'VMare ESXi'
// Datastore URLs for image stores - the top layer is [0], the bottom layer is [len-1]
ImageStores []url.URL `vic:"0.1" scope:"read-only" key:"storage/image_stores"`
}

View File

@@ -0,0 +1,896 @@
// Copyright 2016-2018 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.
package exec
import (
"bytes"
"context"
"fmt"
"io"
"path"
"strings"
"sync"
"syscall"
"time"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/iolog"
"github.com/vmware/vic/lib/portlayer/event/events"
stateevents "github.com/vmware/vic/lib/portlayer/event/events/vsphere"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/uid"
"github.com/vmware/vic/pkg/vsphere/disk"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/sys"
"github.com/vmware/vic/pkg/vsphere/tasks"
"github.com/vmware/vic/pkg/vsphere/vm"
log "github.com/Sirupsen/logrus"
"github.com/google/uuid"
)
type State int
const (
StateUnknown State = iota
StateStarting
StateRunning
StateStopping
StateStopped
StateSuspending
StateSuspended
StateCreated
StateCreating
StateRemoving
StateRemoved
containerLogName = "output.log"
vmNotSuspendedKey = "msg.suspend.powerOff.notsuspended"
vmPoweringOffKey = "msg.rpc.error.poweringoff"
)
func (s State) String() string {
switch s {
case StateCreated:
return "Created"
case StateStarting:
return "Starting"
case StateRunning:
return "Running"
case StateRemoving:
return "Removing"
case StateRemoved:
return "Removed"
case StateStopping:
return "Stopping"
case StateStopped:
return "Stopped"
case StateUnknown:
return "Unknown"
}
return ""
}
// NotFoundError is returned when a types.ManagedObjectNotFound is returned from a vmomi call
type NotFoundError struct {
err error
}
func (r NotFoundError) Error() string {
return "VM has either been deleted or has not been fully created"
}
func IsNotFoundError(err error) bool {
if soap.IsSoapFault(err) {
fault := soap.ToSoapFault(err).VimFault()
if _, ok := fault.(types.ManagedObjectNotFound); ok {
return true
}
}
return false
}
// RemovePowerError is returned when attempting to remove a containerVM that is powered on
type RemovePowerError struct {
err error
}
func (r RemovePowerError) Error() string {
return r.err.Error()
}
// ConcurrentAccessError is returned when concurrent calls tries to modify same object
type ConcurrentAccessError struct {
err error
}
func (r ConcurrentAccessError) Error() string {
return r.err.Error()
}
func IsConcurrentAccessError(err error) bool {
_, ok := err.(ConcurrentAccessError)
return ok
}
type DevicesInUseError struct {
Devices []string
}
func (e DevicesInUseError) Error() string {
return fmt.Sprintf("device %s in use", strings.Join(e.Devices, ","))
}
// Container is used to return data about a container during inspection calls
// It is a copy rather than a live reflection and does not require locking
type ContainerInfo struct {
containerBase
state State
// Size of the leaf (unused)
VMUnsharedDisk int64
}
// Container is used for an entry in the container cache - this is a "live" representation
// of containers in the infrastructure.
// DANGEROUS USAGE CONSTRAINTS:
// None of the containerBase fields should be partially updated - consider them immutable once they're
// part of a cache entry
// i.e. Do not make changes in containerBase.ExecConfig - only swap, under lock, the pointer for a
// completely new ExecConfig.
// This constraint allows us to avoid deep copying those structs every time a container is inspected
type Container struct {
m sync.Mutex
ContainerInfo
logFollowers []io.Closer
newStateEvents map[State]chan struct{}
}
// newContainer constructs a Container suitable for adding to the cache
// it's state is set from the Runtime.PowerState field, or StateCreated if that is not
// viable
// This copies (shallow) the containerBase that's provided
func newContainer(base *containerBase) *Container {
c := &Container{
ContainerInfo: ContainerInfo{
containerBase: *base,
state: StateCreated,
},
newStateEvents: make(map[State]chan struct{}),
}
// if this is a creation path, then Runtime will be nil
if base.Runtime != nil {
// set state
switch base.Runtime.PowerState {
case types.VirtualMachinePowerStatePoweredOn:
// the containerVM is poweredOn, so set state to starting
// then check to see if a start was successful
c.state = StateStarting
// If any sessions successfully started then set to running
for _, s := range base.ExecConfig.Sessions {
if s.Started != "" {
c.state = StateRunning
break
}
}
case types.VirtualMachinePowerStatePoweredOff:
// check if any of the sessions was started
for _, s := range base.ExecConfig.Sessions {
if s.Started != "" {
c.state = StateStopped
break
}
}
case types.VirtualMachinePowerStateSuspended:
c.state = StateSuspended
log.Warnf("container VM %s: invalid power state %s", base.vm.Reference(), base.Runtime.PowerState)
}
}
return c
}
func GetContainer(ctx context.Context, id uid.UID) *Handle {
// get from the cache
container := Containers.Container(id.String())
if container != nil {
return container.NewHandle(ctx)
}
return nil
}
func (c *ContainerInfo) String() string {
return c.ExecConfig.ID
}
// State returns the state at the time the ContainerInfo object was created
func (c *ContainerInfo) State() State {
return c.state
}
func (c *Container) String() string {
return c.ExecConfig.ID
}
// Info returns a copy of the public container configuration that
// is consistent and copied under lock
func (c *Container) Info() *ContainerInfo {
c.m.Lock()
defer c.m.Unlock()
info := c.ContainerInfo
return &info
}
// CurrentState returns current state.
func (c *Container) CurrentState() State {
c.m.Lock()
defer c.m.Unlock()
return c.state
}
// SetState changes container state.
func (c *Container) SetState(op trace.Operation, s State) State {
c.m.Lock()
defer c.m.Unlock()
return c.updateState(op, s)
}
func (c *Container) updateState(op trace.Operation, s State) State {
op.Debugf("Updating container %s state: %s->%s", c, c.state, s)
prevState := c.state
if s != c.state {
c.state = s
if ch, ok := c.newStateEvents[s]; ok {
delete(c.newStateEvents, s)
close(ch)
}
}
return prevState
}
// transitionState changes the container state to finalState if the current state is initialState
// and returns an error otherwise.
func (c *Container) transitionState(op trace.Operation, initialState, finalState State) error {
c.m.Lock()
defer c.m.Unlock()
if c.state == initialState {
c.state = finalState
op.Debugf("Set container %s state: %s->%s", c, initialState, finalState)
return nil
}
return fmt.Errorf("container state is %s and was not changed to %s", c.state, finalState)
}
var closedEventChannel = func() <-chan struct{} {
a := make(chan struct{})
close(a)
return a
}()
// WaitForState subscribes a caller to an event returning
// a channel that will be closed when an expected state is set.
// If expected state is already set the caller will receive a closed channel immediately.
func (c *Container) WaitForState(s State) <-chan struct{} {
c.m.Lock()
defer c.m.Unlock()
if s == c.state {
return closedEventChannel
}
if ch, ok := c.newStateEvents[s]; ok {
return ch
}
eventChan := make(chan struct{})
c.newStateEvents[s] = eventChan
return eventChan
}
func (c *Container) NewHandle(ctx context.Context) *Handle {
// Call property collector to fill the data
if c.vm != nil {
op := trace.FromContext(ctx, "NewHandle")
// FIXME: this should be calling the cache to decide if a refresh is needed
if err := c.Refresh(op); err != nil {
op.Errorf("refreshing container %s failed: %s", c, err)
return nil // nil indicates error
}
}
// return a handle that represents zero changes over the current configuration
// for this container
return newHandle(c)
}
// Refresh updates config and runtime info, holding a lock only while swapping
// the new data for the old
func (c *Container) Refresh(op trace.Operation) error {
c.m.Lock()
defer c.m.Unlock()
if err := c.refresh(op); err != nil {
return err
}
// conditionally sync state (see issue 4872, 6372)
event := stateevents.NewStateEvent(op, c.containerBase.Runtime.PowerState, c.VMReference())
state := eventedState(op, event, c.state)
// trigger internal event publishing if c.state -> state is a transition we care about
// this will update container state and trigger follow up port layer events as needed
c.onEvent(op, state, event)
return nil
}
func (c *Container) refresh(op trace.Operation) error {
return c.containerBase.refresh(op)
}
// RefreshFromHandle updates config and runtime info, holding a lock only while swapping
// the new data for the old
func (c *Container) RefreshFromHandle(op trace.Operation, h *Handle) {
c.m.Lock()
defer c.m.Unlock()
if c.Config != nil && (h.Config == nil || h.Config.ChangeVersion != c.Config.ChangeVersion) {
op.Warnf("container and handle ChangeVersions do not match for %s: %s != %s", c, c.Config.ChangeVersion, h.Config.ChangeVersion)
return
}
// power off doesn't necessarily cause a change version increment and bug1898149 occasionally impacts power on
if c.Runtime != nil && (h.Runtime == nil || h.Runtime.PowerState != c.Runtime.PowerState) {
op.Warnf("container and handle PowerStates do not match: %s != %s", c.Runtime.PowerState, h.Runtime.PowerState)
return
}
// copy over the new state
c.containerBase = h.containerBase
if c.Config != nil {
op.Debugf("Update: updated change version from handle: %s", c.Config.ChangeVersion)
}
}
// Start starts a container vm with the given params
func (c *Container) start(op trace.Operation) error {
defer trace.End(trace.Begin(c.ExecConfig.ID, op))
if c.vm == nil {
return fmt.Errorf("vm not set")
}
// Set state to Starting
c.SetState(op, StateStarting)
err := c.containerBase.start(op)
if err != nil {
// change state to stopped because start task failed
c.SetState(op, StateStopped)
// check if locked disk error
devices := disk.LockedDisks(err)
if len(devices) > 0 {
for i := range devices {
// get device id from datastore file path
// FIXME: find a reasonable way to get device ID from datastore path in exec
devices[i] = strings.TrimSuffix(path.Base(devices[i]), ".vmdk")
}
return DevicesInUseError{devices}
}
return err
}
// wait task to set started field to something
op, cancel := trace.WithTimeout(&op, constants.PropertyCollectorTimeout, "WaitForSession")
defer cancel()
err = c.waitForSession(op, c.ExecConfig.ID)
if err != nil {
// leave this in state starting - if it powers off then the event
// will cause transition to StateStopped which is likely our original state
// if the container was just taking a very long time it'll eventually
// become responsive.
// TODO: mechanism to trigger reinspection of long term transitional states
return err
}
// Transition the state to Running only if it's Starting.
// The current state is already Stopped if the container's process has exited or
// a poweredoff event has been processed.
if err = c.transitionState(op, StateStarting, StateRunning); err != nil {
op.Debugf(err.Error())
}
return nil
}
func (c *Container) stop(op trace.Operation, waitTime *int32) error {
defer trace.End(trace.Begin(c.ExecConfig.ID, op))
defer c.onStop()
// get existing state and set to stopping
// if there's a failure we'll revert to existing
finalState := c.SetState(op, StateStopping)
err := c.containerBase.stop(op, waitTime)
if err != nil {
// we've got no idea what state the container is in at this point
// running is an _optimistic_ statement
// If the current state is Stopping, revert it to the old state.
if stateErr := c.transitionState(op, StateStopping, finalState); stateErr != nil {
op.Debugf(stateErr.Error())
}
return err
}
// Transition the state to Stopped only if it's Stopping.
if err = c.transitionState(op, StateStopping, StateStopped); err != nil {
op.Debugf(err.Error())
}
return nil
}
func (c *Container) Signal(op trace.Operation, num int64) error {
defer trace.End(trace.Begin(c.ExecConfig.ID, op))
if c.vm == nil {
return fmt.Errorf("vm not set")
}
if num == int64(syscall.SIGKILL) {
return c.containerBase.kill(op)
}
return c.startGuestProgram(op, "kill", fmt.Sprintf("%d", num))
}
func (c *Container) onStop() {
lf := c.logFollowers
c.logFollowers = nil
log.Debugf("Container(%s) closing %d log followers", c, len(lf))
for _, l := range lf {
// #nosec: Errors unhandled.
_ = l.Close()
}
}
func (c *Container) LogReader(op trace.Operation, tail int, follow bool, since int64) (io.ReadCloser, error) {
defer trace.End(trace.Begin(c.ExecConfig.ID, op))
c.m.Lock()
defer c.m.Unlock()
if c.vm == nil {
return nil, fmt.Errorf("vm not set")
}
url, err := c.vm.VMPathNameAsURL(op)
if err != nil {
return nil, err
}
name := fmt.Sprintf("%s/%s", url.Path, containerLogName)
var via string
if c.state == StateRunning && c.vm.IsVC() {
// #nosec: Errors unhandled.
hosts, _ := c.vm.Datastore.AttachedHosts(op)
if len(hosts) > 1 {
// In this case, we need download from the VM host as it owns the file lock
// #nosec: Errors unhandled.
h, _ := c.vm.HostSystem(op)
if h != nil {
// get a context that embeds the host as a value
ctx := c.vm.Datastore.HostContext(op, h)
// revert the govmomi returned context to the previous op
// the op was preserved as a value in the context
op = trace.FromContext(ctx, "LogReader")
via = fmt.Sprintf(" via %s", h.Reference())
}
}
}
op.Infof("pulling %s%s", name, via)
file, err := c.vm.Datastore.Open(op, name)
if err != nil {
return nil, err
}
if since > 0 {
err = file.TailFunc(tail, func(line int, message string) bool {
if tail <= line && tail != -1 {
return false
}
buf := bytes.NewBufferString(message)
entry, err := iolog.ParseLogEntry(buf)
if err != nil {
op.Errorf("Error parsing log entry: %s", err.Error())
return false
}
if entry.Timestamp.Unix() <= since {
return false
}
return true
})
} else if tail >= 0 {
err = file.Tail(tail)
if err != nil {
return nil, err
}
}
if follow && c.state == StateRunning {
follower := file.Follow(time.Second)
c.logFollowers = append(c.logFollowers, follower)
return follower, nil
}
return file, nil
}
// Remove removes a containerVM after detaching the disks
func (c *Container) Remove(op trace.Operation, sess *session.Session) error {
// op := trace.FromContext(ctx, "Remove")
defer trace.End(trace.Begin(c.ExecConfig.ID, op))
c.m.Lock()
defer c.m.Unlock()
if c.vm == nil {
return NotFoundError{}
}
// check state first
if c.state == StateRunning {
return RemovePowerError{fmt.Errorf("Container %s is powered on", c)}
}
// get existing state and set to removing
// if there's a failure we'll revert to existing
existingState := c.updateState(op, StateRemoving)
// get the folder the VM is in
url, err := c.vm.VMPathNameAsURL(op)
if err != nil {
// handle the out-of-band removal case
if IsNotFoundError(err) {
Containers.Remove(c.ExecConfig.ID)
return NotFoundError{}
}
op.Errorf("Failed to get datastore path for %s: %s", c, err)
c.updateState(op, existingState)
return err
}
ds, err := sess.Finder.Datastore(op, url.Host)
if err != nil {
return err
}
// enable Destroy
c.vm.EnableDestroy(op)
concurrent := false
// if DeleteExceptDisks succeeds on VC, it leaves the VM orphan so we need to call Unregister
// if DeleteExceptDisks succeeds on ESXi, no further action needed
// if DeleteExceptDisks fails, we should call Unregister and only return an error if that fails too
// Unregister sometimes can fail with ManagedObjectNotFound so we ignore it
_, err = c.vm.WaitForResult(op, func(op context.Context) (tasks.Task, error) {
return c.vm.DeleteExceptDisks(op)
})
if err != nil {
f, ok := err.(types.HasFault)
if !ok {
op.Warnf("DeleteExceptDisks failed with non-fault error %s for %s.", err, c)
c.updateState(op, existingState)
return err
}
switch f.Fault().(type) {
case *types.InvalidState:
op.Warnf("container VM %s is in invalid state, unregistering", c)
if err := c.vm.Unregister(op); err != nil {
op.Errorf("Error while attempting to unregister container VM %s: %s", c, err)
return err
}
case *types.ConcurrentAccess:
// We are getting ConcurrentAccess errors from DeleteExceptDisks - even though we don't set ChangeVersion in that path
// We are ignoring the error because in reality the operation finishes successfully.
op.Warnf("DeleteExceptDisks failed with ConcurrentAccess error for %s. Ignoring it.", c)
concurrent = true
default:
op.Debugf("Unhandled fault while attempting to destroy vm %s: %#v", c, f.Fault())
c.updateState(op, existingState)
return err
}
}
if concurrent && c.vm.IsVC() {
if err := c.vm.Unregister(op); err != nil {
if !IsNotFoundError(err) {
op.Errorf("Error while attempting to unregister container VM %s: %s", c, err)
return err
}
}
}
// remove from datastore
fm := ds.NewFileManager(sess.Datacenter, true)
if err = fm.Delete(op, url.Path); err != nil {
// at this phase error doesn't matter. Just log it.
op.Debugf("Failed to delete %s, %s for %s", url, err, c)
}
//remove container from cache
Containers.Remove(c.ExecConfig.ID)
publishContainerEvent(op, c.ExecConfig.ID, time.Now(), events.ContainerRemoved)
return nil
}
// eventedState will determine the target container
// state based on the current container state and the vsphere event
func eventedState(op trace.Operation, e events.Event, current State) State {
switch e.String() {
case events.ContainerPoweredOn:
// are we in the process of starting
if current != StateStarting {
return StateRunning
}
case events.ContainerPoweredOff:
// are we in the process of stopping or just created
if current != StateStopping && current != StateCreated {
return StateStopped
}
case events.ContainerSuspended:
// are we in the process of suspending
if current != StateSuspending {
return StateSuspended
}
case events.ContainerRemoved:
if current != StateRemoving {
return StateRemoved
}
}
return current
}
func (c *Container) OnEvent(e events.Event) {
op := trace.NewOperation(context.Background(), "OnEvent")
defer trace.End(trace.Begin(fmt.Sprintf("eventID(%s) received for event: %s", e.EventID(), e.String()), op))
c.m.Lock()
defer c.m.Unlock()
if c.vm == nil {
op.Warnf("Event(%s) received for %s but no VM found", e.EventID(), e.Reference())
return
}
newState := eventedState(op, e, c.state)
c.onEvent(op, newState, e)
}
// determine if the containerVM has started - this could pick up stale data in the started field for an out-of-band
// power change such as HA or user intervention where we have not had an opportunity to reset the entry.
func cleanStart(op trace.Operation, c *Container) bool {
if len(c.ExecConfig.Sessions) == 0 {
op.Warnf("Container %c has no sessions stored in in-memory config", c.ExecConfig.ID)
// if no sessions, then nothing to wait for
return true
}
for _, session := range c.ExecConfig.Sessions {
if session.Started != "true" {
return false
}
}
return true
}
// onEvent determines what needs to be done when receiving a state update. It filters duplicate state transitions
// and publishes container events as needed in addition to performing necessary manipulations.
// newState - this is the new state determined by eventedState
// e - the source event used to derive the new State and reason for the transition
func (c *Container) onEvent(op trace.Operation, newState State, e events.Event) {
// does local data report full start
started := cleanStart(op, c)
// do we need a refresh
refresh := e.String() == events.ContainerRelocated
// if it's a state event we've already done a refresh to end up here and dont need another
_, stateEvent := e.(*stateevents.StateEvent)
// the event we're going to publish - may be overridden/transformed by more context aware logic below
// the incoming event is from the very coarse vSphere events
publishEventType := e.String()
if !stateEvent {
if (newState == StateStarting && !started) || newState == StateStopping {
// inherently transient state. Starting with started == true is just accounting that will
// happen below and doesn't need a refresh.
refresh = true
}
if newState == StateRunning && !started {
// if we cannot confirm fully initialized
refresh = true
}
}
if refresh {
op, cancel := trace.WithTimeout(&op, constants.PropertyCollectorTimeout, "vSphere event triggered refresh")
defer cancel()
if err := c.refresh(op); err != nil {
op.Errorf("Container(%s) event driven update failed: %s", c, err)
}
}
started = cleanStart(op, c)
// it doesn't matter how the event was translated, if we're not fully started then we're starting
// if we are then we're running. Only exception is that we don't transition from Running->Starting
if newState == StateRunning && !started && c.state != StateRunning {
newState = StateStarting
}
if newState == StateStarting && started {
newState = StateRunning
}
if newState != c.state {
switch newState {
case StateRunning:
// transform the PoweredOn event into Started
publishEventType = events.ContainerStarted
fallthrough
case StateStarting,
StateStopping,
StateStopped,
StateSuspended:
c.updateState(op, newState)
if newState == StateStopped {
c.onStop()
}
case StateRemoved:
if c.vm != nil && c.vm.IsFixing() {
// is fixing vm, which will be registered back soon, so do not remove from containers cache
op.Debugf("Container(%s) %s is being fixed - %s event ignored", c, newState)
// Received remove event triggered by unregister VM operation - leave
// fixing state now. In a loaded environment, the remove event may be
// received after vm.fixVM() has returned, at which point the container
// should still be in fixing state to avoid removing it from the cache below.
c.vm.LeaveFixingState()
// since we're leaving the container in cache, just return w/o allowing
// a container event to be propogated to subscribers
return
}
op.Debugf("Container(%s) %s via event activity", c, newState)
// if we are here the containerVM has been removed from vSphere, so lets remove it
// from the portLayer cache
Containers.Remove(c.ExecConfig.ID)
c.vm = nil
default:
return
}
op.Debugf("Container (%s) publishing event (state=%s, event=%s) from event %s", c, newState, publishEventType, e.String())
// regardless of state update success or failure publish the container event
publishContainerEvent(op, c.ExecConfig.ID, e.Created(), publishEventType)
return
}
}
// get the containerVMs from infrastructure for this resource pool
func infraContainers(ctx context.Context, sess *session.Session) ([]*Container, error) {
defer trace.End(trace.Begin(""))
var rp mo.ResourcePool
// popluate the vm property of the vch resource pool
if err := Config.ResourcePool.Properties(ctx, Config.ResourcePool.Reference(), []string{"vm"}, &rp); err != nil {
name := Config.ResourcePool.Name()
log.Errorf("List failed to get %s resource pool child vms: %s", name, err)
return nil, err
}
vms, err := populateVMAttributes(ctx, sess, rp.Vm)
if err != nil {
return nil, err
}
return convertInfraContainers(ctx, sess, vms), nil
}
func instanceUUID(id string) (string, error) {
// generate VM instance uuid, which will be used to query back VM
u, err := sys.UUID()
if err != nil {
return "", err
}
namespace, err := uuid.Parse(u)
if err != nil {
return "", errors.Errorf("unable to parse VCH uuid: %s", err)
}
return uuid.NewSHA1(namespace, []byte(id)).String(), nil
}
// populate the vm attributes for the specified morefs
func populateVMAttributes(ctx context.Context, sess *session.Session, refs []types.ManagedObjectReference) ([]mo.VirtualMachine, error) {
defer trace.End(trace.Begin(fmt.Sprintf("populating %d refs", len(refs))))
var vms []mo.VirtualMachine
// current attributes we care about
attrib := []string{"config", "runtime.powerState", "summary"}
// populate the vm properties
err := sess.Retrieve(ctx, refs, attrib, &vms)
return vms, err
}
// convert the infra containers to a container object
func convertInfraContainers(ctx context.Context, sess *session.Session, vms []mo.VirtualMachine) []*Container {
defer trace.End(trace.Begin(fmt.Sprintf("converting %d containers", len(vms))))
var cons []*Container
for _, v := range vms {
vm := vm.NewVirtualMachine(ctx, sess, v.Reference())
base := newBase(vm, v.Config, &v.Runtime)
c := newContainer(base)
id := uid.Parse(c.ExecConfig.ID)
if id == uid.NilUID {
log.Warnf("skipping converting container VM %s: could not parse id", v.Reference())
continue
}
if v.Summary.Storage != nil {
c.VMUnsharedDisk = v.Summary.Storage.Unshared
}
cons = append(cons, c)
}
return cons
}

View File

@@ -0,0 +1,138 @@
// Copyright 2016-2018 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.
package exec
import (
"sync"
"context"
"github.com/vmware/vic/pkg/uid"
"github.com/vmware/vic/pkg/vsphere/session"
)
/*
* ContainerCache will provide an in-memory cache of containerVMs. It will
* be refreshed on portlayer start and updated via container lifecycle
* operations (start, stop, rm) and well as in response to infrastructure
* events
*/
type containerCache struct {
m sync.RWMutex
// cache by container id
cache map[string]*Container
}
var Containers *containerCache
func NewContainerCache() {
// cache by the container ID and the vsphere
// managed object reference
Containers = &containerCache{
cache: make(map[string]*Container),
}
}
func (conCache *containerCache) Container(idOrRef string) *Container {
conCache.m.RLock()
defer conCache.m.RUnlock()
// find by id or moref
return conCache.cache[idOrRef]
}
func (conCache *containerCache) Containers(states []State) []*Container {
conCache.m.RLock()
defer conCache.m.RUnlock()
// cache contains 2 items for each container
capacity := len(conCache.cache) / 2
containers := make([]*Container, 0, capacity)
for id, con := range conCache.cache {
// is the key a proper ID?
if !isContainerID(id) {
continue
}
// no state filtering
if len(states) == 0 {
containers = append(containers, con)
continue
}
// filter by container state
// DO NOT use container.CurrentState as that can
// cause cache deadlocks
for _, state := range states {
if state == con.State() {
containers = append(containers, con)
}
}
}
return containers
}
// puts a container in the cache and will overwrite an existing container
func (conCache *containerCache) Put(container *Container) {
// only add containers w/backing VMs
if container.vm == nil {
return
}
conCache.m.Lock()
defer conCache.m.Unlock()
conCache.put(container)
}
func (conCache *containerCache) put(container *Container) {
// add pointer to cache by container ID
conCache.cache[container.ExecConfig.ID] = container
conCache.cache[container.vm.Reference().String()] = container
}
func (conCache *containerCache) Remove(idOrRef string) {
conCache.m.Lock()
defer conCache.m.Unlock()
// find by id
container := conCache.cache[idOrRef]
if container != nil {
delete(conCache.cache, container.ExecConfig.ID)
delete(conCache.cache, container.vm.Reference().String())
}
}
func (conCache *containerCache) sync(ctx context.Context, sess *session.Session) error {
conCache.m.Lock()
defer conCache.m.Unlock()
cons, err := infraContainers(ctx, sess)
if err != nil {
return err
}
conCache.cache = make(map[string]*Container)
for _, c := range cons {
conCache.put(c)
}
return nil
}
func isContainerID(id string) bool {
return uid.Parse(id) != uid.NilUID
}

View File

@@ -0,0 +1,79 @@
// Copyright 2016 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.
package exec
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/pkg/uid"
"github.com/vmware/vic/pkg/vsphere/vm"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
)
func TestContainerCache(t *testing.T) {
NewContainerCache()
containerID := uid.New().String()
// create a new container
container := newTestContainer(containerID)
// put it in the cache
Containers.Put(container)
// still shouldn't have a container because there's no vm
assert.Equal(t, len(Containers.cache), 0)
// add a test vm
addTestVM(container)
// put in cache
Containers.Put(container)
// get all containers -- should have 1
assert.Equal(t, len(Containers.Containers(nil)), 1)
// Get specific container
cachedContainer := Containers.Container(containerID)
// did we find it?
assert.NotNil(t, cachedContainer)
// do we have this one in the cache?
assert.Equal(t, cachedContainer.ExecConfig.ID, containerID)
// remove the container
Containers.Remove(containerID)
assert.Equal(t, len(Containers.cache), 0)
// remove non-existent container
Containers.Remove("blahblah")
}
func TestIsContainerID(t *testing.T) {
validID := uid.New().String()
invalidID := "ABC-XZ_@"
assert.True(t, isContainerID(validID))
assert.False(t, isContainerID(invalidID))
}
// addTestVM will add a pseudo VM to the container
func addTestVM(container *Container) {
mo := types.ManagedObjectReference{Type: "vm", Value: "12"}
v := object.NewVirtualMachine(nil, mo)
container.vm = vm.NewVirtualMachineFromVM(nil, nil, v)
}
func newTestContainer(id string) *Container {
h := TestHandle(id)
return newContainer(&h.containerBase)
}

View File

@@ -0,0 +1,58 @@
// Copyright 2016 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.
package exec
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/pkg/uid"
)
func TestStateStringer(t *testing.T) {
c := &Container{
ContainerInfo: ContainerInfo{
state: StateRunning,
},
}
assert.Equal(t, "Running", c.state.String())
c.state = StateStopped
assert.Equal(t, "Stopped", c.state.String())
c.state = StateStopping
assert.Equal(t, "Stopping", c.state.String())
c.state = StateRemoving
assert.Equal(t, "Removing", c.state.String())
c.state = StateStarting
assert.Equal(t, "Starting", c.state.String())
c.state = StateCreated
assert.Equal(t, "Created", c.state.String())
}
func NewContainer(id uid.UID) *Handle {
con := &Container{
ContainerInfo: ContainerInfo{
state: StateCreating,
},
newStateEvents: make(map[State]chan struct{}),
}
h := newHandle(con)
h.ExecConfig.ID = id.String()
return h
}

156
vendor/github.com/vmware/vic/lib/portlayer/exec/exec.go generated vendored Normal file
View File

@@ -0,0 +1,156 @@
// 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.
package exec
import (
"fmt"
"sync"
"time"
"context"
log "github.com/Sirupsen/logrus"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/portlayer/event"
"github.com/vmware/vic/lib/portlayer/event/collector/vsphere"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
"github.com/vmware/vic/pkg/vsphere/session"
)
var (
initializer struct {
err error
once sync.Once
}
)
func Init(ctx context.Context, sess *session.Session, source extraconfig.DataSource, _ extraconfig.DataSink) error {
log.Info("Beginning initialization of portlayer exec component")
initializer.once.Do(func() {
var err error
defer func() {
if err != nil {
initializer.err = err
}
}()
f := find.NewFinder(sess.Vim25(), false)
extraconfig.Decode(source, &Config)
log.Debugf("Decoded VCH config for execution: %#v", Config)
ccount := len(Config.ComputeResources)
if ccount != 1 {
err = fmt.Errorf("expected singular compute resource element, found %d", ccount)
log.Error(err)
return
}
cr := Config.ComputeResources[0]
var r object.Reference
r, err = f.ObjectReference(ctx, cr)
if err != nil {
err = fmt.Errorf("could not get resource pool or virtual app reference from %q: %s", cr.String(), err)
log.Error(err)
return
}
switch o := r.(type) {
case *object.VirtualApp:
Config.VirtualApp = o
Config.ResourcePool = o.ResourcePool
case *object.ResourcePool:
Config.ResourcePool = o
default:
err = fmt.Errorf("could not get resource pool or virtual app from reference %q: object type is wrong", cr.String())
log.Error(err)
return
}
// we want to monitor the cluster, so create a vSphere Event Collector
// The cluster managed object will either be a proper vSphere Cluster or
// a specific host when standalone mode
ec := vsphere.NewCollector(sess.Vim25(), sess.Cluster.Reference().String())
// start the collection of vsphere events
err = ec.Start()
if err != nil {
err = fmt.Errorf("%s failed to start: %s", ec.Name(), err)
log.Error(err)
return
}
// instantiate the container cache now
NewContainerCache()
// create the event manager & register the existing collector
Config.EventManager = event.NewEventManager(ec)
// subscribe the exec layer to the event stream for Vm events
vmSub := Config.EventManager.Subscribe(events.NewEventType(vsphere.VMEvent{}).Topic(), "exec", func(e events.Event) {
if c := Containers.Container(e.Reference()); c != nil {
c.OnEvent(e)
}
})
// Grab the AboutInfo about our host environment
about := sess.Vim25().ServiceContent.About
vch := GetVCHstats(ctx)
Config.VCHMhz = vch.CPULimit
Config.VCHMemoryLimit = vch.MemoryLimit
Config.HostOS = about.OsType
Config.HostOSVersion = about.Version
Config.HostProductName = about.Name
log.Debugf("Host - OS (%s), version (%s), name (%s)", about.OsType, about.Version, about.Name)
log.Debugf("VCH limits - %d Mhz, %d MB", Config.VCHMhz, Config.VCHMemoryLimit)
// sync container cache
vmSub.Suspend(true)
defer vmSub.Resume()
log.Info("Syncing container cache")
if err = Containers.sync(ctx, sess); err != nil {
log.Errorf("Error encountered during container cache sync during init process: %s", err)
return
}
})
return initializer.err
}
// publishContainerEvent will publish a ContainerEvent to the vic event stream
func publishContainerEvent(op trace.Operation, id string, created time.Time, eventType string) {
if Config.EventManager == nil || eventType == "" {
return
}
ce := &events.ContainerEvent{
BaseEvent: &events.BaseEvent{
// containerEvents are a construct of vic, so lets set the
// ID equal to the operation that created the event
ID: op.ID(),
Ref: id,
CreatedTime: created,
Event: eventType,
Detail: fmt.Sprintf("Container %s %s", id, eventType),
},
}
Config.EventManager.Publish(ce)
}

View File

@@ -0,0 +1,180 @@
// Copyright 2016-2018 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.
package exec
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/portlayer/event"
"github.com/vmware/vic/lib/portlayer/event/collector/vsphere"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/vic/pkg/trace"
)
var containerEvents []events.Event
func TestEventedState(t *testing.T) {
id := "123439"
container := newTestContainer(id)
addTestVM(container)
op := trace.NewOperation(context.Background(), "test operations")
// poweredOn event
event := createVMEvent(container, StateRunning)
assert.EqualValues(t, StateStarting, eventedState(op, event, StateStarting))
assert.EqualValues(t, StateRunning, eventedState(op, event, StateRunning))
assert.EqualValues(t, StateRunning, eventedState(op, event, StateStopped))
assert.EqualValues(t, StateRunning, eventedState(op, event, StateSuspended))
// // powerOff event
event = createVMEvent(container, StateStopped)
assert.EqualValues(t, StateStopping, eventedState(op, event, StateStopping))
assert.EqualValues(t, StateStopped, eventedState(op, event, StateStopped))
assert.EqualValues(t, StateStopped, eventedState(op, event, StateRunning))
// // suspended event
event = createVMEvent(container, StateSuspended)
assert.EqualValues(t, StateSuspending, eventedState(op, event, StateSuspending))
assert.EqualValues(t, StateSuspended, eventedState(op, event, StateSuspended))
assert.EqualValues(t, StateSuspended, eventedState(op, event, StateRunning))
// removed event
event = createVMEvent(container, StateRemoved)
assert.EqualValues(t, StateRemoved, eventedState(op, event, StateRemoved))
assert.EqualValues(t, StateRemoved, eventedState(op, event, StateStopped))
assert.EqualValues(t, StateRemoving, eventedState(op, event, StateRemoving))
}
func TestPublishContainerEvent(t *testing.T) {
NewContainerCache()
containerEvents = make([]events.Event, 0)
Config = Configuration{}
mgr := event.NewEventManager()
Config.EventManager = mgr
mgr.Subscribe(events.NewEventType(events.ContainerEvent{}).Topic(), "testing", containerCallback)
op := trace.NewOperation(context.Background(), "test publish event operation")
// create new running container and place in cache
id := "123439"
container := newTestContainer(id)
addTestVM(container)
container.SetState(op, StateRunning)
Containers.Put(container)
publishContainerEvent(trace.NewOperation(context.Background(), "container"), id, time.Now().UTC(), events.ContainerPoweredOff)
time.Sleep(time.Millisecond * 30)
assert.Equal(t, 1, len(containerEvents))
assert.Equal(t, id, containerEvents[0].Reference())
assert.Equal(t, events.ContainerPoweredOff, containerEvents[0].String())
}
func TestVMRemovedEventCallback(t *testing.T) {
NewContainerCache()
containerEvents = make([]events.Event, 0)
Config = Configuration{}
mgr := event.NewEventManager()
Config.EventManager = mgr
// subscribe the exec layer to the event stream for VM events
mgr.Subscribe(events.NewEventType(&vsphere.VMEvent{}).Topic(), "testing", func(e events.Event) {
if c := Containers.Container(e.Reference()); c != nil {
c.OnEvent(e)
}
})
op := trace.NewOperation(context.Background(), "test removed event operation")
// create new running container and place in cache
id := "123439"
container := newTestContainer(id)
addTestVM(container)
container.SetState(op, StateRunning)
Containers.Put(container)
container.vm.EnterFixingState()
vmEvent := createVMEvent(container, StateRemoved)
mgr.Publish(vmEvent)
time.Sleep(time.Millisecond * 30)
assertMsg := "Container should have left fixing state in VM remove event handler"
assert.False(t, container.vm.IsFixing(), assertMsg)
mgr.Publish(vmEvent)
time.Sleep(time.Millisecond * 30)
assertMsg = "Container should be removed now that it has left fixing state"
assert.True(t, Containers.Container(id) == nil, assertMsg)
}
func containerCallback(ee events.Event) {
containerEvents = append(containerEvents, ee)
}
func createVMEvent(container *Container, state State) *vsphere.VMEvent {
// event to return
var vmEvent *vsphere.VMEvent
// basic event info
vme := types.Event{
CreatedTime: time.Now().UTC(),
Key: int32(101),
Vm: &types.VmEventArgument{
Vm: container.vm.Reference(),
},
}
switch state {
case StateSuspended:
// suspended
vmwEve := &types.VmSuspendedEvent{
VmEvent: types.VmEvent{
Event: vme,
},
}
vmEvent = vsphere.NewVMEvent(vmwEve)
case StateStopped:
// poweredOff
vmwEve := &types.VmPoweredOffEvent{
VmEvent: types.VmEvent{
Event: vme,
},
}
vmEvent = vsphere.NewVMEvent(vmwEve)
case StateRemoved:
// removed
vmwEve := &types.VmRemovedEvent{
VmEvent: types.VmEvent{
Event: vme,
},
}
vmEvent = vsphere.NewVMEvent(vmwEve)
default:
// poweredOn
vmwEve := &types.VmPoweredOnEvent{
VmEvent: types.VmEvent{
Event: vme,
},
}
vmEvent = vsphere.NewVMEvent(vmwEve)
}
return vmEvent
}

View File

@@ -0,0 +1,374 @@
// Copyright 2016-2018 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.
package exec
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"context"
log "github.com/Sirupsen/logrus"
"github.com/golang/groupcache/lru"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/lib/spec"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
"github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi"
"github.com/vmware/vic/pkg/vsphere/session"
)
// Resources describes the resource allocation for the containerVM
type Resources struct {
NumCPUs int64
MemoryMB int64
}
// ContainerCreateConfig defines the parameters for Create call
type ContainerCreateConfig struct {
Metadata *executor.ExecutorConfig
Resources Resources
}
var handles *lru.Cache
var handlesLock sync.Mutex
const (
handleLen = 16
lruSize = 1000
)
func init() {
handles = lru.New(lruSize)
}
type Handle struct {
// copy from container cache
containerBase
// The guest used to generate specific device types
Guest guest.Guest
// desired spec
Spec *spec.VirtualMachineConfigSpec
// desired changes to extraconfig
changes []types.BaseOptionValue
// desired state
targetState State
// should this change trigger a reload in the target container
reload bool
// allow for passing outside of the process
key string
}
func newHandleKey() string {
b := make([]byte, handleLen)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
panic(err) // This shouldn't happen
}
return hex.EncodeToString(b)
}
// Added solely to support testing - need a better way to do this
func TestHandle(id string) *Handle {
defer trace.End(trace.Begin("Handle.Create"))
h := newHandle(&Container{})
h.ExecConfig.ID = id
return h
}
// newHandle creates a handle for an existing container
// con must not be nil
func newHandle(con *Container) *Handle {
h := &Handle{
key: newHandleKey(),
targetState: StateUnknown,
containerBase: *newBase(con.vm, con.Config, con.Runtime),
// currently every operation has a spec, because even the power operations
// make changes to extraconfig for timestamps and session status
Spec: &spec.VirtualMachineConfigSpec{
VirtualMachineConfigSpec: &types.VirtualMachineConfigSpec{},
},
}
handlesLock.Lock()
defer handlesLock.Unlock()
handles.Add(h.key, h)
return h
}
func (h *Handle) TargetState() State {
return h.targetState
}
func (h *Handle) SetTargetState(s State) {
h.targetState = s
}
func (h *Handle) Reload() {
h.reload = true
}
// Rename updates the container name in ExecConfig as well as the vSphere display name
func (h *Handle) Rename(op trace.Operation, newName string) *Handle {
defer trace.End(trace.Begin(newName))
h.ExecConfig.Name = newName
s := &spec.VirtualMachineConfigSpecConfig{
ID: h.ExecConfig.ID,
Name: newName,
}
h.Spec.Spec().Name = util.DisplayName(op, s, Config.ContainerNameConvention)
return h
}
// GetHandle finds and returns the handle that is referred by key
func GetHandle(key string) *Handle {
handlesLock.Lock()
defer handlesLock.Unlock()
if h, ok := handles.Get(key); ok {
return h.(*Handle)
}
return nil
}
// HandleFromInterface returns the Handle
func HandleFromInterface(key interface{}) *Handle {
defer trace.End(trace.Begin(""))
if h, ok := key.(string); ok {
return GetHandle(h)
}
log.Errorf("Type assertion failed for %#+v", key)
return nil
}
// ReferenceFromHandle returns the reference of the given handle
func ReferenceFromHandle(handle interface{}) interface{} {
defer trace.End(trace.Begin(""))
if h, ok := handle.(*Handle); ok {
return h.String()
}
log.Errorf("Type assertion failed for %#+v", handle)
return nil
}
func (h *Handle) String() string {
return h.key
}
func (h *Handle) Commit(op trace.Operation, sess *session.Session, waitTime *int32) error {
cfg := make(map[string]string)
// Set timestamps based on target state
switch h.TargetState() {
case StateRunning:
for _, sc := range h.ExecConfig.Sessions {
sc.StartTime = time.Now().UTC().Unix()
sc.Started = ""
sc.ExitStatus = 0
}
case StateStopped:
for _, sc := range h.ExecConfig.Sessions {
sc.StopTime = time.Now().UTC().Unix()
sc.Started = ""
}
}
s := h.Spec.Spec()
if h.Config != nil {
s.ChangeVersion = h.Config.ChangeVersion
}
// if runtime is nil, should be fresh container create
var filter int
if h.Runtime == nil || h.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOff || h.TargetState() == StateStopped {
// any values set with VM powered off are inherently persistent
filter = ^extraconfig.NonPersistent
} else {
filter = extraconfig.NonPersistent | extraconfig.Hidden
}
extraconfig.Encode(extraconfig.ScopeFilterSink(uint(filter), extraconfig.MapSink(cfg)), h.ExecConfig)
// strip unmodified keys from the update
if h.Config != nil {
h.changes = append(s.ExtraConfig, vmomi.OptionValueUpdatesFromMap(h.Config.ExtraConfig, cfg)...)
} else {
h.changes = append(s.ExtraConfig, vmomi.OptionValueFromMap(cfg, true)...)
}
s.ExtraConfig = h.changes
if err := Commit(op, sess, h, waitTime); err != nil {
return err
}
h.Close()
return nil
}
// refresh is for internal use only - it's sole purpose at this time is to allow the stop path to update ChangeVersion
// and corresponding state before performing any associated reconfigure
func (h *Handle) refresh(op trace.Operation) {
// update Config and Runtime to reflect current state
h.containerBase.refresh(op)
// reapply extraconfig changes
s := h.Spec.Spec()
s.ExtraConfig = h.changes
s.ChangeVersion = h.Config.ChangeVersion
}
func (h *Handle) Close() {
handlesLock.Lock()
defer handlesLock.Unlock()
handles.Remove(h.key)
}
// Create returns a new handle that can be Committed to create a new container.
// At this time the config is *not* deep copied so should not be changed once passed
//
// TODO: either deep copy the configuration, or provide an alternative means of passing the data that
// avoids the need for the caller to unpack/repack the parameters
func Create(ctx context.Context, vmomiSession *session.Session, config *ContainerCreateConfig) (*Handle, error) {
op := trace.FromContext(ctx, "Handle.Create")
defer trace.End(trace.Begin(config.Metadata.Name, op))
h := &Handle{
key: newHandleKey(),
targetState: StateCreated,
containerBase: containerBase{
ExecConfig: config.Metadata,
},
}
// configure with debug
h.ExecConfig.Diagnostics.DebugLevel = Config.DebugLevel
h.ExecConfig.Diagnostics.SysLogConfig = Config.SysLogConfig
// Convert the management hostname to IP
ips, err := net.LookupIP(constants.ManagementHostName)
if err != nil {
log.Errorf("Unable to look up %s during create of %s: %s", constants.ManagementHostName, config.Metadata.ID, err)
return nil, err
}
if len(ips) == 0 {
log.Errorf("No IP found for %s during create of %s", constants.ManagementHostName, config.Metadata.ID)
return nil, fmt.Errorf("No IP found on %s", constants.ManagementHostName)
}
if len(ips) > 1 {
log.Errorf("Multiple IPs found for %s during create of %s: %v", constants.ManagementHostName, config.Metadata.ID, ips)
return nil, fmt.Errorf("Multiple IPs found on %s: %#v", constants.ManagementHostName, ips)
}
uuid, err := instanceUUID(config.Metadata.ID)
if err != nil {
detail := fmt.Sprintf("unable to get instance UUID: %s", err)
log.Error(detail)
return nil, errors.New(detail)
}
specconfig := &spec.VirtualMachineConfigSpecConfig{
NumCPUs: int32(config.Resources.NumCPUs),
MemoryMB: config.Resources.MemoryMB,
ID: config.Metadata.ID,
Name: config.Metadata.Name,
BiosUUID: uuid,
// TODO: make this toggle for pod or single based on number of images joined
BootMediaPath: Config.BootstrapImagePath,
VMPathName: fmt.Sprintf("[%s]", vmomiSession.Datastore.Name()),
Metadata: config.Metadata,
}
// if not vsan, set the datastore folder name to containerID
if !vmomiSession.IsVSAN(op) {
specconfig.VMPathName = fmt.Sprintf("[%s] %s/%s.vmx", vmomiSession.Datastore.Name(), specconfig.ID, specconfig.ID)
}
specconfig.VMFullName = util.DisplayName(op, specconfig, Config.ContainerNameConvention)
// log only core portions
s := specconfig
log.Debugf("id: %s, name: %s, cpu: %d, mem: %d, parent: %s, os: %s, path: %s", s.ID, s.Name, s.NumCPUs, s.MemoryMB, s.ParentImageID, s.BootMediaPath, s.VMPathName)
m := s.Metadata
log.Debugf("annotations: %#v, reponame: %s", m.Annotations, m.RepoName)
for name, sess := range m.Sessions {
log.Debugf("session: %s, path: %s, dir: %s, runblock: %t, tty: %t, restart: %t, stdin: %t, stopsig: %s",
name, sess.Cmd.Path, sess.Cmd.Dir, sess.RunBlock, sess.Tty, sess.Restart, sess.OpenStdin, sess.StopSignal)
}
// If the debug level is high, dump everything
// we still do the logging above for consistency so searching the logs for common strings works.
// TODO: move this into a debug level aware structure renderer
if Config.DebugLevel > 2 {
log.Debugf("Config: %#v", specconfig)
log.Debugf("Executor spec: %#v", *specconfig.Metadata)
for _, sess := range m.Sessions {
log.Debugf("Session spec: %#v", *sess)
}
}
// Create a linux guest
linux, err := guest.NewLinuxGuest(op, vmomiSession, specconfig)
if err != nil {
log.Errorf("Failed during linux specific spec generation during create of %s: %s", config.Metadata.ID, err)
return nil, err
}
h.Guest = linux
h.Spec = linux.Spec()
handlesLock.Lock()
defer handlesLock.Unlock()
handles.Add(h.key, h)
return h, nil
}

View File

@@ -0,0 +1,80 @@
// Copyright 2016 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.
package exec
import (
"context"
log "github.com/Sirupsen/logrus"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
)
type VCHStats struct {
CPULimit int64 // resource pool CPU limit
CPUUsage int64 // resource pool CPU usage in MhZ
MemoryLimit int64 // resource pool Memory limit
MemoryUsage int64 // resource pool Memory Usage
}
func GetVCHstats(ctx context.Context, moref ...types.ManagedObjectReference) VCHStats {
var p mo.ResourcePool
var vch VCHStats
if Config.ResourcePool == nil {
log.Errorf("Unable to retrieve VCHstats: Config.ResourcePool is nil")
return vch
}
r := Config.ResourcePool.Reference()
if len(moref) > 0 {
r = moref[0]
}
ps := []string{"config.cpuAllocation", "config.memoryAllocation", "runtime.cpu", "runtime.memory", "parent"}
if err := Config.ResourcePool.Properties(ctx, r, ps, &p); err != nil {
log.Errorf("VCH stats error: %s", err)
return vch
}
vch.CPUUsage = p.Runtime.Cpu.OverallUsage
vch.MemoryUsage = p.Runtime.Memory.OverallUsage
if p.Config.CpuAllocation.Limit != nil {
vch.CPULimit = *p.Config.CpuAllocation.Limit
}
if p.Config.MemoryAllocation.Limit != nil {
vch.MemoryLimit = *p.Config.MemoryAllocation.Limit
}
stats := []int64{vch.CPULimit,
vch.MemoryLimit,
vch.CPUUsage,
vch.MemoryUsage}
log.Debugf("The VCH stats are: %+v", stats)
// If any of the stats is -1, we need to get the vch stats from the parent resource pool
for _, v := range stats {
if v == -1 {
return GetVCHstats(ctx, *p.Parent)
}
}
return vch
}

View File

@@ -0,0 +1,121 @@
// Copyright 2016 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.
package exec2
import (
"net/url"
"time"
"github.com/google/uuid"
"github.com/vmware/vic/pkg/vsphere/vm"
)
type ID uuid.UUID
func GenerateID() ID {
return ID(uuid.New())
}
func ParseID(idStr string) (ID, error) {
result, err := uuid.Parse(idStr)
return ID(result), err
}
func (id ID) String() string {
return uuid.UUID(id).String()
}
// Struct that represents the internal port-layer representation of a container
// All data in this struct must be data that is either immutable
// or can be relied upon without having to query either the container guest
// or the underlying infrastructure. Some of this state will be updated by events
type container struct {
ConstantConfig
vm vm.VirtualMachine
runState RunState
config Config
mainProcess ProcessConfig // container main process
execdProcess []ProcessConfig
filesToCopy []FileToCopy // cache if copy while stopped
}
// config that will be applied to a container on commit
// Needs to be public as it will be shared by net, storage etc
type PendingCommit struct {
ConstantConfig
runState RunState
config Config
mainProcess ProcessConfig
filesToCopy []FileToCopy
}
// config state that cannot change for the lifetime of the container
type ConstantConfig struct {
ContainerID ID
Created time.Time
}
// variable container configuration state
type Config struct {
Name string
Limits ResourceLimits
}
// configuration state of a container process
type ProcessConfig struct {
ProcessID ID
WorkDir string
ExecPath string
ExecArgs string
}
func NewProcessConfig(workDir string, execPath string, execArgs string) ProcessConfig {
return ProcessConfig{ProcessID: GenerateID(), WorkDir: workDir, ExecArgs: execArgs, ExecPath: execPath}
}
type ProcessStatus int
const (
_ ProcessStatus = iota
Started
Exited
)
// runtime status of a container process
type ProcessRunState struct {
ProcessID ID
Status ProcessStatus
GuestPid int
ExitCode int
ExitMsg string
StartedAt time.Time
FinishedAt time.Time
}
type FileToCopy struct {
target url.URL
perms int16
data []byte
}
type ResourceLimits struct {
MemoryMb int
CPUMhz int
}

View File

@@ -0,0 +1,117 @@
// Copyright 2016 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.
package exec2
// ContainerLifecycle represents operations concerned with the creation, modification
// and deletion of containers
type ContainerLifecycle interface {
// CreateContainer creates a new container representation and returns a Handle
// The Handle can be used to configure the container before its actually created
// Calling Commit on the Handle will create the container
CreateContainer(name string) (Handle, error)
// GetHandle allows for an existing container to be modified
// The Handle can be used to reconfigure the container
// Calling Commit on the Handle will apply the reconfiguration
// Commit will fail if another client committed a modification after GetHandle was called
GetHandle(cid ID) (Handle, error)
// CopyTo copies a file into the container represented by the handle
// If the container is stopped, the file will be copied in when it is next started
CopyTo(handle Handle, targetDir string, fname string, perms int16, data []byte) (Handle, error)
// SetEntryPoint sets the entry point for the container
// This is the executable, the lifecycle of which is tied to the container lifecycle
SetEntryPoint(handle Handle, workDir string, execPath string, execArgs string) (Handle, error)
// SetLimits sets resource limits on the container
// A value of -1 implies a default value, not unlimited
// New limits will be ignored if committed to a running container
SetLimits(handle Handle, memoryMb int, cpuMhz int) (Handle, error)
// SetRunState allows for the running state of a container to be modified
// Created is not a valid state and will return an error
SetRunState(handle Handle, runState RunState) (Handle, error)
// Commit applies changes made to the Handle to either a new or running container
// Commit will fail if another client committed a modification after GetHandle was called
// Commit blocks until all changes have been committed
Commit(handle Handle) (ID, error)
// DestroyContainer destroys an stopped container
// It is up to the caller to put the container in stopped state before calling Destroy
DestroyContainer(cid ID) error
}
type ProcessLifecycle interface {
// ExecProcess executes a process in the container
// The lifecycle of the process is independent of the container main process
// The ID returned is a uuid handle to the process
ExecProcess(cid ID, workDir string, execPath string, execArgs string) (ID, error)
// Send a signal to the process
// Specifying a process ID will signal an exec'd process. Specifying the container ID will signal the main process
Signal(processID ID, signal int) error
}
// ContainerQuery represents queries that can be made against a Container
type ContainerQuery interface {
// ListContainers lists all container IDs for a given state
// If forState is nil, all containers are returned
ListContainers(forState RunState) ([]ID, error)
// GetConfig returns container and process config
GetContainerConfig(cid ID) (ContainerConfig, error)
// GetState returns the current state of the container and its processes
// This call will return a snapshot of the most recent state for each entity
GetContainerState(cid ID) (ContainerState, error)
// CopyFrom copies file data out of a running container
// Returns an error if the container is not running
CopyFrom(cid ID, sourceDir string, fname string) ([]byte, error)
}
// RunState represents the running state of a container
type RunState int
const (
_ RunState = iota
Created
Running
Stopped
)
// ContainerConfig is a type representing the configuration of a container and its processes
type ContainerConfig struct {
ConstantConfig
Config
MainProcess ProcessConfig
ExecdProcs []ProcessConfig
}
// ContainerState is a type representing the runtime state of a container and its processes
type ContainerState struct {
Status RunState
MainProcess ProcessState
ExecdProcs []ProcessState
}
// ProcessState is the runtime state of a process in a container
type ProcessState struct {
ProcessRunState
}

View File

@@ -0,0 +1,128 @@
// Copyright 2016 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.
package exec2
import (
"fmt"
"net/url"
)
// PortLayerVsphere is a WIP implementation of the execution.go interfaces
type PortLayerVsphere struct {
vmomiGateway VmomiGateway
handles HandleFactory
containers map[ID]*container
}
func (p *PortLayerVsphere) getContainer(handle Handle) *container {
return p.containers[handle.(*PendingCommit).ContainerID]
}
func (p *PortLayerVsphere) newHandle(cid ID) *PendingCommit {
return p.handles.createHandle(cid).(*PendingCommit)
}
func (p *PortLayerVsphere) Init(gateway VmomiGateway, factory HandleFactory) error {
p.handles = factory
p.vmomiGateway = gateway
p.containers = make(map[ID]*container)
return nil
}
func (p *PortLayerVsphere) CreateContainer(name string) (Handle, error) {
cid := GenerateID()
handle := p.newHandle(cid)
handle.config.Name = name
handle.runState = Created
return handle, nil
}
func (p *PortLayerVsphere) GetHandle(cid ID) (Handle, error) {
c := p.containers[cid]
if c == nil {
return nil, fmt.Errorf("Invalid container ID")
}
return p.handles.createHandle(c.ContainerID), nil
}
func (p *PortLayerVsphere) SetEntryPoint(handle Handle, workDir string, execPath string, execArgs string) (Handle, error) {
resolvedHandle := handle.(*PendingCommit)
resolvedHandle.mainProcess = NewProcessConfig(workDir, execPath, execArgs)
return p.handles.refreshHandle(handle), nil
}
func (p *PortLayerVsphere) Commit(handle Handle) (ID, error) {
var err error
c := p.getContainer(handle)
if c == nil {
c, err = p.createContainer(handle)
} else {
// if c.vm == nil {
// return "", fmt.Errorf("Cannot modify container with no VM")
// }
err = p.modifyContainer(c.runState, handle)
}
// Handle will be garbage collected
return c.ContainerID, err
}
func (p *PortLayerVsphere) CopyTo(handle Handle, targetDir string, fname string, perms int16, data []byte) (Handle, error) {
var result Handle
resolvedHandle := handle.(*PendingCommit)
u, err := url.Parse("file://" + targetDir + "/" + fname)
if err == nil {
fileToCopy := FileToCopy{target: *u, perms: perms, data: data}
resolvedHandle.filesToCopy = append(resolvedHandle.filesToCopy, fileToCopy)
result = p.handles.refreshHandle(handle)
}
return result, err
}
func (p *PortLayerVsphere) SetLimits(handle Handle, memoryMb int, cpuMhz int) (Handle, error) {
resolvedHandle := handle.(*PendingCommit)
resolvedHandle.config.Limits = ResourceLimits{MemoryMb: memoryMb, CPUMhz: cpuMhz}
return p.handles.refreshHandle(handle), nil
}
func (p *PortLayerVsphere) SetRunState(handle Handle, runState RunState) (Handle, error) {
resolvedHandle := handle.(*PendingCommit)
resolvedHandle.runState = runState
return p.handles.refreshHandle(handle), nil
}
func (p *PortLayerVsphere) DestroyContainer(cid ID) error {
c := p.containers[cid]
if c == nil {
return fmt.Errorf("Invalid container ID")
}
delete(p.containers, cid)
return nil
}
func (p *PortLayerVsphere) createContainer(handle Handle) (*container, error) {
resolvedHandle := handle.(*PendingCommit)
c := container{}
p.containers[resolvedHandle.ContainerID] = &c
c.ContainerID = resolvedHandle.ContainerID
c.runState = resolvedHandle.runState
// followed by other transfer of state from pending to container
// fmt.Printf("Creating container for %v\n", pending)
return &c, nil
}
func (p *PortLayerVsphere) modifyContainer(runState RunState, handle Handle) error {
// fmt.Printf("Modifying container for %v\n", pending)
return nil
}

View File

@@ -0,0 +1,37 @@
// Copyright 2016 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.
package exec2
/* A Handle should be completely opaque */
type Handle interface{}
type HandleFactory interface {
createHandle(cID ID) Handle
refreshHandle(oldHandle Handle) Handle
}
type BasicHandleFactory struct {
}
func (h *BasicHandleFactory) createHandle(cid ID) Handle {
newPc := &PendingCommit{}
newPc.ContainerID = cid
return newPc
}
// Basic handle resolver just passes back the handle passed in
func (h *BasicHandleFactory) refreshHandle(oldHandle Handle) Handle {
return oldHandle
}

View File

@@ -0,0 +1,126 @@
// Copyright 2016 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.
package remote
import (
"encoding/gob"
"net/rpc"
"github.com/google/uuid"
"github.com/vmware/vic/lib/portlayer/exec2"
)
const serverAddress string = "localhost"
type PortLayerRPCClient struct {
client *rpc.Client
}
func (p *PortLayerRPCClient) Connect() error {
// Ignore Init args on the client - that is the server's responsibility
var err error
gob.Register(uuid.New())
p.client, err = rpc.DialHTTP("tcp", serverAddress+":1234")
return err
}
type CreateArgs struct {
Name string
}
func (p *PortLayerRPCClient) CreateContainer(name string) (exec2.Handle, error) {
args := &CreateArgs{Name: name}
var reply exec2.Handle
err := p.client.Call("PortLayerRPCServer.CreateContainer", args, &reply)
return reply, err
}
func (p *PortLayerRPCClient) GetHandle(cid exec2.ID) (exec2.Handle, error) {
var reply exec2.Handle
err := p.client.Call("PortLayerRPCServer.GetHandle", cid, &reply)
return reply, err
}
type CopyToArgs struct {
Handle exec2.Handle
TargetDir string
Fname string
Perms int16
Data []byte
}
func (p *PortLayerRPCClient) CopyTo(handle exec2.Handle, targetDir string, fname string, perms int16, data []byte) (exec2.Handle, error) {
args := &CopyToArgs{Handle: handle, TargetDir: targetDir, Fname: fname, Perms: perms, Data: data}
var reply exec2.Handle
err := p.client.Call("PortLayerRPCServer.CopyTo", args, &reply)
return reply, err
}
type SetEntryPointArgs struct {
Handle exec2.Handle
WorkDir string
ExecPath string
Args string
}
func (p *PortLayerRPCClient) SetEntryPoint(handle exec2.Handle, workDir string, execPath string, args string) (exec2.Handle, error) {
epArgs := &SetEntryPointArgs{Handle: handle, WorkDir: workDir, ExecPath: execPath, Args: args}
var reply exec2.Handle
err := p.client.Call("PortLayerRPCServer.SetEntryPoint", epArgs, &reply)
return reply, err
}
type SetLimitsArgs struct {
Handle exec2.Handle
MemoryMb int
CPUMhz int
}
func (p *PortLayerRPCClient) SetLimits(handle exec2.Handle, memoryMb int, cpuMhz int) (exec2.Handle, error) {
args := &SetLimitsArgs{Handle: handle, MemoryMb: memoryMb, CPUMhz: cpuMhz}
var reply exec2.Handle
err := p.client.Call("PortLayerRPCServer.SetLimits", args, &reply)
return reply, err
}
type SetRunStateArgs struct {
Handle exec2.Handle
RunState exec2.RunState
}
func (p *PortLayerRPCClient) SetRunState(handle exec2.Handle, runState exec2.RunState) (exec2.Handle, error) {
args := &SetRunStateArgs{Handle: handle, RunState: runState}
var reply exec2.Handle
err := p.client.Call("PortLayerRPCServer.SetRunState", args, &reply)
return reply, err
}
type CommitArgs struct {
Handle exec2.Handle
}
func (p *PortLayerRPCClient) Commit(handle exec2.Handle) (exec2.ID, error) {
args := &CommitArgs{Handle: handle}
var reply exec2.ID
err := p.client.Call("PortLayerRPCServer.Commit", args, &reply)
return reply, err
}
func (p *PortLayerRPCClient) DestroyContainer(cid exec2.ID) error {
/* Ignore the reply */
var reply exec2.ID
return p.client.Call("PortLayerRPCServer.DestroyContainer", cid, &reply)
}

View File

@@ -0,0 +1,133 @@
// 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.
package main
import (
"encoding/gob"
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"github.com/google/uuid"
"github.com/vmware/vic/lib/portlayer/exec2"
"github.com/vmware/vic/lib/portlayer/exec2/remote"
)
type PortLayerRPCServer struct {
}
// A Handle can be anything, so this takes advantage of that by creating a sparse handle
// to send to the client and using that sparse handle as a key in a hashtable which points to
// rich handles created by the HandleFactory
type SparseHandle exec2.Handle
var lcTarget exec2.ContainerLifecycle
//var lcQuery exec2.ContainerQuery
var handles map[SparseHandle]exec2.Handle
func init() {
pl := &exec2.PortLayerVsphere{}
pl.Init(nil, &exec2.BasicHandleFactory{})
lcTarget = pl
//lcQuery = lcTarget
handles = make(map[SparseHandle]exec2.Handle)
}
func main() {
gob.Register(uuid.New())
rpcServer := new(PortLayerRPCServer)
rpc.Register(rpcServer)
rpc.HandleHTTP()
// #nosec: Binds to all network interfaces
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
fmt.Println("Server listening")
http.Serve(l, nil)
}
// A sparse handle is simply a random string
func newSparseHandle() SparseHandle {
return SparseHandle(uuid.New())
}
func createSparseHandle(handle exec2.Handle) SparseHandle {
key := newSparseHandle()
handles[key] = handle
return key
}
func resolveSparseHandle(handle SparseHandle) exec2.Handle {
return handles[handle]
}
func refreshHandle(result *exec2.Handle, oldHandle SparseHandle, newHandle exec2.Handle, err error) error {
*result = createSparseHandle(newHandle)
delete(handles, oldHandle)
return err
}
func (*PortLayerRPCServer) CreateContainer(args remote.CreateArgs, result *exec2.Handle) error {
handle, err := lcTarget.CreateContainer(args.Name)
*result = createSparseHandle(handle)
return err
}
func (*PortLayerRPCServer) GetHandle(cid exec2.ID, result *exec2.Handle) error {
handle, err := lcTarget.GetHandle(cid)
*result = createSparseHandle(handle)
return err
}
func (*PortLayerRPCServer) CopyTo(args remote.CopyToArgs, result *exec2.Handle) error {
handle := resolveSparseHandle(args.Handle)
newHandle, err := lcTarget.CopyTo(handle, args.TargetDir, args.Fname, args.Perms, args.Data)
return refreshHandle(result, handle, newHandle, err)
}
func (*PortLayerRPCServer) SetEntryPoint(args remote.SetEntryPointArgs, result *exec2.Handle) error {
handle := resolveSparseHandle(args.Handle)
newHandle, err := lcTarget.SetEntryPoint(handle, args.WorkDir, args.ExecPath, args.Args)
return refreshHandle(result, handle, newHandle, err)
}
func (*PortLayerRPCServer) SetLimits(args remote.SetLimitsArgs, result *exec2.Handle) error {
handle := resolveSparseHandle(args.Handle)
newHandle, err := lcTarget.SetLimits(handle, args.MemoryMb, args.CPUMhz)
return refreshHandle(result, handle, newHandle, err)
}
func (*PortLayerRPCServer) SetRunState(args remote.SetRunStateArgs, result *exec2.Handle) error {
handle := resolveSparseHandle(args.Handle)
newHandle, err := lcTarget.SetRunState(handle, args.RunState)
return refreshHandle(result, handle, newHandle, err)
}
func (*PortLayerRPCServer) Commit(args remote.CommitArgs, result *exec2.ID) error {
cid, err := lcTarget.Commit(resolveSparseHandle(args.Handle))
*result = cid
return err
}
func (*PortLayerRPCServer) DestroyContainer(cid exec2.ID, result *exec2.ID) error {
err := lcTarget.DestroyContainer(cid)
*result = cid
return err
}

View File

@@ -0,0 +1,19 @@
// Copyright 2016 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.
package exec2
// VmomiGateway represents an interface to a pre-authenticated Vmomi API
type VmomiGateway interface {
}

View File

@@ -0,0 +1,203 @@
// Copyright 2016 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.
package logging
import (
"context"
"fmt"
"sync"
log "github.com/Sirupsen/logrus"
"strings"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/portlayer/event/collector/vsphere"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/pkg/retry"
"github.com/vmware/vic/pkg/trace"
)
var once sync.Once
func Init(ctx context.Context) error {
once.Do(func() {
// Subscribe to vm events
exec.Config.EventManager.Subscribe(
events.NewEventType(vsphere.VMEvent{}).Topic(),
"logging",
func(ie events.Event) {
eventCallback(ie)
})
})
return nil
}
// listens migrated events and connects the file backed serial ports
func eventCallback(ie events.Event) {
defer trace.End(trace.Begin(""))
switch ie.String() {
case events.ContainerMigrated,
events.ContainerMigratedByDrs:
op := trace.NewOperation(context.Background(), "LoggingEvent")
op.Debugf("Logging processing eventID(%s): %s", ie.EventID(), ie)
// grab the container from the cache
container := exec.Containers.Container(ie.Reference())
if container == nil {
op.Errorf("Container %s not found. Dropping the event %s from Logging subsystem.", ie.Reference(), ie)
return
}
operation := func() error {
var err error
handle := container.NewHandle(op)
if handle == nil {
err = fmt.Errorf("Handle for %s cannot be created", ie.Reference())
log.Error(err)
return err
}
defer handle.Close()
// set them to true
if handle, err = toggle(handle, true); err != nil {
op.Errorf("Failed to toggle logging after %s event for container %s: %s", ie, ie.Reference(), err)
return err
}
if err = handle.Commit(op, nil, nil); err != nil {
op.Errorf("Failed to commit handle after getting %s event for container %s: %s", ie, ie.Reference(), err)
return err
}
return nil
}
if err := retry.Do(operation, exec.IsConcurrentAccessError); err != nil {
op.Errorf("Multiple attempts failed to commit handle after getting %s event for container %s: %s", ie, ie.Reference(), err)
}
}
}
func toggle(handle *exec.Handle, connected bool) (*exec.Handle, error) {
defer trace.End(trace.Begin(""))
// get the virtual device list
devices := object.VirtualDeviceList(handle.Config.Hardware.Device)
// select the virtual serial ports
serials := devices.SelectByBackingInfo((*types.VirtualSerialPortFileBackingInfo)(nil))
if len(serials) == 0 {
return nil, fmt.Errorf("Unable to find a device with desired backing")
}
for i := range serials {
serial := serials[i]
log.Debugf("Found a device with desired backing: %#v", serial)
c := serial.GetVirtualDevice().Connectable
if c.Connected == connected {
log.Debugf("Already in the desired state (connected: %t)", connected)
continue
}
log.Debugf("Setting Connected to %t", connected)
c.Connected = connected
config := &types.VirtualDeviceConfigSpec{
Device: serial,
Operation: types.VirtualDeviceConfigSpecOperationEdit,
}
handle.Spec.DeviceChange = append(handle.Spec.DeviceChange, config)
}
return handle, nil
}
// Join adds two file backed serial port and configures them
func Join(h interface{}) (interface{}, error) {
defer trace.End(trace.Begin(""))
handle, ok := h.(*exec.Handle)
if !ok {
return nil, fmt.Errorf("Type assertion failed for %#+v", handle)
}
var logFilePath string
VMPathName := handle.Spec.VMPathName()
VMName := handle.Spec.Spec().Name
logFilePath = fmt.Sprintf("%s/%s", VMPathName, VMName)
// on non-vsan setup, VMPathName is set to "[datastore_name] containerID/containerID.vmx"
if strings.HasSuffix(VMPathName, ".vmx") {
idx := strings.LastIndex(VMPathName, "/")
logFilePath = VMPathName[:idx]
}
for _, logFile := range []string{"tether.debug", "output.log"} {
filename := fmt.Sprintf("%s/%s", logFilePath, logFile)
log.Infof("set log file name to: %s", filename)
// Debug and log serial ports - backed by datastore file
serial := &types.VirtualSerialPort{
VirtualDevice: types.VirtualDevice{
Backing: &types.VirtualSerialPortFileBackingInfo{
VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
FileName: filename,
},
},
Connectable: &types.VirtualDeviceConnectInfo{
Connected: true,
StartConnected: true,
AllowGuestControl: true,
},
},
YieldOnPoll: true,
}
config := &types.VirtualDeviceConfigSpec{
Device: serial,
Operation: types.VirtualDeviceConfigSpecOperationAdd,
}
handle.Spec.DeviceChange = append(handle.Spec.DeviceChange, config)
}
return handle, nil
}
// Bind sets the *Connected fields of the VirtualSerialPort
func Bind(h interface{}) (interface{}, error) {
defer trace.End(trace.Begin(""))
handle, ok := h.(*exec.Handle)
if !ok {
return nil, fmt.Errorf("Type assertion failed for %#+v", handle)
}
return toggle(handle, true)
}
// Unbind unsets the *Connected fields of the VirtualSerialPort
func Unbind(h interface{}) (interface{}, error) {
defer trace.End(trace.Begin(""))
handle, ok := h.(*exec.Handle)
if !ok {
return nil, fmt.Errorf("Type assertion failed for %#+v", handle)
}
return toggle(handle, false)
}

View File

@@ -0,0 +1,106 @@
// Copyright 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.
package metrics
import (
"context"
"fmt"
"reflect"
"sync"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/performance"
"github.com/vmware/vic/pkg/vsphere/session"
)
var (
Supervisor *super
initializer struct {
err error
once sync.Once
}
)
// super manages the lifecycle and access to the
// available metrics collectors
type super struct {
vms *performance.VMCollector
}
type UnsupportedTypeError struct {
subscriber interface{}
}
func (ute UnsupportedTypeError) Error() string {
t := reflect.TypeOf(ute.subscriber)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
return fmt.Sprintf("type %s is not supported by metrics", t)
}
func Init(ctx context.Context, session *session.Session) error {
defer trace.End(trace.Begin(""))
initializer.once.Do(func() {
var err error
defer func() {
if err != nil {
initializer.err = err
}
}()
Supervisor = newSupervisor(session)
})
return initializer.err
}
func newSupervisor(session *session.Session) *super {
defer trace.End(trace.Begin(""))
// create the vm metric collector
v := performance.NewVMCollector(session)
return &super{
vms: v,
}
}
func (s *super) Subscribe(op trace.Operation, subscriber interface{}) (chan interface{}, error) {
switch sub := subscriber.(type) {
case *exec.Container:
return s.vms.Subscribe(op, sub.VMReference(), sub.String())
}
err := UnsupportedTypeError{
subscriber: subscriber,
}
op.Errorf("%s", err)
return nil, err
}
func (s *super) Unsubscribe(op trace.Operation, subscriber interface{}, ch chan interface{}) {
switch sub := subscriber.(type) {
case *exec.Container:
s.vms.Unsubscribe(op, sub.VMReference(), ch)
default:
err := UnsupportedTypeError{
subscriber: subscriber,
}
op.Errorf("%s", err)
}
return
}

View File

@@ -0,0 +1,46 @@
// Copyright 2016 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.
package network
import (
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/config"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
)
type Configuration struct {
source extraconfig.DataSource `vic:"0.1" scope:"read-only" recurse:"depth=0"`
sink extraconfig.DataSink `vic:"0.1" scope:"read-only" recurse:"depth=0"`
// Turn on debug logging
DebugLevel int `vic:"0.1" scope:"read-only" key:"init/diagnostics/debug"`
// Port Layer - network
config.Network `vic:"0.1" scope:"read-only" key:"network"`
// The bridge link
BridgeLink Link `vic:"0.1" scope:"read-only" recurse:"depth=0"`
// the vsphere portgroups corresponding to container network configuration
PortGroups map[string]object.NetworkReference `vic:"0.1" scope:"read-only" recurse:"depth=0"`
}
func (c *Configuration) Encode() {
extraconfig.Encode(c.sink, c)
}
func (c *Configuration) Decode() {
extraconfig.Decode(c.source, c)
}

View File

@@ -0,0 +1,117 @@
// Copyright 2016 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.
package network
import (
"context"
"fmt"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/pkg/uid"
)
type Container struct {
sync.Mutex
id uid.UID
name string
endpoints []*Endpoint
}
func (c *Container) Endpoints() []*Endpoint {
c.Lock()
defer c.Unlock()
ret := make([]*Endpoint, len(c.endpoints))
copy(ret, c.endpoints)
return ret
}
func (c *Container) ID() uid.UID {
return c.id
}
func (c *Container) Name() string {
return c.name
}
func (c *Container) endpoint(s *Scope) *Endpoint {
for _, e := range c.endpoints {
if e.Scope() == s {
return e
}
}
return nil
}
func (c *Container) Endpoint(s *Scope) *Endpoint {
c.Lock()
defer c.Unlock()
return c.endpoint(s)
}
func (c *Container) Scopes() []*Scope {
c.Lock()
defer c.Unlock()
scopes := make([]*Scope, len(c.endpoints))
i := 0
for _, e := range c.endpoints {
scopes[i] = e.Scope()
i++
}
return scopes
}
func (c *Container) addEndpoint(e *Endpoint) {
c.Lock()
defer c.Unlock()
c.endpoints = append(c.endpoints, e)
}
func (c *Container) removeEndpoint(e *Endpoint) {
c.Lock()
defer c.Unlock()
c.endpoints = removeEndpointHelper(e, c.endpoints)
}
func (c *Container) Refresh(ctx context.Context) error {
c.Lock()
defer c.Unlock()
// this will "refresh" the container executor config that contains
// the current ip addresses
h := exec.GetContainer(ctx, c.ID())
if h == nil {
return fmt.Errorf("could not find container %s", c.ID())
}
defer h.Close()
for _, e := range c.endpoints {
if err := e.refresh(h); err != nil {
log.Warnf("could not refresh endpoint for container %s: %s", h.ExecConfig.ID, err)
}
}
return nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,229 @@
// Copyright 2016 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.
package network
import (
"fmt"
"net"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/pkg/ip"
"github.com/vmware/vic/pkg/uid"
)
type alias struct {
Name string
Container string
ep *Endpoint
}
var badAlias = alias{}
type Endpoint struct {
container *Container
scope *Scope
ip net.IP
static bool
ports map[Port]interface{} // exposed ports
aliases map[string][]alias
gw *net.IP
subnet *net.IPNet
}
// scopeName returns the "fully qualified" name of an alias. Aliases are scoped
// by the container and network scope they are in.
func (a alias) scopedName() string {
// an alias for the container itself is network scoped
for _, al := range a.ep.getAliases("") {
if a.Name == al.Name {
return ScopedAliasName(a.ep.Scope().Name(), "", a.Name)
}
}
return ScopedAliasName(a.ep.Scope().Name(), a.ep.Container().Name(), a.Name)
}
// ScopedAliasName returns the fully qualified name of an alias, scoped to
// the scope and optionally a container
func ScopedAliasName(scope string, container string, alias string) string {
if container != "" {
return fmt.Sprintf("%s:%s:%s", scope, container, alias)
}
return fmt.Sprintf("%s:%s", scope, alias)
}
func newEndpoint(container *Container, scope *Scope, eip *net.IP, pciSlot *int32) *Endpoint {
e := &Endpoint{
container: container,
scope: scope,
ip: net.IPv4(0, 0, 0, 0),
static: false,
ports: make(map[Port]interface{}),
aliases: make(map[string][]alias),
}
if eip != nil && !ip.IsUnspecifiedIP(*eip) {
e.ip = *eip
e.static = true
}
return e
}
func removeEndpointHelper(ep *Endpoint, eps []*Endpoint) []*Endpoint {
for i, e := range eps {
if ep != e {
continue
}
return append(eps[:i], eps[i+1:]...)
}
return eps
}
func (e *Endpoint) addPort(p Port) error {
if _, ok := e.ports[p]; ok {
return fmt.Errorf("port %s already exposed", p)
}
e.ports[p] = nil
return nil
}
func (e *Endpoint) IP() net.IP {
return e.ip
}
func (e *Endpoint) Scope() *Scope {
return e.scope
}
func (e *Endpoint) Subnet() *net.IPNet {
if e.subnet != nil {
return e.subnet
}
return e.Scope().Subnet()
}
func (e *Endpoint) Container() *Container {
return e.container
}
func (e *Endpoint) ID() uid.UID {
return e.container.ID()
}
func (e *Endpoint) Name() string {
return e.container.Name()
}
func (e *Endpoint) Gateway() net.IP {
if e.gw != nil {
return *e.gw
}
return e.Scope().Gateway()
}
func (e *Endpoint) Ports() []Port {
ports := make([]Port, len(e.ports))
i := 0
for p := range e.ports {
ports[i] = p
i++
}
return ports
}
func (e *Endpoint) addAlias(con, a string) (alias, bool) {
if a == "" {
return badAlias, false
}
if con == "" {
con = e.container.Name()
}
aliases := e.aliases[con]
for _, as := range aliases {
if as.Name == a {
// already present
return as, true
}
}
na := alias{
Name: a,
Container: con,
ep: e,
}
e.aliases[con] = append(aliases, na)
return na, false
}
func (e *Endpoint) getAliases(con string) []alias {
if con == "" {
con = e.container.Name()
}
return e.aliases[con]
}
func (e *Endpoint) copy() *Endpoint {
other := *e
other.aliases = make(map[string][]alias)
for k, v := range e.aliases {
a := make([]alias, len(v))
copy(a, v)
other.aliases[k] = a
}
other.ports = make(map[Port]interface{})
for p := range e.ports {
other.ports[p] = nil
}
return &other
}
func (e *Endpoint) refresh(h *exec.Handle) error {
if !e.scope.isDynamic() {
return nil
}
s := e.scope
ne := h.ExecConfig.Networks[s.Name()]
if ne == nil {
return fmt.Errorf("container config does not have info for network scope %s", s.Name())
}
if ip.IsUnspecifiedSubnet(&ne.Network.Assigned.Gateway) {
return fmt.Errorf("updating endpoint for container %s: gateway not present for scope %s", h.ExecConfig.ID, s.name)
}
gw, snet, err := net.ParseCIDR(ne.Network.Assigned.Gateway.String())
if err != nil {
return fmt.Errorf("could not parse gateway for container %s: %s", h.ExecConfig.ID, err)
}
e.ip = ne.Assigned.IP
e.gw = &gw
e.subnet = snet
return nil
}

View File

@@ -0,0 +1,76 @@
// Copyright 2016 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.
package network
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEndpointNameID(t *testing.T) {
c := &Container{id: "foo", name: "bar"}
s := &Scope{
gateway: net.ParseIP("10.10.10.1"),
subnet: &net.IPNet{IP: net.ParseIP("10.10.10.0"), Mask: net.CIDRMask(24, 32)},
}
e := Endpoint{
container: c,
scope: s,
ip: net.ParseIP("10.10.10.10"),
static: true,
ports: make(map[Port]interface{}),
}
assert.Equal(t, c.ID(), e.ID())
assert.Equal(t, c.Name(), e.Name())
}
func TestEndpointCopy(t *testing.T) {
c := &Container{id: "foo"}
s := &Scope{
gateway: net.ParseIP("10.10.10.1"),
subnet: &net.IPNet{IP: net.ParseIP("10.10.10.0"), Mask: net.CIDRMask(24, 32)},
}
e := Endpoint{
container: c,
scope: s,
ip: net.ParseIP("10.10.10.10"),
static: true,
ports: make(map[Port]interface{}),
}
p, err := ParsePort("80/tcp")
assert.NoError(t, err, "")
e.ports[p] = nil
other := e.copy()
assert.Equal(t, other.ID(), e.ID())
assert.Equal(t, other.container, c)
assert.Equal(t, other.container, e.container)
assert.Equal(t, other.scope, s)
assert.Equal(t, other.scope, e.scope)
assert.True(t, other.ip.Equal(e.ip), "other.ip (%s) != e.ip (%s)", other.ip, e.ip)
assert.True(t, other.Gateway().Equal(e.Gateway()), "other.Gateway() (%s) != e.Gateway() (%s)", other.Gateway(), e.Gateway())
assert.True(t, other.Subnet().IP.Equal(e.Subnet().IP), "other.Subnet() (%s) != e.Subnet() (%s)", other.Subnet(), e.Subnet())
assert.Equal(t, other.Subnet().Mask, e.Subnet().Mask, "other.Subnet() (%s) != e.Subnet() (%s)", other.Subnet(), e.Subnet())
assert.EqualValues(t, other.ports, e.ports)
// make sure .ports is a copy
other.ports["foo"] = nil
assert.NotContains(t, e.ports, "foo")
}

View File

@@ -0,0 +1,426 @@
// Copyright 2016 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.
// IP address management
//
// The API here just concerns itself with tracking blocks of
// IP addresses, as well as individual IPs within the blocks.
// The API does not have a concept of "network", in particular
// when managing CIDR blocks, the network and broadcast address
// are available as valid addresses. This behavior can be
// accomplished, however, by just reserving those two addresses
// first thing after requesting a CIDR address space, by using
// the ReserveIP4() call.
package network
import (
"bytes"
"fmt"
"net"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vic/pkg/ip"
)
// An AddressSpace is a collection of
// IP address ranges
type AddressSpace struct {
Parent *AddressSpace
Network *net.IPNet
Pool *ip.Range
availableRanges []*ip.Range
}
// compareIPv4 compares two IPv4 addresses.
// Returns -1 if ip1 < ip2, 0 if they are equal,
// and 1 if ip1 > ip2
func compareIP4(ip1 net.IP, ip2 net.IP) int {
ip1 = ip1.To16()
ip2 = ip2.To16()
return bytes.Compare(ip1, ip2)
}
func incrementIP4(ip net.IP) net.IP {
if !isIP4(ip) {
return nil
}
newIP := copyIP(ip)
s := 0
if len(ip) == net.IPv6len {
s = 12
}
for i := len(newIP) - 1; i >= s; i-- {
newIP[i]++
if newIP[i] > 0 {
break
}
}
return newIP
}
func decrementIP4(ip net.IP) net.IP {
if !isIP4(ip) {
return nil
}
newIP := copyIP(ip)
s := 0
if len(ip) == net.IPv6len {
s = 12
}
for i := len(newIP) - 1; i >= s; i-- {
newIP[i]--
if newIP[i] != 0xff {
break
}
}
return newIP
}
func copyIP(ip net.IP) net.IP {
newIP := make([]byte, len(ip))
copy(newIP, ip)
return newIP
}
func isIP4(ip net.IP) bool {
return ip.To4() != nil
}
// lowestIP4 returns the lowest possible IP address
// in an IP network. For example:
//
// lowestIP4(net.IPNet{}IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(16, 32)}) -> 172.16.0.0
//
func lowestIP4(ipRange *net.IPNet) net.IP {
return ipRange.IP.Mask(ipRange.Mask).To16()
}
// highestIP4 returns the highest possible IP address
// in an IP network. For example:
//
// highestIP4(net.IPNet{}IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(16, 32)}) -> 172.16.255.255
//
func highestIP4(ipRange *net.IPNet) net.IP {
if !isIP4(ipRange.IP) {
return nil
}
newIP := net.IPv4(0, 0, 0, 0)
ipRange.IP = ipRange.IP.To4()
for i := 0; i < len(ipRange.Mask); i++ {
newIP[i+12] = ipRange.IP[i] | ^ipRange.Mask[i]
}
return newIP
}
// NewAddressSpaceFromNetwork creates a new AddressSpace from a network specification.
func NewAddressSpaceFromNetwork(ipRange *net.IPNet) *AddressSpace {
s := &AddressSpace{
Network: ipRange,
Pool: &ip.Range{FirstIP: lowestIP4(ipRange), LastIP: highestIP4(ipRange)},
}
s.availableRanges = []*ip.Range{s.Pool}
return s
}
// NewAddressSpaceFromRange creates a new AddressSpace from a range of IP addresses.
func NewAddressSpaceFromRange(firstIP net.IP, lastIP net.IP) *AddressSpace {
if compareIP4(firstIP, lastIP) > 0 {
return nil
}
return &AddressSpace{
Pool: &ip.Range{FirstIP: firstIP, LastIP: lastIP},
availableRanges: []*ip.Range{{FirstIP: firstIP, LastIP: lastIP}}}
}
func (s *AddressSpace) NextIP4Net(mask net.IPMask) (*net.IPNet, error) {
ones, _ := mask.Size()
for _, r := range s.availableRanges {
network := r.FirstIP.Mask(mask).To16()
var firstIP net.IP
// check if the start of the current range
// is lower than the network boundary
if compareIP4(network, r.FirstIP) >= 0 {
// found the start of the range
firstIP = network
} else {
// network address is lower than the first
// ip in the range; try the next network
// in the mask
for i := len(network) - 1; i >= 12; i-- {
partialByteIndex := ones/8 + 12
var inc byte
if i == partialByteIndex {
// this octet may only be occupied
// by the mask partially, e.g.
// for a /25, the last octet has
// only one bit in the mask
//
// in order to get the next network
// we need to increment starting at
// the last bit of the mask, e.g. 25
// in this example, which would be
// bit 8 in the last octet.
inc = (byte)(1 << (uint)(8-ones%8))
} else if i < partialByteIndex {
// we are past the partial octets,
// so this is portion where the mask
// occupies the octets fully, so
// we can just increment the last bit
inc = 1
}
if inc == 0 {
continue
}
network[i] += inc
if network[i] > 0 {
firstIP = network
break
}
}
}
if firstIP != nil {
// we found the first IP for the requested range,
// now check if the available range can accommodate
// the highest address given the first IP and the mask
lastIP := highestIP4(&net.IPNet{IP: firstIP, Mask: mask})
if compareIP4(lastIP, r.LastIP) <= 0 {
return &net.IPNet{IP: firstIP, Mask: mask}, nil
}
}
}
return nil, fmt.Errorf("could not find IP range for mask %s", mask)
}
// ReserveNextIP4Net reserves a new sub address space within the given address
// space, given a bitmask specifying the "width" of the requested space.
func (s *AddressSpace) ReserveNextIP4Net(mask net.IPMask) (*AddressSpace, error) {
n, err := s.NextIP4Net(mask)
if err != nil {
return nil, err
}
return s.ReserveIP4Net(n)
}
func splitRange(parentRange *ip.Range, firstIP net.IP, lastIP net.IP) (before, reserved, after *ip.Range) {
if !firstIP.Equal(parentRange.FirstIP) {
before = ip.NewRange(parentRange.FirstIP, decrementIP4(firstIP))
}
if !lastIP.Equal(parentRange.LastIP) {
after = ip.NewRange(incrementIP4(lastIP), parentRange.LastIP)
}
reserved = ip.NewRange(firstIP, lastIP)
return
}
// ReserveIP4Net reserves a new sub address space given an IP and mask.
// Mask is required.
// If IP is nil or "0.0.0.0", same as calling ReserveNextIP4Net
// with the mask.
func (s *AddressSpace) ReserveIP4Net(ipNet *net.IPNet) (*AddressSpace, error) {
if ipNet.Mask == nil {
return nil, fmt.Errorf("network mask not specified")
}
if ipNet.IP == nil || ipNet.IP.Equal(net.ParseIP("0.0.0.0")) {
return s.ReserveNextIP4Net(ipNet.Mask)
}
sub, err := s.ReserveIP4Range(lowestIP4(ipNet), highestIP4(ipNet))
if err != nil {
return nil, err
}
sub.Network = &net.IPNet{IP: ipNet.IP, Mask: ipNet.Mask}
return sub, nil
}
func (s *AddressSpace) reserveSubRange(firstIP net.IP, lastIP net.IP, index int) {
before, _, after := splitRange(s.availableRanges[index], firstIP, lastIP)
s.availableRanges = append(s.availableRanges[:index], s.availableRanges[index+1:]...)
if before != nil {
s.availableRanges = insertAddressRanges(s.availableRanges, index, before)
index++
}
if after != nil {
s.availableRanges = insertAddressRanges(s.availableRanges, index, after)
}
}
// ReserveIP4Range reserves a sub address space given a first and last IP.
func (s *AddressSpace) ReserveIP4Range(firstIP net.IP, lastIP net.IP) (*AddressSpace, error) {
for i, r := range s.availableRanges {
if compareIP4(firstIP, r.FirstIP) < 0 ||
compareIP4(lastIP, r.LastIP) > 0 {
continue
}
// found range
log.Infof("Reserving IP range [%s, %s]", firstIP.String(), lastIP.String())
s.reserveSubRange(firstIP, lastIP, i)
subSpace := NewAddressSpaceFromRange(firstIP, lastIP)
subSpace.Parent = s
return subSpace, nil
}
var err error
if compareIP4(firstIP, s.Pool.FirstIP) > 0 && compareIP4(lastIP, s.Pool.LastIP) < 0 {
// IP range is within the pool but not found available
err = fmt.Errorf("Cannot reserve IP range %s - %s. Already in use", firstIP.String(), lastIP.String())
} else {
err = fmt.Errorf("Cannot reserve IP range %s - %s. Not within pool's range %s - %s",
firstIP.String(), lastIP.String(), s.Pool.FirstIP, s.Pool.LastIP)
}
log.Errorf(err.Error())
return nil, err
}
func insertAddressRanges(r []*ip.Range, index int, ranges ...*ip.Range) []*ip.Range {
if index == len(r) {
return append(r, ranges...)
}
for i := 0; i < len(ranges); i++ {
r = append(r, &ip.Range{})
}
copy(r[index+len(ranges):], r[index:])
for i := 0; i < len(ranges); i++ {
r[index+i] = ranges[i]
}
return r
}
// ReserveNextIP4 reserves the next available IPv4 address.
func (s *AddressSpace) ReserveNextIP4() (net.IP, error) {
space, err := s.ReserveIP4Net(&net.IPNet{Mask: net.CIDRMask(32, 32)})
if err != nil {
return nil, err
}
return space.availableRanges[0].FirstIP, nil
}
// ReserveIP4 reserves the given IPv4 address.
func (s *AddressSpace) ReserveIP4(ip net.IP) error {
_, err := s.ReserveIP4Range(ip, ip)
return err
}
// ReleaseIP4Range releases a sub address space into the parent address space.
// Sub address space has to have only a single available range.
func (s *AddressSpace) ReleaseIP4Range(space *AddressSpace) error {
// nothing to release
if space == nil || len(space.availableRanges) == 0 {
return nil
}
if space.Parent != s {
return fmt.Errorf("cannot release subspace into another parent")
}
// cannot release a range if it has more than one available sub range
if len(space.availableRanges) > 1 {
return fmt.Errorf("can not release an address space with more than one available range")
}
firstIP := space.availableRanges[0].FirstIP
lastIP := space.availableRanges[0].LastIP
if compareIP4(firstIP, lastIP) > 0 {
return fmt.Errorf("address space first ip %s is greater than last ip %s", firstIP, lastIP)
}
i := 0
for ; i < len(s.availableRanges); i++ {
if compareIP4(lastIP, s.availableRanges[i].FirstIP) < 0 {
if i == 0 {
break
}
if i > 0 && compareIP4(firstIP, s.availableRanges[i-1].LastIP) > 0 {
break
}
}
}
if i > 0 && i == len(s.availableRanges) {
if compareIP4(firstIP, s.availableRanges[i-1].LastIP) <= 0 {
return fmt.Errorf("Could not release IP range")
}
}
s.availableRanges = insertAddressRanges(s.availableRanges, i, space.availableRanges...)
// #nosec: Errors unhandled.
s.Defragment()
log.Infof("Released IP range [%s, %s]", firstIP, lastIP)
return nil
}
// ReleaseIP4 releases the given IPv4 address.
func (s *AddressSpace) ReleaseIP4(ip net.IP) error {
tmp := NewAddressSpaceFromRange(ip, ip)
tmp.Parent = s
return s.ReleaseIP4Range(tmp)
}
func (s *AddressSpace) Defragment() error {
for i := 1; i < len(s.availableRanges); {
first := s.availableRanges[i-1]
second := s.availableRanges[i]
if incrementIP4(first.LastIP).Equal(second.FirstIP) {
first.LastIP = second.LastIP
s.availableRanges = append(s.availableRanges[:i], s.availableRanges[i+1:]...)
} else {
i++
}
}
return nil
}
// Equal compares two address spaces for equality.
func (s *AddressSpace) Equal(other *AddressSpace) bool {
if len(s.availableRanges) != len(other.availableRanges) {
return false
}
for i := 0; i < len(s.availableRanges); i++ {
if compareIP4(s.availableRanges[i].FirstIP, other.availableRanges[i].FirstIP) != 0 ||
compareIP4(s.availableRanges[i].LastIP, other.availableRanges[i].LastIP) != 0 {
return false
}
}
return true
}

View File

@@ -0,0 +1,384 @@
// Copyright 2016 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.
package network
import (
"net"
"testing"
)
func TestIncrementIP4(t *testing.T) {
var tests = []struct {
in net.IP
out net.IP
}{
{net.IPv6loopback, nil},
{net.ParseIP("10.10.10.255"), net.ParseIP("10.10.11.0")},
{net.ParseIP("10.10.255.255"), net.ParseIP("10.11.0.0")},
{net.ParseIP("10.255.255.255"), net.ParseIP("11.0.0.0")},
{net.ParseIP("255.255.255.255"), net.ParseIP("0.0.0.0")},
}
for _, te := range tests {
ip := incrementIP4(te.in)
if !te.out.Equal(ip) {
t.Errorf("got: %s, expected: %s", ip, te.out)
}
}
}
func TestDecrementIP4(t *testing.T) {
var tests = []struct {
in net.IP
out net.IP
}{
{net.IPv6loopback, nil},
{net.ParseIP("10.10.10.0"), net.ParseIP("10.10.9.255")},
{net.ParseIP("10.10.0.0"), net.ParseIP("10.9.255.255")},
{net.ParseIP("10.0.0.0"), net.ParseIP("9.255.255.255")},
{net.ParseIP("0.0.0.0"), net.ParseIP("255.255.255.255")},
}
for _, te := range tests {
ip := decrementIP4(te.in)
if !te.out.Equal(ip) {
t.Errorf("got: %s, expected: %s", ip, te.out)
}
}
}
func TestCompareIP4(t *testing.T) {
ips := []net.IP{
net.ParseIP("10.10.10.10"),
net.ParseIP("10.10.10.9"),
net.ParseIP("10.10.9.9"),
net.ParseIP("10.9.9.9"),
net.ParseIP("9.9.9.9")}
for i := 0; i < len(ips)-1; i++ {
if res := compareIP4(ips[i+1], ips[i]); res != -1 {
t.Fatalf("comparing %s %s got: %v, expected: -1", ips[i+1], ips[i], res)
}
if res := compareIP4(ips[i], ips[i+1]); res != 1 {
t.Fatalf("comparing %s %s got: %v, expected: 1", ips[i], ips[i+1], res)
}
if res := compareIP4(ips[i], ips[i]); res != 0 {
t.Fatalf("comparing %s %s got: %v expected: 0", ips[i], ips[i], res)
}
}
}
func TestIsIP4(t *testing.T) {
ip4 := net.IPv4(10, 10, 10, 10)
if !isIP4(ip4) {
t.Fatalf("ip %s got: false expected: true", ip4)
}
ip6 := net.IPv6loopback
if isIP4(ip6) {
t.Fatalf("ip %s got: true, expected: false", ip6)
}
}
func TestLowestIP4(t *testing.T) {
r := &net.IPNet{IP: net.ParseIP("10.10.10.10").To4(), Mask: net.CIDRMask(24, 32)}
ip := net.ParseIP("10.10.10.0")
if res := lowestIP4(r); !res.Equal(ip) {
t.Errorf("range %s got: %s expected %s", r, res, ip)
}
}
func TestHighestIP4(t *testing.T) {
var tests = []struct {
in *net.IPNet
out net.IP
}{
{&net.IPNet{IP: net.IPv6loopback}, nil},
{&net.IPNet{IP: net.ParseIP("10.10.10.10").To4(), Mask: net.CIDRMask(24, 32)}, net.ParseIP("10.10.10.255")},
}
for _, te := range tests {
if res := highestIP4(te.in); !res.Equal(te.out) {
t.Errorf("range %s got: %s expected %s", te.in, res, te.out)
}
}
}
func TestReserveIP4(t *testing.T) {
space := NewAddressSpaceFromRange(net.ParseIP("10.10.10.10"),
net.ParseIP("10.10.10.11"))
ip, err := space.ReserveNextIP4()
expected := net.ParseIP("10.10.10.10")
if err != nil || !ip.Equal(expected) {
t.Errorf("got: %s, %s expected: %s, nil", ip, err, expected)
}
ip, err = space.ReserveNextIP4()
expected = net.ParseIP("10.10.10.11")
if err != nil || !ip.Equal(expected) {
t.Errorf("got: %s, %s expected: %s, nil", ip, err, expected)
}
ip, err = space.ReserveNextIP4()
if err == nil {
t.Errorf("got: %s, %s expected: nil, error", ip, err)
}
}
func TestReleaseIP4(t *testing.T) {
space := NewAddressSpaceFromRange(net.ParseIP("10.10.10.10"),
net.ParseIP("10.10.10.11"))
ip, err := space.ReserveNextIP4()
expected := net.ParseIP("10.10.10.10")
if err != nil || !ip.Equal(expected) {
t.Errorf("got: %s, %s expected: %s, nil", ip, err, expected)
}
ip, err = space.ReserveNextIP4()
expected = net.ParseIP("10.10.10.11")
if err != nil || !ip.Equal(expected) {
t.Errorf("got: %s, %s expected: %s, nil", ip, err, expected)
}
ip, err = space.ReserveNextIP4()
if err == nil {
t.Errorf("got: %s, %s expected: nil, error", ip, err)
}
err = space.ReleaseIP4(net.ParseIP("10.10.10.10"))
if err != nil {
t.Errorf("got: %s expected: nil", err)
}
err = space.ReleaseIP4(net.ParseIP("10.10.10.10"))
if err == nil {
t.Errorf("got: nil expected: error")
}
err = space.ReleaseIP4(net.ParseIP("10.10.10.11"))
if err != nil {
t.Errorf("got: %s expected: nil", err)
}
ip, err = space.ReserveNextIP4()
expected = net.ParseIP("10.10.10.10")
if err != nil || !ip.Equal(expected) {
t.Errorf("got: %s, %s expected: %s, nil", ip, err, expected)
}
}
func TestReserveNextIP4Net(t *testing.T) {
_, net1, _ := net.ParseCIDR("172.16.0.0/12")
space := NewAddressSpaceFromNetwork(net1)
firstIP := net.IPv4(172, 16, 0, 0)
lastIP := net.IPv4(172, 16, 255, 255)
totalSubspaces := 0
subspace, err := space.ReserveNextIP4Net(net.CIDRMask(16, 32))
for err == nil {
totalSubspaces++
if compareIP4(firstIP, subspace.availableRanges[0].FirstIP) != 0 {
t.Errorf("got: %s, expected: %s", subspace.availableRanges[0].FirstIP, firstIP)
}
if compareIP4(lastIP, subspace.availableRanges[0].LastIP) != 0 {
t.Errorf("got: %s, expected: %s", subspace.availableRanges[0].LastIP, lastIP)
}
firstIP = net.IPv4(172, firstIP[13]+1, 0, 0)
lastIP = net.IPv4(172, lastIP[13]+1, 255, 255)
subspace, err = space.ReserveNextIP4Net(net.CIDRMask(16, 32))
}
if totalSubspaces != 16 {
t.Errorf("got: %d, expected: 16", totalSubspaces)
}
space = NewAddressSpaceFromNetwork(net1)
// peal off one ip from the range
ip, err := space.ReserveNextIP4()
if !ip.Equal(net.ParseIP("172.16.0.0")) {
t.Errorf("got: %s, expected: 172.16.0.0", ip)
}
subSpace, err := space.ReserveNextIP4Net(net.CIDRMask(16, 32))
ip, err = subSpace.ReserveNextIP4()
if compareIP4(ip, net.ParseIP("172.17.0.0")) != 0 {
t.Errorf("got: %s, expected: %s", ip, net.ParseIP("172.17.0.0"))
}
subSpace, err = space.ReserveNextIP4Net(net.CIDRMask(15, 32))
ip, err = subSpace.ReserveNextIP4()
if compareIP4(ip, net.ParseIP("172.18.0.0")) != 0 {
t.Errorf("got: %s, expected: %s", ip, net.ParseIP("172.18.0.0"))
}
}
func TestReserveIP4Net(t *testing.T) {
ipNet := &net.IPNet{IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(12, 32)}
space := NewAddressSpaceFromNetwork(ipNet)
// no mask
_, err := space.ReserveIP4Net(&net.IPNet{IP: net.ParseIP("10.10.10.10")})
if err == nil {
t.Errorf("got: nil, expected: error")
}
// IP == nil, Mask != nil
_, err = space.ReserveIP4Net(&net.IPNet{Mask: net.CIDRMask(12, 32)})
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
_, err = space.ReserveNextIP4()
if err == nil {
t.Errorf("got: nil, expected: error")
}
space = NewAddressSpaceFromNetwork(ipNet)
// ip == "0.0.0.0", Mask != nil
_, err = space.ReserveIP4Net(&net.IPNet{IP: net.ParseIP("0.0.0.0"), Mask: net.CIDRMask(12, 32)})
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
_, err = space.ReserveNextIP4()
if err == nil {
t.Errorf("got: nil, expected: error")
}
space = NewAddressSpaceFromNetwork(ipNet)
// reserve the full space
_, err = space.ReserveIP4Net(ipNet)
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
// no more ips left
_, err = space.ReserveNextIP4()
if err == nil {
t.Errorf("got: nil, expected: error")
}
}
func TestReserveIP4Range(t *testing.T) {
s := NewAddressSpaceFromNetwork(&net.IPNet{IP: net.IPv4(10, 10, 10, 0), Mask: net.CIDRMask(24, 32)})
s.ReserveNextIP4()
// try to reserve an unavailable range
_, err := s.ReserveIP4Range(net.IPv4(10, 10, 10, 0), net.IPv4(10, 10, 10, 255))
if err == nil {
t.Errorf("got: nil, expected: error")
}
}
func TestReleaseIP4Range(t *testing.T) {
_, net1, _ := net.ParseCIDR("172.16.0.0/12")
space := NewAddressSpaceFromNetwork(net1)
err := space.ReleaseIP4Range(nil)
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
// reserve the full range
subSpaces := make([]*AddressSpace, 16)
totalReserved := 0
subSpaces[0], err = space.ReserveNextIP4Net(net.CIDRMask(16, 32))
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
totalReserved++
for i := 1; i < len(subSpaces) && err == nil; i++ {
totalReserved++
subSpaces[i], err = space.ReserveNextIP4Net(net.CIDRMask(16, 32))
}
if totalReserved != 16 {
t.Errorf("got: %d, expected: 16", totalReserved)
}
// release a range at the beginning
err = space.ReleaseIP4Range(subSpaces[0])
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
// try to release an already released range
err = space.ReleaseIP4Range(subSpaces[0])
if err == nil {
t.Errorf("got: nil, expected: error")
}
// release a range in the middle
err = space.ReleaseIP4Range(subSpaces[5])
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
// release a range at the end
err = space.ReleaseIP4Range(subSpaces[len(subSpaces)-1])
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
// try to reserve a released range
subspace, err := space.ReserveNextIP4Net(net.CIDRMask(16, 32))
if err != nil || !subSpaces[0].Equal(subspace) {
t.Fail()
}
space = NewAddressSpaceFromNetwork(net1)
// get a sub space
subSpace, err := space.ReserveNextIP4Net(net.CIDRMask(16, 32))
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
// fragment the sub space
err = subSpace.ReserveIP4(net.ParseIP("172.16.0.2"))
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
// try to release it; should fail
err = space.ReleaseIP4Range(subSpace)
if err == nil {
t.Errorf("got: nil, expected: error")
}
}
func TestDefragment(t *testing.T) {
_, net1, _ := net.ParseCIDR("172.16.0.0/24")
space := NewAddressSpaceFromNetwork(net1)
ip, _ := space.ReserveNextIP4()
if compareIP4(ip, net.ParseIP("172.16.0.0")) != 0 {
t.Errorf("got: %s, expected: %s", ip, net.ParseIP("172.16.0.0"))
}
err := space.ReserveIP4(net.ParseIP("172.16.0.24"))
if err != nil {
t.Errorf("got: %s, expected: nil", err)
}
space.ReleaseIP4(ip)
if len(space.availableRanges) != 2 {
t.Errorf("got: %d, expected: 2", len(space.availableRanges))
}
space.Defragment()
if len(space.availableRanges) != 2 {
t.Errorf("got: %d, expected: 2", len(space.availableRanges))
}
space.ReleaseIP4(net.ParseIP("172.16.0.24"))
if len(space.availableRanges) != 1 {
t.Errorf("got: %d, expected: 1", len(space.availableRanges))
}
space.Defragment()
if len(space.availableRanges) != 1 {
t.Errorf("got: %d, expected: 1", len(space.availableRanges))
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2016 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.
package network
import "net"
type Link interface {
AddrAdd(net.IPNet) error
AddrDel(net.IPNet) error
Attrs() *LinkAttrs
}
type LinkAttrs struct {
Name string
}

View File

@@ -0,0 +1,25 @@
// Copyright 2016 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.
package network
import "fmt"
func LinkByName(name string) (Link, error) {
return nil, fmt.Errorf("not implemented")
}
func LinkByAlias(alias string) (Link, error) {
return nil, fmt.Errorf("not implemented")
}

View File

@@ -0,0 +1,59 @@
// Copyright 2016 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.
package network
import "net"
import "github.com/vishvananda/netlink"
type link struct {
netlink.Link
attrs *LinkAttrs
}
func newLink(l netlink.Link) Link {
attrs := &LinkAttrs{Name: l.Attrs().Name}
return &link{Link: l, attrs: attrs}
}
func LinkByName(name string) (Link, error) {
l, err := netlink.LinkByName(name)
if err != nil {
return nil, err
}
return newLink(l), nil
}
func LinkByAlias(alias string) (Link, error) {
l, err := netlink.LinkByAlias(alias)
if err != nil {
return nil, err
}
return newLink(l), nil
}
func (l *link) AddrAdd(addr net.IPNet) error {
return netlink.AddrAdd(l.Link, &netlink.Addr{IPNet: &addr})
}
func (l *link) AddrDel(addr net.IPNet) error {
return netlink.AddrDel(l.Link, &netlink.Addr{IPNet: &addr})
}
func (l *link) Attrs() *LinkAttrs {
return l.attrs
}

View File

@@ -0,0 +1,25 @@
// Copyright 2016 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.
package network
import "fmt"
func LinkByName(name string) (Link, error) {
return nil, fmt.Errorf("not implemented")
}
func LinkByAlias(alias string) (Link, error) {
return nil, fmt.Errorf("not implemented")
}

View File

@@ -0,0 +1,246 @@
// Copyright 2016-2018 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.
package network
import (
"context"
"fmt"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/portlayer/event"
"github.com/vmware/vic/lib/portlayer/event/events"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/lib/portlayer/store"
"github.com/vmware/vic/pkg/kvstore"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/uid"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
"github.com/vmware/vic/pkg/vsphere/session"
)
var (
DefaultContext *Context
initializer struct {
err error
once sync.Once
}
)
type DuplicateResourceError struct {
resID string
}
type ResourceNotFoundError struct {
error
}
func (e DuplicateResourceError) Error() string {
return fmt.Sprintf("%s already exists", e.resID)
}
func Init(ctx context.Context, sess *session.Session, source extraconfig.DataSource, sink extraconfig.DataSink) error {
trace.End(trace.Begin(""))
initializer.once.Do(func() {
var err error
defer func() {
initializer.err = err
}()
f := find.NewFinder(sess.Vim25(), false)
var config Configuration
config.sink = sink
config.source = source
config.Decode()
config.PortGroups = make(map[string]object.NetworkReference)
log.Debugf("Decoded VCH config for network: %#v", config)
for nn, n := range config.ContainerNetworks {
pgref := new(types.ManagedObjectReference)
if !pgref.FromString(n.ID) {
log.Warnf("Could not reacquire object reference from id for network %s: %s", nn, n.ID)
}
var r object.Reference
if r, err = f.ObjectReference(ctx, *pgref); err != nil {
log.Warnf("could not get network reference for %s network: %s", nn, err)
err = nil
continue
}
config.PortGroups[nn] = r.(object.NetworkReference)
}
// make sure a NIC attached to the bridge network exists
config.BridgeLink, err = getBridgeLink(&config)
if err != nil {
return
}
var kv kvstore.KeyValueStore
kv, err = store.NewDatastoreKeyValue(ctx, sess, "network.contexts.default")
if err != nil {
return
}
var netctx *Context
if netctx, err = NewContext(&config, kv); err != nil {
return
}
if err = engageContext(ctx, netctx, exec.Config.EventManager); err == nil {
DefaultContext = netctx
log.Infof("Default network context allocated")
}
})
return initializer.err
}
// handleEvent processes events
func handleEvent(netctx *Context, ie events.Event) {
switch ie.String() {
case events.ContainerPoweredOff:
op := trace.NewOperation(context.Background(), fmt.Sprintf("handleEvent(%s)", ie.EventID()))
op.Infof("Handling Event: %s", ie.EventID())
// grab the operation from the event
handle := exec.GetContainer(op, uid.Parse(ie.Reference()))
if handle == nil {
_, err := netctx.RemoveIDFromScopes(op, ie.Reference())
if err != nil {
op.Errorf("Failed to remove container %s scope: %s", ie.Reference(), err)
}
return
}
defer handle.Close()
if handle.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOff {
op.Warnf("Live power state check on power off event shows %s: not unbinding network", ie.Reference(), handle.Runtime.PowerState)
return
}
if _, err := netctx.UnbindContainer(op, handle); err != nil {
op.Warnf("Failed to unbind container %s: %s", ie.Reference(), err)
return
}
if err := handle.Commit(op, nil, nil); err != nil {
op.Warnf("Failed to commit handle after network unbind for container %s: %s", ie.Reference(), err)
}
}
return
}
// engageContext connects the given network context into a vsphere environment
// using an event manager, and a container cache. This hooks up a callback to
// react to vsphere events, as well as populate the context with any containers
// that are present.
func engageContext(ctx context.Context, netctx *Context, em event.EventManager) error {
var err error
// grab the context lock so that we do not unbind any containers
// that stop out of band. this could cause, for example, for us
// to bind a container when it has already been unbound by an
// event callback
netctx.Lock()
defer netctx.Unlock()
// subscribe to the event stream for Vm events
if em == nil {
return fmt.Errorf("event manager is required for default network context")
}
sub := fmt.Sprintf("%s(%p)", "netCtx", netctx)
topic := events.NewEventType(events.ContainerEvent{}).Topic()
s := em.Subscribe(topic, sub, func(ie events.Event) {
handleEvent(netctx, ie)
})
defer func() {
if err != nil {
em.Unsubscribe(topic, sub)
}
}()
op := trace.NewOperation(context.Background(), "engageContext")
s.Suspend(true)
defer s.Resume()
for _, c := range exec.Containers.Containers(nil) {
log.Debugf("adding container %s", c)
h := c.NewHandle(ctx)
defer h.Close()
// add any user created networks that show up in container's config
for n, ne := range h.ExecConfig.Networks {
var s []*Scope
s, err = netctx.findScopes(&n)
if err != nil {
if _, ok := err.(ResourceNotFoundError); !ok {
return err
}
}
if len(s) > 0 {
continue
}
pools := make([]string, len(ne.Network.Pools))
for i := range ne.Network.Pools {
pools[i] = ne.Network.Pools[i].String()
}
log.Debugf("adding scope %s", n)
scopeData := &ScopeData{
ScopeType: ne.Network.Type,
Name: n,
Subnet: &ne.Network.Gateway,
Gateway: ne.Network.Gateway.IP,
DNS: ne.Network.Nameservers,
Pools: pools,
}
if _, err = netctx.newScope(scopeData); err != nil {
return err
}
}
if c.CurrentState() == exec.StateRunning {
if _, err = netctx.bindContainer(op, h); err != nil {
return err
}
}
}
return nil
}
func getBridgeLink(config *Configuration) (Link, error) {
// add the gateway address to the bridge interface
link, err := LinkByName(config.BridgeNetwork)
if err != nil {
// lookup by alias
return LinkByAlias(config.BridgeNetwork)
}
return link, nil
}

View File

@@ -0,0 +1,76 @@
// Copyright 2016 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.
package network
import (
"fmt"
"strconv"
"strings"
"github.com/docker/go-connections/nat"
)
type Port string
const NilPort = Port("")
// PortFromMapping constructs the full form of a port mapping
// This has been added to help migrate towards consistent data returns for Endpoint structures
func PortFromMapping(mapping nat.PortMapping) Port {
p := fmt.Sprintf("%s:%s", mapping.Binding.HostPort, string(mapping.Port))
return Port(p)
}
func ParsePort(p string) (Port, error) {
if _, err := Port(p).Port(); err != nil {
return NilPort, err
}
proto := Port(p).Proto()
if proto == "" {
return NilPort, fmt.Errorf("bad port spec %s", p)
}
return Port(p), nil
}
func (p Port) Proto() string {
parts := strings.Split(string(p), ":")
proto, _ := nat.SplitProtoPort(parts[len(parts)-1])
return proto
}
func (p Port) Port() (uint16, error) {
parts := strings.Split(string(p), ":")
_, port := nat.SplitProtoPort(parts[len(parts)-1])
if port == "" {
return 0, fmt.Errorf("bad port spec %s", p)
}
pout, err := strconv.Atoi(port)
if err != nil {
return 0, fmt.Errorf("bad port spec %s", p)
}
return uint16(pout), nil
}
func (p Port) String() string {
parts := strings.Split(string(p), ":")
return string(parts[len(parts)-1])
}
func (p Port) FullString() string {
return string(p)
}

View File

@@ -0,0 +1,404 @@
// 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.
package network
import (
"context"
"encoding/json"
"fmt"
"net"
"sync"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/pkg/ip"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/uid"
)
type Scope struct {
sync.RWMutex
id uid.UID
name string
scopeType string
subnet *net.IPNet
gateway net.IP
dns []net.IP
trustLevel executor.TrustLevel
containers map[uid.UID]*Container
endpoints []*Endpoint
spaces []*AddressSpace
builtin bool
network object.NetworkReference
annotations map[string]string
internal bool
}
func newScope(id uid.UID, scopeType string, network object.NetworkReference, scopeData *ScopeData) *Scope {
return &Scope{
id: id,
name: scopeData.Name,
scopeType: scopeType,
subnet: scopeData.Subnet,
gateway: scopeData.Gateway,
dns: scopeData.DNS,
trustLevel: scopeData.TrustLevel,
network: network,
containers: make(map[uid.UID]*Container),
annotations: make(map[string]string),
internal: scopeData.Internal,
}
}
func (s *Scope) Annotations() map[string]string {
s.RLock()
defer s.RUnlock()
return s.annotations
}
func (s *Scope) Name() string {
s.RLock()
defer s.RUnlock()
return s.name
}
func (s *Scope) ID() uid.UID {
s.RLock()
defer s.RUnlock()
return s.id
}
func (s *Scope) Type() string {
s.RLock()
defer s.RUnlock()
return s.scopeType
}
func (s *Scope) Internal() bool {
s.RLock()
defer s.RUnlock()
return s.internal
}
func (s *Scope) Network() object.NetworkReference {
s.RLock()
defer s.RUnlock()
return s.network
}
func (s *Scope) isDynamic() bool {
return s.scopeType != constants.BridgeScopeType && len(s.spaces) == 0
}
func (s *Scope) Pools() []*ip.Range {
s.RLock()
defer s.RUnlock()
return s.pools()
}
func (s *Scope) TrustLevel() executor.TrustLevel {
s.RLock()
defer s.RUnlock()
return s.trustLevel
}
func (s *Scope) pools() []*ip.Range {
pools := make([]*ip.Range, len(s.spaces))
for i := range s.spaces {
sp := s.spaces[i]
if sp.Network != nil {
r := ip.ParseRange(sp.Network.String())
if r == nil {
continue
}
pools[i] = r
continue
}
pools[i] = sp.Pool
}
return pools
}
func (s *Scope) reserveEndpointIP(e *Endpoint) error {
if s.isDynamic() {
return nil
}
// reserve an ip address
var err error
for _, p := range s.spaces {
if !ip.IsUnspecifiedIP(e.ip) {
if err = p.ReserveIP4(e.ip); err == nil {
return nil
}
} else {
var eip net.IP
if eip, err = p.ReserveNextIP4(); err == nil {
e.ip = eip
return nil
}
}
}
return err
}
func (s *Scope) releaseEndpointIP(e *Endpoint) error {
if s.isDynamic() {
return nil
}
for _, p := range s.spaces {
if err := p.ReleaseIP4(e.ip); err == nil {
if !e.static {
e.ip = net.IPv4(0, 0, 0, 0)
}
return nil
}
}
return fmt.Errorf("could not release IP for endpoint")
}
func (s *Scope) AddContainer(con *Container, e *Endpoint) error {
op := trace.NewOperation(context.Background(), "Add container to the scope")
s.Lock()
defer s.Unlock()
if con == nil {
return fmt.Errorf("container is nil")
}
_, ok := s.containers[con.id]
if ok {
return DuplicateResourceError{resID: con.id.String()}
}
op.Debugf("Adding container %s to the scope %s(%s)",
con.id, s.name, s.id)
if err := s.reserveEndpointIP(e); err != nil {
return err
}
con.addEndpoint(e)
s.endpoints = append(s.endpoints, e)
s.containers[con.id] = con
return nil
}
func (s *Scope) RemoveContainer(con *Container) error {
s.Lock()
defer s.Unlock()
op := trace.NewOperation(context.Background(), "Removing container from the scope")
c, ok := s.containers[con.id]
if !ok || c != con {
op.Debugf("Container %s not found in the scope %s(%s)", con.id, s.name, s.id)
return ResourceNotFoundError{}
}
e := c.Endpoint(s)
if e == nil {
op.Debugf("No scope endpoint for container %s in the scope %s(%s)", con.id, s.name, s.id)
return ResourceNotFoundError{}
}
if err := s.releaseEndpointIP(e); err != nil {
return err
}
delete(s.containers, c.id)
s.endpoints = removeEndpointHelper(e, s.endpoints)
c.removeEndpoint(e)
op.Debugf("Container %s removed from the scope %s(%s)", con.id, s.name, s.id)
return nil
}
func (s *Scope) Containers() []*Container {
s.RLock()
defer s.RUnlock()
containers := make([]*Container, len(s.containers))
i := 0
for _, c := range s.containers {
containers[i] = c
i++
}
return containers
}
func (s *Scope) Container(id uid.UID) *Container {
s.RLock()
defer s.RUnlock()
if c, ok := s.containers[id]; ok {
return c
}
return nil
}
func (s *Scope) ContainerByAddr(addr net.IP) *Endpoint {
s.RLock()
defer s.RUnlock()
if addr == nil || addr.IsUnspecified() {
return nil
}
for _, e := range s.endpoints {
if addr.Equal(e.IP()) {
return e
}
}
return nil
}
func (s *Scope) Endpoints() []*Endpoint {
s.RLock()
defer s.RUnlock()
eps := make([]*Endpoint, len(s.endpoints))
copy(eps, s.endpoints)
return eps
}
func (s *Scope) Subnet() *net.IPNet {
s.RLock()
defer s.RUnlock()
return s.subnet
}
func (s *Scope) Gateway() net.IP {
s.RLock()
defer s.RUnlock()
return s.gateway
}
func (s *Scope) DNS() []net.IP {
s.RLock()
defer s.RUnlock()
return s.dns
}
type scopeJSON struct {
ID uid.UID
Name string
Type string
Subnet *net.IPNet
Gateway net.IP
DNS []net.IP
Builtin bool
Pools []*ip.Range
Annotations map[string]string
Internal bool
}
func (s *Scope) MarshalJSON() ([]byte, error) {
s.RLock()
defer s.RUnlock()
return json.Marshal(&scopeJSON{
ID: s.id,
Name: s.name,
Type: s.scopeType,
Subnet: s.subnet,
Gateway: s.gateway,
DNS: s.dns,
Builtin: s.builtin,
Pools: s.pools(),
Annotations: s.annotations,
Internal: s.internal,
})
}
func (s *Scope) UnmarshalJSON(data []byte) error {
s.Lock()
defer s.Unlock()
var sj scopeJSON
if err := json.Unmarshal(data, &sj); err != nil {
return err
}
ns := Scope{
containers: make(map[uid.UID]*Container),
annotations: make(map[string]string),
}
ns.id = sj.ID
ns.name = sj.Name
ns.scopeType = sj.Type
ns.subnet = sj.Subnet
ns.gateway = sj.Gateway
ns.dns = sj.DNS
ns.builtin = sj.Builtin
ns.spaces = make([]*AddressSpace, len(sj.Pools))
for i := range sj.Pools {
sp := NewAddressSpaceFromRange(sj.Pools[i].FirstIP, sj.Pools[i].LastIP)
if sp == nil {
return fmt.Errorf("invalid pool %s in scope %s", sj.Pools[i].String(), sj.Name)
}
ns.spaces[i] = sp
}
for k, v := range sj.Annotations {
ns.annotations[k] = v
}
ns.internal = sj.Internal
s.swap(&ns)
return nil
}
func (s *Scope) swap(other *Scope) {
s.id, other.id = other.id, s.id
s.name, other.name = other.name, s.name
s.scopeType, other.scopeType = other.scopeType, s.scopeType
s.subnet, other.subnet = other.subnet, s.subnet
s.gateway, other.gateway = other.gateway, s.gateway
s.dns, other.dns = other.dns, s.dns
s.builtin, other.builtin = other.builtin, s.builtin
s.spaces, other.spaces = other.spaces, s.spaces
s.endpoints, other.endpoints = other.endpoints, s.endpoints
s.containers, other.containers = other.containers, s.containers
s.network, other.network = other.network, s.network
s.annotations, other.annotations = other.annotations, s.annotations
s.internal, other.internal = other.internal, s.internal
}

View File

@@ -0,0 +1,195 @@
// Copyright 2016 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.
package network
import (
"context"
"fmt"
"net"
"reflect"
"testing"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/uid"
)
func makeIP(a, b, c, d byte) *net.IP {
i := net.IPv4(a, b, c, d)
return &i
}
var addEthernetCardOrig = addEthernetCard
var addEthernetCardErr = func(_ *exec.Handle, _ *Scope) (types.BaseVirtualDevice, error) {
return nil, fmt.Errorf("")
}
func TestScopeAddRemoveContainer(t *testing.T) {
var err error
ctx, err := NewContext(testConfig(), nil)
if err != nil {
t.Errorf("NewContext() => (nil, %s), want (ctx, nil)", err)
return
}
op := trace.NewOperation(context.Background(), "TestScopeAddRemoveContainer")
s := ctx.defaultScope
idFoo := uid.New()
idBar := uid.New()
var tests1 = []struct {
c *Container
ip *net.IP
out *Endpoint
err error
}{
// no container
{nil, nil, nil, fmt.Errorf("")},
// add a new container to scope
{&Container{id: idFoo}, nil, &Endpoint{ip: net.IPv4(172, 16, 0, 2), scope: s}, nil},
// container already part of scope
{&Container{id: idFoo}, nil, nil, DuplicateResourceError{}},
// container with ip
{&Container{id: idBar}, makeIP(172, 16, 0, 3), &Endpoint{ip: net.IPv4(172, 16, 0, 3), scope: s, static: true}, nil},
}
for _, te := range tests1 {
e := newEndpoint(te.c, s, te.ip, nil)
err = s.AddContainer(te.c, e)
if te.err != nil {
if err == nil {
t.Errorf("s.AddContainer() => (_, nil), want (_, err)")
continue
}
if reflect.TypeOf(err) != reflect.TypeOf(te.err) {
t.Errorf("s.AddContainer() => (_, %v), want (_, %v)", reflect.TypeOf(err), reflect.TypeOf(te.err))
continue
}
if te.c == nil {
continue
}
// for any other error other than DuplicateResourcError
// verify that the container was not added
if _, ok := err.(DuplicateResourceError); !ok {
c := s.Container(te.c.ID())
if c != nil {
t.Errorf("s.Container(%s) => (%v, %v), want (nil, err)", te.c.ID(), c, err)
}
}
continue
}
if !e.IP().Equal(te.out.IP()) {
t.Errorf("s.AddContainer() => e.IP() == %v, want e.IP() == %v", e.IP(), te.out.IP())
continue
}
if !e.Gateway().Equal(te.out.Gateway()) {
t.Errorf("s.AddContainer() => e.Gateway() == %v, want e.Gateway() == %v", e.Gateway(), te.out.Gateway())
continue
}
if e.Subnet().String() != s.Subnet().String() {
t.Errorf("s.AddContainer() => e.Subnet() == %s, want e.Subnet() == %s", e.Subnet(), s.Subnet())
continue
}
if e.static != te.out.static {
t.Errorf("s.AddContainer() => e.static == %#v, want e.static == %#v", e.static, te.out.static)
}
if e.container.ID() != te.c.ID() {
t.Errorf("s.AddContainer() => e.container == %s, want e.container == %s", e.container.ID(), te.c.ID())
continue
}
found := false
for _, e1 := range s.Endpoints() {
if e1 == e {
found = true
break
}
}
if !found {
t.Errorf("s.endpoints does not contain %v", e)
}
c := s.Container(te.c.id)
if c == nil {
t.Errorf("s.Container(%s) => nil, want %v", te.c.ID(), te.c)
continue
}
if c.Endpoint(s) != e {
t.Errorf("container %s does not contain %v", te.c.ID(), e)
}
}
options := &AddContainerOptions{
Scope: ctx.defaultScope.Name(),
}
bound := exec.TestHandle("bound")
ctx.AddContainer(bound, options)
ctx.BindContainer(op, bound)
// test RemoveContainer
var tests2 = []struct {
c *Container
err error
}{
// container not found
{&Container{id: "c1"}, ResourceNotFoundError{}},
// remove a container
{s.Container(idFoo), nil},
}
for _, te := range tests2 {
err = s.RemoveContainer(te.c)
if te.err != nil {
if err == nil {
t.Errorf("s.RemoveContainer() => nil, want %v", te.err)
}
continue
}
// container was removed, verify
if err != nil {
t.Errorf("s.RemoveContainer() => %s, want nil", err)
continue
}
c := s.Container(te.c.ID())
if c != nil {
t.Errorf("s.RemoveContainer() did not remove container %s", te.c.ID())
continue
}
for _, e := range s.endpoints {
if e.container.ID() == te.c.ID() {
t.Errorf("s.RemoveContainer() did not remove endpoint for container %s", te.c.ID())
break
}
}
}
}

182
vendor/github.com/vmware/vic/lib/portlayer/portlayer.go generated vendored Normal file
View File

@@ -0,0 +1,182 @@
// Copyright 2016-2018 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.
package portlayer
import (
"context"
"fmt"
"path"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/lib/portlayer/attach"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/lib/portlayer/logging"
"github.com/vmware/vic/lib/portlayer/metrics"
"github.com/vmware/vic/lib/portlayer/network"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/store"
"github.com/vmware/vic/pkg/retry"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// Init initializes portlayer components at startup
func Init(ctx context.Context, sess *session.Session) error {
defer trace.End(trace.Begin(""))
source, err := extraconfig.GuestInfoSource()
if err != nil {
return err
}
sink, err := extraconfig.GuestInfoSink()
if err != nil {
return err
}
// create or restore a portlayer k/v store in the VCH's directory.
vch, err := guest.GetSelf(ctx, sess)
if err != nil {
return err
}
vchvm := vm.NewVirtualMachineFromVM(ctx, sess, vch)
vmPath, err := vchvm.VMPathName(ctx)
if err != nil {
return err
}
// vmPath is set to the vmx. Grab the directory from that.
vmFolder, err := datastore.ToURL(path.Dir(vmPath))
if err != nil {
return err
}
vmParentPool, err := vchvm.ResourcePool(ctx)
if err != nil {
return err
}
if err = storage.Init(ctx, sess, vmParentPool, source, sink); err != nil {
return err
}
if err = store.Init(ctx, sess, vmFolder); err != nil {
return err
}
if err := exec.Init(ctx, sess, source, sink); err != nil {
return err
}
if err = network.Init(ctx, sess, source, sink); err != nil {
return err
}
if err = logging.Init(ctx); err != nil {
return err
}
if err = metrics.Init(ctx, sess); err != nil {
return err
}
// Unbind containerVM serial ports configured with the old VCH IP.
// Useful when the appliance restarts and the VCH has a different IP.
TakeCareOfSerialPorts(sess)
return nil
}
// TakeCareOfSerialPorts disconnects serial ports backed by network on the VCH's old IP and connects serial ports backed by file.
// This is useful when the appliance or the portlayer restarts and the VCH has a new IP or container vms gets migrated
// Any errors are logged and portlayer init proceeds as usual.
func TakeCareOfSerialPorts(sess *session.Session) {
op := trace.NewOperation(context.Background(), "SerialPorts")
defer trace.End(trace.Begin("", op))
// Get all running containers from the portlayer cache
// Including starting containers here as well
// TODO: for starting containers, if using the runblocking mechanism present as of this date, we should cause the
// unbind change to blocking status to propagate into the container and release the process for start
containers := exec.Containers.Containers([]exec.State{exec.StateRunning, exec.StateStarting})
for i := range containers {
var containerID string
if containers[i].ExecConfig != nil {
containerID = containers[i].ExecConfig.ID
}
op.Infof("unbinding serial port for running container %s", containerID)
operation := func() error {
// Obtain a container handle
handle := containers[i].NewHandle(op)
if handle == nil {
err := fmt.Errorf("unable to obtain a handle for container %s", containerID)
op.Errorf("%s", err)
return err
}
// Unbind the network backed VirtualSerialPort
unbindHandle, err := attach.Unbind(handle, containerID)
if err != nil {
err := fmt.Errorf("unable to unbind serial port for container %s: %s", containerID, err)
op.Errorf("%s", err)
return err
}
execHandle, ok := unbindHandle.(*exec.Handle)
if !ok {
err := fmt.Errorf("handle type assertion failed for container %s", containerID)
op.Errorf("%s", err)
return err
}
// Bind the file backed VirtualSerialPort
bindHandle, err := logging.Bind(execHandle)
if err != nil {
err := fmt.Errorf("unable to unbind serial port for container %s: %s", containerID, err)
op.Errorf("%s", err)
return err
}
execHandle, ok = bindHandle.(*exec.Handle)
if !ok {
err := fmt.Errorf("handle type assertion failed for container %s", containerID)
op.Errorf("%s", err)
return err
}
// Commit the handle
if err := execHandle.Commit(op, sess, nil); err != nil {
op.Errorf("unable to commit handle for container %s: %s", containerID, err)
return err
}
return nil
}
if err := retry.Do(operation, exec.IsConcurrentAccessError); err != nil {
op.Errorf("Multiple attempts failed for committing the handle with %s", err)
}
}
}

View File

@@ -0,0 +1,35 @@
// Copyright 2016 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.
package storage
import (
"github.com/vmware/govmomi/view"
"github.com/vmware/vic/lib/config"
)
var Config Configuration
// Configuration is a slice of the VCH config that is relevant to the storage part of the port layer
type Configuration struct {
// Turn on debug logging
DebugLevel int `vic:"0.1" scope:"read-only" key:"init/diagnostics/debug"`
// Port Layer - storage
config.Storage `vic:"0.1" scope:"read-only" key:"storage"`
// ContainerView
// https://pubs.vmware.com/vsphere-6-0/index.jsp#com.vmware.wssdk.apiref.doc/vim.view.ContainerView.html
ContainerView *view.ContainerView
}

View File

@@ -0,0 +1,177 @@
// Copyright 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.
package container
import (
"errors"
"io"
"net/url"
"os"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/storage/vsphere"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/disk"
"github.com/vmware/vic/pkg/vsphere/vm"
)
func (c *ContainerStore) Export(op trace.Operation, id, ancestor string, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
l, err := c.NewDataSource(op, id)
if err != nil {
return nil, err
}
if ancestor == "" {
op.Infof("No ancestor specified so following basic export path")
return l.Export(op, spec, data)
}
// for now we assume ancetor instead of entirely generic left/right
// this allows us to assume it's an image
img, err := c.images.URL(op, ancestor)
if err != nil {
op.Errorf("Failed to map ancestor %s to image: %s", ancestor, err)
l.Close()
return nil, err
}
op.Debugf("Mapped ancestor %s to %s", ancestor, img.String())
r, err := c.newDataSource(op, img, false)
if err != nil {
op.Debugf("Unable to get datasource for ancestor: %s", err)
l.Close()
return nil, err
}
closers := func() error {
op.Debugf("Callback to io.Closer function for container export")
l.Close()
r.Close()
return nil
}
ls := l.Source()
rs := r.Source()
fl, lok := ls.(*os.File)
fr, rok := rs.(*os.File)
if !lok || !rok {
go closers()
return nil, errors.New("mismatched datasource types")
}
// if we want data, exclude the xattrs, otherwise assume diff
xattrs := !data
tar, err := archive.Diff(op, fl.Name(), fr.Name(), spec, data, xattrs)
if err != nil {
go closers()
return nil, err
}
return &storage.ProxyReadCloser{
ReadCloser: tar,
Closer: closers,
}, nil
}
// NewDataSource creates and returns an DataSource associated with container storage
func (c *ContainerStore) NewDataSource(op trace.Operation, id string) (storage.DataSource, error) {
uri, err := c.URL(op, id)
if err != nil {
return nil, err
}
offlineAttempt := 0
offline:
offlineAttempt++
// This is persistent to avoid issues with concurrent Stat/Import calls
source, err := c.newDataSource(op, uri, true)
if err == nil {
return source, err
}
// check for vmdk locked error here
if !disk.IsLockedError(err) {
op.Warnf("Unable to mount %s and do not know how to recover from error")
// continue anyway because maybe there's an online option
}
// online - Owners() should filter out the appliance VM
// #nosec: Errors unhandled.
owners, _ := c.Owners(op, uri, disk.LockedVMDKFilter)
if len(owners) == 0 {
op.Infof("No online owners were found for %s", id)
return nil, errors.New("unable to create offline data source and no online owners found")
}
for _, o := range owners {
// sanity check to see if we are the owner - this should catch transitions
// from container running to diff or commit for example between the offline attempt and here
uuid, err := o.UUID(op)
if err == nil {
// check if the vm is appliance VM if we can successfully get its UUID
// #nosec: Errors unhandled.
self, _ := guest.IsSelf(op, uuid)
if self && offlineAttempt < 2 {
op.Infof("Appliance is owner of online vmdk - retrying offline source path")
goto offline
}
}
online, err := c.newOnlineDataSource(op, o, id)
if online != nil {
return online, err
}
op.Debugf("Failed to create online datasource with owner %s: %s", o.Reference(), err)
}
return nil, errors.New("unable to create online or offline data source")
}
func (c *ContainerStore) newDataSource(op trace.Operation, url *url.URL, persistent bool) (storage.DataSource, error) {
mountPath, cleanFunc, err := c.Mount(op, url, persistent)
if err != nil {
return nil, err
}
f, err := os.Open(mountPath)
if err != nil {
cleanFunc()
return nil, err
}
op.Debugf("Created mount data source for access to %s at %s", url, mountPath)
return storage.NewMountDataSource(op, f, cleanFunc), nil
}
func (c *ContainerStore) newOnlineDataSource(op trace.Operation, owner *vm.VirtualMachine, id string) (storage.DataSource, error) {
op.Debugf("Constructing toolbox data source: %s.%s", owner.Reference(), id)
return &vsphere.ToolboxDataSource{
VM: owner,
ID: id,
Clean: func() { return },
}, nil
}

View File

@@ -0,0 +1,120 @@
// Copyright 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.
package container
import (
"errors"
"io"
"net/url"
"os"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/storage/vsphere"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/disk"
"github.com/vmware/vic/pkg/vsphere/vm"
)
func (c *ContainerStore) Import(op trace.Operation, id string, spec *archive.FilterSpec, tarstream io.ReadCloser) error {
l, err := c.NewDataSink(op, id)
if err != nil {
return err
}
return l.Import(op, spec, tarstream)
}
// NewDataSink creates and returns an DataSink associated with container storage
func (c *ContainerStore) NewDataSink(op trace.Operation, id string) (storage.DataSink, error) {
uri, err := c.URL(op, id)
if err != nil {
return nil, err
}
offlineAttempt := 0
offline:
offlineAttempt++
sink, err := c.newDataSink(op, uri)
if err == nil {
return sink, err
}
// check for vmdk locked error here
if !disk.IsLockedError(err) {
op.Warnf("Unable to mount %s and do not know how to recover from error")
// continue anyway because maybe there's an online option
}
// online - Owners() should filter out the appliance VM
// #nosec: Errors unhandled.
owners, _ := c.Owners(op, uri, disk.LockedVMDKFilter)
if len(owners) == 0 {
op.Infof("No online owners were found for %s", id)
return nil, errors.New("unable to create offline data sink and no online owners found")
}
for _, o := range owners {
// sanity check to see if we are the owner - this should catch transitions
// from container running to diff or commit for example between the offline attempt and here
uuid, err := o.UUID(op)
if err == nil {
// check if the vm is appliance VM if we can successfully get its UUID
// #nosec: Errors unhandled.
self, _ := guest.IsSelf(op, uuid)
if self && offlineAttempt < 2 {
op.Infof("Appliance is owner of online vmdk - retrying offline source path")
goto offline
}
}
online, err := c.newOnlineDataSink(op, o, id)
if online != nil {
return online, err
}
op.Debugf("Failed to create online datasink with owner %s: %s", o.Reference(), err)
}
return nil, errors.New("unable to create online or offline data sink")
}
func (c *ContainerStore) newDataSink(op trace.Operation, url *url.URL) (storage.DataSink, error) {
mountPath, cleanFunc, err := c.Mount(op, url, true)
if err != nil {
return nil, err
}
f, err := os.Open(mountPath)
if err != nil {
cleanFunc()
return nil, err
}
op.Debugf("Created mount data sink for access to %s at %s", url, mountPath)
return storage.NewMountDataSink(op, f, cleanFunc), nil
}
func (c *ContainerStore) newOnlineDataSink(op trace.Operation, owner *vm.VirtualMachine, id string) (storage.DataSink, error) {
op.Debugf("Constructing toolbox data sink: %s.%s", owner.Reference(), id)
return &vsphere.ToolboxDataSink{
VM: owner,
ID: id,
Clean: func() { return },
}, nil
}

View File

@@ -0,0 +1,90 @@
// Copyright 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.
package container
import (
"errors"
"net/url"
"strings"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/disk"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// ContainerStorer defines the interface contract expected to allow import and export
// against containers
type ContainerStorer interface {
storage.Resolver
storage.Importer
storage.Exporter
}
// ContainerStore stores container storage information
type ContainerStore struct {
disk.Vmdk
// used to resolve images when diffing
images storage.Resolver
}
// NewContainerStore creates and returns a new container store
func NewContainerStore(op trace.Operation, s *session.Session, imageResolver storage.Resolver) (*ContainerStore, error) {
dm, err := disk.NewDiskManager(op, s, storage.Config.ContainerView)
if err != nil {
return nil, err
}
cs := &ContainerStore{
Vmdk: disk.Vmdk{
Manager: dm,
//ds: ds,
Session: s,
},
images: imageResolver,
}
return cs, nil
}
// URL converts the id of a resource to a URL
func (c *ContainerStore) URL(op trace.Operation, id string) (*url.URL, error) {
// using diskfinder with a basic suffix match is an inefficient and potentially error prone way of doing this
// mapping, but until the container store has a structured means of knowing this information it's at least
// not going to be incorrect without an ID collision.
dsPath, err := c.DiskFinder(op, func(filename string) bool {
return strings.HasSuffix(filename, id+".vmdk")
})
if err != nil {
return nil, err
}
return &url.URL{
Scheme: "ds",
Path: dsPath,
}, nil
}
// Owners returns a list of VMs that are using the resource specified by `url`
func (c *ContainerStore) Owners(op trace.Operation, url *url.URL, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
if url.Scheme != "ds" {
return nil, errors.New("vmdk path must be a datastore url with \"ds\" scheme")
}
return c.Vmdk.Owners(op, url, filter)
}

View File

@@ -0,0 +1,32 @@
// Copyright 2018 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.
package image
type ErrImageInUse struct {
Msg string
}
func (e *ErrImageInUse) Error() string {
return e.Msg
}
func IsErrImageInUse(err error) bool {
if err == nil {
return false
}
_, ok := err.(*ErrImageInUse)
return ok
}

View File

@@ -0,0 +1,202 @@
// 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.
package image
import (
"errors"
"io"
"net/url"
"path/filepath"
"strings"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/pkg/index"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/disk"
)
// ImageStorer is an interface to store images in the Image Store
type ImageStorer interface {
// CreateImageStore creates a location to store images and creates a root
// disk which serves as the parent of all layers.
//
// storeName - The name of the image store to be created. This must be
// unique.
//
// Returns the URL of the created store
CreateImageStore(op trace.Operation, storeName string) (*url.URL, error)
// DeleteImageStore is used to cleanup the image store. This can only be
// called once there are no images left in the image store.
DeleteImageStore(op trace.Operation, storeName string) error
// Gets the url to an image store via name
GetImageStore(op trace.Operation, storeName string) (*url.URL, error)
// ListImageStores lists the available image stores
ListImageStores(op trace.Operation) ([]*url.URL, error)
// WriteImage creates a new image layer from the given parent. Eg
// parentImage + newLayer = new Image built from parent
//
// parent - The parent image to create the new image from.
// ID - textual ID for the image to be written
// meta - metadata associated with the image
// sum - expected sha266 sum of the image content.
// r - the image tar to be written
WriteImage(op trace.Operation, parent *Image, ID string, meta map[string][]byte, sum string, r io.Reader) (*Image, error)
// GetImage queries the image store for the specified image.
//
// store - The image store to query name - The name of the image (optional)
// ID - textual ID for the image to be retrieved
GetImage(op trace.Operation, store *url.URL, ID string) (*Image, error)
// ListImages returns a list of Images given a list of image IDs, or all
// images in the image store if no param is passed.
ListImages(op trace.Operation, store *url.URL, IDs []string) ([]*Image, error)
// DeleteImage deletes an image from the image store. If the image is in
// use either by way of inheritance or because it's attached to a
// container, this will return an error.
DeleteImage(op trace.Operation, image *Image) (*Image, error)
storage.Resolver
storage.Importer
storage.Exporter
}
// Image is the handle to identify an image layer on the backing store. The
// URI namespace used to identify the Image in the storage layer has the
// following path scheme:
//
// `/storage/<image store identifier, usually the vch uuid>/<image id>`
//
type Image struct {
// ID is the identifier for this layer. Usually a SHA
ID string
// SelfLink is the URL for this layer. Filled in by the runtime.
SelfLink *url.URL
// ParentLink is the URL for the parent. It's the VMDK this snapshot inherits from.
ParentLink *url.URL
// Store is the URL for the image store the image can be found on.
Store *url.URL
// Metadata associated with the image.
Metadata map[string][]byte
// Disk is the underlying disk implementation
Disk *disk.VirtualDisk
// DatastorePath is the dspath for actually using this image
// NOTE: this should be replaced by structure accessors for the data and updated storage
// interfaces that use _one_ variant of url/path for identifying images, volumes and stores.
// URL was only suggested as an existing structure that could be leveraged when object.DatastorePath
// was note available. The suggestion seems to have spawned monstruous unnecessary complexity.
DatastorePath *object.DatastorePath
}
func (i *Image) Copy() index.Element {
// #nosec: Errors unhandled.
selflink, _ := url.Parse(i.SelfLink.String())
// #nosec: Errors unhandled.
store, _ := url.Parse(i.Store.String())
var parent *url.URL
if i.ParentLink != nil {
// #nosec: Errors unhandled.
parent, _ = url.Parse(i.ParentLink.String())
}
c := &Image{
ID: i.ID,
SelfLink: selflink,
ParentLink: parent,
Store: store,
DatastorePath: &object.DatastorePath{
Datastore: i.DatastorePath.Datastore,
Path: i.DatastorePath.Path,
},
}
if i.Metadata != nil {
c.Metadata = make(map[string][]byte)
for k, v := range i.Metadata {
buf := make([]byte, len(v))
copy(buf, v)
c.Metadata[k] = buf
}
}
return c
}
// Returns the Selflink of the image
func (i *Image) Self() string {
return i.SelfLink.String()
}
// Returns a link to the parent. Returns link to self if there is no parent
func (i *Image) Parent() string {
if i.ParentLink != nil {
return i.ParentLink.String()
}
return i.Self()
}
func Parse(u *url.URL) (*Image, error) {
// Check the path isn't malformed.
if !filepath.IsAbs(u.Path) {
return nil, errors.New("invalid uri path")
}
segments := strings.Split(filepath.Clean(u.Path), "/")
if segments[0] == "" {
segments = segments[1:]
}
if segments[0] != util.StorageURLPath {
return nil, errors.New("not a storage path")
}
if len(segments) < 3 {
return nil, errors.New("uri path mismatch")
}
store, err := util.ImageStoreNameToURL(segments[2])
if err != nil {
return nil, err
}
id := segments[3]
var SelfLink url.URL
SelfLink = *u
i := &Image{
ID: id,
SelfLink: &SelfLink,
Store: store,
}
return i, nil
}

View File

@@ -0,0 +1,508 @@
// 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.
package image
import (
"errors"
"fmt"
"io"
"net/url"
"os"
"sync"
"time"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/pkg/index"
"github.com/vmware/vic/pkg/retry"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/tasks"
"github.com/vmware/vic/pkg/vsphere/vm"
)
var ErrCorruptImageStore = errors.New("Corrupt image store")
// NameLookupCache the global view of all of the image stores. To avoid unnecessary
// lookups, the image cache keeps an in memory map of the store URI to the map
// of images on disk.
type NameLookupCache struct {
// The individual store locations -> Index
storeCache map[url.URL]*index.Index
// Guard against concurrent writes to the storeCache map
storeCacheLock sync.Mutex
// The image store implementation. This mutates the actual disk images.
DataStore ImageStorer
}
func NewLookupCache(ds ImageStorer) *NameLookupCache {
return &NameLookupCache{
DataStore: ds,
storeCache: make(map[url.URL]*index.Index),
}
}
// isRetry will check the error for retryability - if so reset the cache
func (c *NameLookupCache) isRetry(op trace.Operation, err error) bool {
if tasks.IsRetryError(op, err) {
op.Debugf("%s is retryable, resetting store cache", err)
c.storeCache = make(map[url.URL]*index.Index)
return true
}
return false
}
// GetImageStore checks to see if a named image store exists and returns the
// URL to it if so or error.
func (c *NameLookupCache) GetImageStore(op trace.Operation, storeName string) (*url.URL, error) {
defer trace.End(trace.Begin(fmt.Sprintf("StoreName: %s", storeName), op))
store, err := util.ImageStoreNameToURL(storeName)
if err != nil {
return nil, err
}
c.storeCacheLock.Lock()
defer c.storeCacheLock.Unlock()
// check the cache
_, ok := c.storeCache[*store]
if !ok {
op.Infof("Refreshing image cache from datastore.")
// Store isn't in the cache. Look it up in the datastore.
storeName, err := util.ImageStoreName(store)
if err != nil {
return nil, err
}
// If the store doesn't exist, we'll fall out here.
_, err = c.DataStore.GetImageStore(op, storeName)
if err != nil {
return nil, err
}
indx := index.NewIndex()
c.storeCache[*store] = indx
// Add Scratch
scratch, err := c.DataStore.GetImage(op, store, constants.ScratchLayerID)
if err != nil {
op.Errorf("ImageCache Error: looking up scratch on %s: %s", store.String(), err)
if c.isRetry(op, err) {
return nil, err
}
// potentially a recoverable error
return nil, ErrCorruptImageStore
}
if err = indx.Insert(scratch); err != nil {
return nil, err
}
// XXX after creating the indx and populating the map, we can put the rest in a go routine
images, err := c.DataStore.ListImages(op, store, nil)
if err != nil {
// if error is retryable we'll reset the cache
c.isRetry(op, err)
return nil, err
}
op.Debugf("Found %d images", len(images))
// Build image map to simplify tree traversal.
imageMap := make(map[string]*Image, len(images))
for _, img := range images {
if img.ID == constants.ScratchLayerID {
continue
}
imageMap[img.Self()] = img
}
for k := range imageMap {
parentTree(op, k, indx, imageMap)
}
}
return store, nil
}
// parentTree adds images into the cache starting from the parent.
func parentTree(op trace.Operation, imgLink string, idx *index.Index, imageMap map[string]*Image) {
img, ok := imageMap[imgLink]
if !ok {
return
}
if img.Parent() != img.Self() {
op.Debugf("Looking for parent %s for %s", img.Parent(), img.Self())
parentTree(op, img.Parent(), idx, imageMap)
}
if err := idx.Insert(img); err != nil {
op.Errorf("Could not insert image %s: %v", imgLink, err)
} else {
op.Infof("Added image %s on datastore.", imgLink)
}
delete(imageMap, imgLink)
}
func (c *NameLookupCache) CreateImageStore(op trace.Operation, storeName string) (*url.URL, error) {
store, err := util.ImageStoreNameToURL(storeName)
if err != nil {
return nil, err
}
// GetImageStore Operation is able to be retried...
getStore := func() error {
// Check for existence and rehydrate the cache if it exists on disk.
_, err = c.GetImageStore(op, storeName)
return err
}
// is the error retryable
isRetry := func(err error) bool {
return tasks.IsRetryError(op, err)
}
config := retry.NewBackoffConfig()
config.InitialInterval = time.Second * 15
config.MaxInterval = time.Second * 30
config.MaxElapsedTime = time.Minute * 3
// attempt to get the image store
err = retry.DoWithConfig(getStore, isRetry, config)
if err == nil {
// no error means that the image store exists and we can
// safely return
return nil, os.ErrExist
}
// if the image store doesn't exist or is corrupt we will continue,
// otherwise fail here
if err != os.ErrNotExist && err != ErrCorruptImageStore {
op.Errorf("Error getting image store %s: %s", storeName, err)
return nil, err
}
c.storeCacheLock.Lock()
defer c.storeCacheLock.Unlock()
store, err = c.DataStore.CreateImageStore(op, storeName)
if err != nil {
return nil, err
}
// Create the root image
scratch, err := c.DataStore.WriteImage(op, &Image{Store: store}, constants.ScratchLayerID, nil, "", nil)
if err != nil {
// if we failed here, remove the image store
op.Infof("Removing failed image store %s", storeName)
if e := c.DataStore.DeleteImageStore(op, storeName); e != nil {
op.Errorf("image store cleanup failed: %s", e.Error())
}
return nil, err
}
indx := index.NewIndex()
c.storeCache[*store] = indx
if err = indx.Insert(scratch); err != nil {
return nil, err
}
return store, nil
}
// ListImageStores returns a list of strings representing all existing image stores
func (c *NameLookupCache) ListImageStores(op trace.Operation) ([]*url.URL, error) {
c.storeCacheLock.Lock()
defer c.storeCacheLock.Unlock()
stores := make([]*url.URL, 0, len(c.storeCache))
for key := range c.storeCache {
stores = append(stores, &key)
}
return stores, nil
}
func (c *NameLookupCache) WriteImage(op trace.Operation, parent *Image, ID string, meta map[string][]byte, sum string, r io.Reader) (*Image, error) {
// Check the parent exists (at least in the cache).
p, err := c.GetImage(op, parent.Store, parent.ID)
if err != nil {
return nil, fmt.Errorf("parent (%s) doesn't exist in %s: %s", parent.ID, parent.Store.String(), err)
}
// Check the image doesn't already exist in the cache. A miss in this will trigger a datastore lookup.
i, err := c.GetImage(op, p.Store, ID)
if err == nil && i != nil {
// TODO(FA) check sums to make sure this is the right image
return i, nil
}
// Definitely not in cache or image store, create image.
i, err = c.DataStore.WriteImage(op, p, ID, meta, sum, r)
if err != nil {
op.Errorf("WriteImage of %s failed with: %s", ID, err)
return nil, err
}
c.storeCacheLock.Lock()
indx := c.storeCache[*parent.Store]
c.storeCacheLock.Unlock()
// Add the new image to the cache
if err = indx.Insert(i); err != nil {
return nil, err
}
return i, nil
}
func (c *NameLookupCache) Export(op trace.Operation, store *url.URL, id, ancestor string, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
return c.DataStore.Export(op, id, ancestor, spec, data)
}
func (c *NameLookupCache) Import(op trace.Operation, store *url.URL, diskID string, spec *archive.FilterSpec, tarStream io.ReadCloser) error {
return c.DataStore.Import(op, diskID, spec, tarStream)
}
func (c *NameLookupCache) NewDataSource(op trace.Operation, id string) (storage.DataSource, error) {
return c.DataStore.NewDataSource(op, id)
}
func (c *NameLookupCache) URL(op trace.Operation, id string) (*url.URL, error) {
return c.DataStore.URL(op, id)
}
func (c *NameLookupCache) Owners(op trace.Operation, url *url.URL, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
return c.DataStore.Owners(op, url, filter)
}
// GetImage gets the specified image from the given store by retreiving it from the cache.
func (c *NameLookupCache) GetImage(op trace.Operation, store *url.URL, ID string) (*Image, error) {
op.Debugf("Getting image %s from %s", ID, store.String())
storeName, err := util.ImageStoreName(store)
if err != nil {
return nil, err
}
// Check the store exists
if _, err = c.GetImageStore(op, storeName); err != nil {
return nil, err
}
c.storeCacheLock.Lock()
indx := c.storeCache[*store]
c.storeCacheLock.Unlock()
imgURL, err := util.ImageURL(storeName, ID)
if err != nil {
return nil, err
}
node, err := c.storeCache[*store].Get(imgURL.String())
var img *Image
if err != nil {
if err == index.ErrNodeNotFound {
op.Debugf("Image %s not in cache, retreiving from datastore", ID)
// Not in the cache. Try to load it.
img, err = c.DataStore.GetImage(op, store, ID)
if err != nil {
return nil, err
}
if err = indx.Insert(img); err != nil {
return nil, err
}
} else {
return nil, err
}
} else {
img, _ = node.(*Image)
}
return img, nil
}
// ListImages returns a list of Images for a list of IDs, or all if no IDs are passed
func (c *NameLookupCache) ListImages(op trace.Operation, store *url.URL, IDs []string) ([]*Image, error) {
// Filter the results
imageList := make([]*Image, 0, len(IDs))
if len(IDs) > 0 {
for _, id := range IDs {
i, err := c.GetImage(op, store, id)
if err == nil {
imageList = append(imageList, i)
}
}
} else {
storeName, err := util.ImageStoreName(store)
if err != nil {
return nil, err
}
// Check the store exists before we start iterating it. This will populate the cache if it's empty.
if _, err := c.GetImageStore(op, storeName); err != nil {
return nil, err
}
// get the relevant cache
c.storeCacheLock.Lock()
indx := c.storeCache[*store]
c.storeCacheLock.Unlock()
images, err := indx.List()
if err != nil {
return nil, err
}
for _, v := range images {
img, _ := v.(*Image)
// filter out scratch
if img.ID == constants.ScratchLayerID {
continue
}
imageList = append(imageList, img)
}
}
return imageList, nil
}
// DeleteImage deletes an image from the image store. If it is in use or is
// being inheritted from, then this will return an error.
func (c *NameLookupCache) DeleteImage(op trace.Operation, image *Image) (*Image, error) {
// prevent deletes of scratch
if image.ID == constants.ScratchLayerID {
return nil, nil
}
op.Infof("DeleteImage: deleting %s", image.Self())
// Check the image exists. This will rehydrate the cache if necessary.
img, err := c.GetImage(op, image.Store, image.ID)
if err != nil {
op.Errorf("DeleteImage: %s", err)
return nil, err
}
// get the relevant cache
c.storeCacheLock.Lock()
indx := c.storeCache[*img.Store]
c.storeCacheLock.Unlock()
hasChildren, err := indx.HasChildren(img.Self())
if err != nil {
op.Errorf("DeleteImage: %s", err)
return nil, err
}
if hasChildren {
return nil, &ErrImageInUse{img.Self() + " in use by child images"}
}
// The datastore will tell us if the image is attached
if _, err = c.DataStore.DeleteImage(op, img); err != nil {
op.Errorf("%s", err)
return nil, err
}
// Remove the image from the cache
if _, err = indx.Delete(img.Self()); err != nil {
op.Errorf("%s", err)
return nil, err
}
return img, nil
}
// DeleteBranch deletes a branch of images, starting from nodeID, up to the
// first node with degree greater than 1. keepNodes is the array of images to
// keep (and their branches).
func (c *NameLookupCache) DeleteBranch(op trace.Operation, image *Image, keepNodes []*url.URL) ([]*Image, error) {
op.Infof("DeleteBranch: deleting branch starting at %s", image.Self())
var deletedImages []*Image
// map of images to keep
keep := make(map[url.URL]int)
for _, elem := range keepNodes {
op.Debugf("DeleteBranch: keep node %s", elem.String())
keep[*elem] = 0
}
// Check if the error is actually an error. If we deleted something,
// then eat the error. This should really only return an error if the leaf
// has issues.
checkErr := func(err error, deleted []*Image) ([]*Image, error) {
if err != nil {
if len(deleted) == 0 {
// we failed deleting any elements.
return nil, err
}
}
if len(deleted) == 0 {
// This can't happen. deleteNode should have returned an err
op.Debugf("No images deleted!!")
}
// we deleted a section of a branch
return deleted, nil
}
for {
if _, ok := keep[*image.SelfLink]; ok {
return checkErr(fmt.Errorf("%s can't be deleted", image.Self()), deletedImages)
}
deletedImage, err := c.DeleteImage(op, image)
if err != nil {
op.Debugf(err.Error())
return checkErr(err, deletedImages)
}
deletedImages = append(deletedImages, deletedImage)
// iterate to the parent
parent, err := Parse(deletedImage.ParentLink)
if err != nil {
return deletedImages, err
}
// set image to the parent
image, err = c.GetImage(op, parent.Store, parent.ID)
if err != nil {
return deletedImages, err
}
if image.ID == constants.ScratchLayerID {
op.Infof("DeleteBranch: Done deleting images")
break
}
}
return deletedImages, nil
}

View File

@@ -0,0 +1,690 @@
// 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.
package image
import (
"context"
"fmt"
"io"
"net/url"
"os"
"strconv"
"testing"
"github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/vm"
)
type MockDataStore struct {
// id -> image
db map[url.URL]map[string]*Image
createImageStoreError error
writeImageError error
}
func NewMockDataStore() *MockDataStore {
m := &MockDataStore{
db: make(map[url.URL]map[string]*Image),
}
return m
}
// GetImageStore checks to see if a named image store exists and returls the
// URL to it if so or error.
func (c *MockDataStore) GetImageStore(op trace.Operation, storeName string) (*url.URL, error) {
u, err := util.ImageStoreNameToURL(storeName)
if err != nil {
return nil, err
}
if _, ok := c.db[*u]; !ok {
return nil, os.ErrNotExist
}
return u, nil
}
func (c *MockDataStore) CreateImageStore(op trace.Operation, storeName string) (*url.URL, error) {
if c.createImageStoreError != nil {
return nil, c.createImageStoreError
}
u, err := util.ImageStoreNameToURL(storeName)
if err != nil {
return nil, err
}
c.db[*u] = make(map[string]*Image)
return u, nil
}
func (c *MockDataStore) DeleteImageStore(op trace.Operation, storeName string) error {
u, err := util.ImageStoreNameToURL(storeName)
if err != nil {
return err
}
c.db[*u] = nil
return nil
}
func (c *MockDataStore) ListImageStores(op trace.Operation) ([]*url.URL, error) {
return nil, nil
}
func (c *MockDataStore) WriteImage(op trace.Operation, parent *Image, ID string, meta map[string][]byte, sum string, r io.Reader) (*Image, error) {
if c.writeImageError != nil {
op.Infof("WriteImage: returning error")
return nil, c.writeImageError
}
storeName, err := util.ImageStoreName(parent.Store)
if err != nil {
return nil, err
}
selflink, err := util.ImageURL(storeName, ID)
if err != nil {
return nil, err
}
var parentLink *url.URL
if parent.ID != "" {
parentLink, err = util.ImageURL(storeName, parent.ID)
if err != nil {
return nil, err
}
}
i := &Image{
ID: ID,
Store: parent.Store,
ParentLink: parentLink,
SelfLink: selflink,
Metadata: meta,
}
c.db[*parent.Store][ID] = i
return i, nil
}
// GetImage gets the specified image from the given store by retreiving it from the cache.
func (c *MockDataStore) GetImage(op trace.Operation, store *url.URL, ID string) (*Image, error) {
i, ok := c.db[*store][ID]
if !ok {
return nil, fmt.Errorf("not found")
}
return i, nil
}
// ListImages resturns a list of Images for a list of IDs, or all if no IDs are passed
func (c *MockDataStore) ListImages(op trace.Operation, store *url.URL, IDs []string) ([]*Image, error) {
var imageList []*Image
for _, i := range c.db[*store] {
imageList = append(imageList, i)
}
return imageList, nil
}
// DeleteImage removes an image from the image store
func (c *MockDataStore) DeleteImage(op trace.Operation, image *Image) (*Image, error) {
delete(c.db[*image.Store], image.ID)
return image, nil
}
func (c *MockDataStore) Export(op trace.Operation, child, ancestor string, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
return nil, nil
}
func (c *MockDataStore) Import(op trace.Operation, id string, spec *archive.FilterSpec, tarstream io.ReadCloser) error {
return nil
}
func (c *MockDataStore) NewDataSink(op trace.Operation, id string) (storage.DataSink, error) {
return nil, nil
}
func (c *MockDataStore) NewDataSource(op trace.Operation, id string) (storage.DataSource, error) {
return nil, nil
}
func (c *MockDataStore) URL(op trace.Operation, id string) (*url.URL, error) {
return nil, nil
}
func (c *MockDataStore) Owners(op trace.Operation, url *url.URL, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
return nil, nil
}
func TestListImages(t *testing.T) {
s := NewLookupCache(NewMockDataStore())
op := trace.NewOperation(context.Background(), "test")
storeURL, err := s.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) {
return
}
if !assert.NotNil(t, storeURL) {
return
}
// Create a set of images
images := make(map[string]*Image)
parent := Image{
ID: constants.ScratchLayerID,
}
parent.Store = storeURL
testSum := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
for i := 1; i < 50; i++ {
id := fmt.Sprintf("ID-%d", i)
img, werr := s.WriteImage(op, &parent, id, nil, testSum, nil)
if !assert.NoError(t, werr) {
return
}
if !assert.NotNil(t, img) {
return
}
images[id] = img
}
// List all images
outImages, err := s.ListImages(op, storeURL, nil)
if !assert.NoError(t, err) {
return
}
// check we retrieve all of the iamges
assert.Equal(t, len(outImages), len(images))
for _, img := range outImages {
_, ok := images[img.ID]
if !assert.True(t, ok) {
return
}
}
// Check we can retrieve a subset
inIDs := []string{"ID-1", "ID-2", "ID-3"}
outImages, err = s.ListImages(op, storeURL, inIDs)
if !assert.NoError(t, err) {
return
}
for _, img := range outImages {
reference, ok := images[img.ID]
if !assert.True(t, ok) {
return
}
if !assert.Equal(t, reference, img) {
return
}
}
}
// Create an image on the datastore directly and try to WriteImage via the
// cache. The datastore should reflect the image already exists and bale out
// without an error.
func TestOutsideCacheWriteImage(t *testing.T) {
s := NewLookupCache(NewMockDataStore())
op := trace.NewOperation(context.Background(), "test")
storeURL, err := s.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) {
return
}
if !assert.NotNil(t, storeURL) {
return
}
// Create a set of images
images := make(map[string]*Image)
parent := Image{
ID: constants.ScratchLayerID,
}
parent.Store = storeURL
for i := 1; i < 50; i++ {
id := fmt.Sprintf("ID-%d", i)
// Write to the datastore creating images
img, werr := s.DataStore.WriteImage(op, &parent, id, nil, "", nil)
if !assert.NoError(t, werr) {
return
}
if !assert.NotNil(t, img) {
return
}
images[id] = img
}
testSum := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
// Try to write the same images as above, but this time via the cache. WriteImage should return right away without any data written.
for i := 1; i < 50; i++ {
id := fmt.Sprintf("ID-%d", i)
// Write to the datastore creating images
img, werr := s.WriteImage(op, &parent, id, nil, testSum, nil)
if !assert.NoError(t, werr) {
return
}
if !assert.NotNil(t, img) {
return
}
// assert it's the same image
if !assert.Equal(t, images[img.ID], img) {
return
}
}
}
// Create 2 store caches but use the same backing datastore. Create images
// with the first cache, then get the image with the second. This simulates
// restart since the second cache is empty and has to go to the backing store.
func TestImageStoreRestart(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
ds := NewMockDataStore()
op := trace.NewOperation(context.Background(), "test")
firstCache := NewLookupCache(ds)
secondCache := NewLookupCache(ds)
storeURL, err := firstCache.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) {
return
}
if !assert.NotNil(t, storeURL) {
return
}
// Create a set of images
expectedImages := make(map[string]*Image)
parent, err := firstCache.GetImage(op, storeURL, constants.ScratchLayerID)
if !assert.NoError(t, err) {
return
}
testSum := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
for i := 1; i < 50; i++ {
id := fmt.Sprintf("ID-%d", i)
img, werr := firstCache.WriteImage(op, parent, id, nil, testSum, nil)
if !assert.NoError(t, werr) {
return
}
if !assert.NotNil(t, img) {
return
}
expectedImages[id] = img
}
// get the images from the second cache to ensure it goes to the ds
for id, expectedImg := range expectedImages {
img, werr := secondCache.GetImage(op, storeURL, id)
if !assert.NoError(t, werr) || !assert.Equal(t, expectedImg, img) {
return
}
}
// Nuke the second cache's datastore. All data should come from the cache.
secondCache.DataStore = nil
for id, expectedImg := range expectedImages {
img, gerr := secondCache.GetImage(op, storeURL, id)
if !assert.NoError(t, gerr) || !assert.Equal(t, expectedImg, img) {
return
}
}
// Same should happen with a third cache when image list is called
thirdCache := NewLookupCache(ds)
imageList, err := thirdCache.ListImages(op, storeURL, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, imageList) {
return
}
if !assert.Equal(t, len(expectedImages), len(imageList)) {
return
}
// check the image data is the same
for id, expectedImg := range expectedImages {
img, err := thirdCache.GetImage(op, storeURL, id)
if !assert.NoError(t, err) || !assert.Equal(t, expectedImg, img) {
return
}
}
}
func TestDeleteImage(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
imageCache := NewLookupCache(NewMockDataStore())
op := trace.NewOperation(context.Background(), "test")
storeURL, err := imageCache.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) || !assert.NotNil(t, storeURL) {
return
}
scratch, err := imageCache.GetImage(op, storeURL, constants.ScratchLayerID)
if !assert.NoError(t, err) {
return
}
// create a 3 level tree with 4 branches
branches := 4
images := make(map[int]*Image)
for branch := 1; branch < branches; branch++ {
// level 1
img, err := imageCache.WriteImage(op, scratch, strconv.Itoa(branch), nil, "", nil)
if !assert.NoError(t, err) || !assert.NotNil(t, img) {
return
}
images[branch] = img
// level 2
i, err := imageCache.WriteImage(op, img, strconv.Itoa(branch*10), nil, "", nil)
if !assert.NoError(t, err) || !assert.NotNil(t, i) {
return
}
images[branch*10] = i
// level 3
i, err = imageCache.WriteImage(op, img, strconv.Itoa(branch*100), nil, "", nil)
if !assert.NoError(t, err) || !assert.NotNil(t, i) {
return
}
images[branch*100] = i
}
// Deletion of an intermediate node should fail
_, err = imageCache.DeleteImage(op, images[1])
if !assert.Error(t, err) {
return
}
imageList, err := imageCache.ListImages(op, storeURL, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, imageList) {
return
}
// image list should be uneffected
if !assert.Equal(t, len(images), len(imageList)) {
return
}
// Deletion of leaves should be fine
for branch := 1; branch < branches; branch++ {
// range up the branch
for _, img := range []*Image{images[branch*100], images[branch*10], images[branch]} {
_, err = imageCache.DeleteImage(op, img)
if !assert.NoError(t, err) {
return
}
// the image should be gone
i, err := imageCache.GetImage(op, storeURL, img.ID)
if !assert.Error(t, err) || !assert.Nil(t, i) {
return
}
}
}
// List images should be empty (because we filter out scratch)
imageList, err = imageCache.ListImages(op, storeURL, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, imageList) {
return
}
if !assert.True(t, len(imageList) == 0) {
return
}
}
func TestDeleteBranch(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
trace.Logger.Level = logrus.DebugLevel
imageCache := NewLookupCache(NewMockDataStore())
op := trace.NewOperation(context.Background(), "test")
storeURL, err := imageCache.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) || !assert.NotNil(t, storeURL) {
return
}
scratch, err := imageCache.GetImage(op, storeURL, constants.ScratchLayerID)
if !assert.NoError(t, err) {
return
}
// create a 3 level tree with 3 branches. The third branch will have an extra node.
// scratch
// 1 2 3
// 10 20 30
// 100 200 300 301
branches := 4
images := make(map[int]*Image)
for branch := 1; branch < branches; branch++ {
// level 1
img, err := imageCache.WriteImage(op, scratch, strconv.Itoa(branch), nil, "", nil)
if !assert.NoError(t, err) || !assert.NotNil(t, img) {
return
}
images[branch] = img
// level 2
img, err = imageCache.WriteImage(op, img, strconv.Itoa(branch*10), nil, "", nil)
if !assert.NoError(t, err) || !assert.NotNil(t, img) {
return
}
images[branch*10] = img
// level 3
img, err = imageCache.WriteImage(op, img, strconv.Itoa(branch*100), nil, "", nil)
if !assert.NoError(t, err) || !assert.NotNil(t, img) {
return
}
images[branch*100] = img
}
// Add an extra node to the last branch
img, err := imageCache.WriteImage(op, images[30], "301", nil, "", nil)
if !assert.NoError(t, err) || !assert.NotNil(t, img) {
return
}
images[301] = img
//
// Everything above here is just setup. Everything from here on is the test.
//
// Deletion of an intermediate node should fail
imagesDeleted, err := imageCache.DeleteBranch(op, images[1], nil)
if !assert.Error(t, err) && assert.Nil(t, imagesDeleted) {
return
}
imageList, err := imageCache.ListImages(op, storeURL, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, imageList) {
return
}
// image list should be uneffected
if !assert.Equal(t, len(images), len(imageList)) {
return
}
//
// Deletion of a branch
//
imagesDeleted, err = imageCache.DeleteBranch(op, images[100], nil)
if !assert.NoError(t, err) {
return
}
// List images should be missing a branch
imageList, err = imageCache.ListImages(op, storeURL, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, imageList) {
return
}
if !assert.Equal(t, 7, len(imageList)) || !assert.Equal(t, 3, len(imagesDeleted)) {
return
}
//
// Deletion of the split branch should only allow deletion of a single image
//
imagesDeleted, err = imageCache.DeleteBranch(op, images[300], nil)
if !assert.NoError(t, err) {
return
}
imageList, err = imageCache.ListImages(op, storeURL, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, imageList) {
return
}
// only 300 should have been deleted
if !assert.Equal(t, 6, len(imageList)) || !assert.Equal(t, images[300], imagesDeleted[0]) {
return
}
//
// Test keep with our 1 remaining branch
//
imagesDeleted, err = imageCache.DeleteBranch(op, images[200], []*url.URL{images[2].SelfLink})
if !assert.NoError(t, err) {
return
}
imageList, err = imageCache.ListImages(op, storeURL, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, imageList) {
return
}
// only 20 and 200 should have been deleted
if !assert.Equal(t, 4, len(imageList)) || !assert.Equal(t, images[200], imagesDeleted[0]) || !assert.Equal(t, images[20], imagesDeleted[1]) {
for _, img = range imageList {
t.Logf("image = %#v", img)
}
return
}
}
func TestCreateImageStoreFailureCleanup(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
trace.Logger.Level = logrus.DebugLevel
mds := NewMockDataStore()
imageCache := NewLookupCache(mds)
op := trace.NewOperation(context.Background(), "create image store error")
mds.createImageStoreError = fmt.Errorf("foo error")
storeURL, err := imageCache.CreateImageStore(op, "testStore")
if !assert.Error(t, err) || !assert.Nil(t, storeURL) {
return
}
mds.createImageStoreError = nil
storeURL, err = imageCache.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) || !assert.NotNil(t, storeURL) {
return
}
op = trace.NewOperation(context.Background(), "write image error")
mds = NewMockDataStore()
mds.writeImageError = fmt.Errorf("foo error")
imageCache = NewLookupCache(mds)
storeURL, err = imageCache.CreateImageStore(op, "testStore2")
if !assert.Error(t, err) || !assert.Nil(t, storeURL) {
return
}
mds.writeImageError = nil
storeURL, err = imageCache.CreateImageStore(op, "testStore2")
if !assert.NoError(t, err) || !assert.NotNil(t, storeURL) {
return
}
}
// Cache population should be happening in order starting from parent(id1) to children(id4)
func TestPopulateCacheInExpectedOrder(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
st := NewMockDataStore()
op := trace.NewOperation(context.Background(), "test")
storeURL, _ := util.ImageStoreNameToURL("testStore")
storageURLStr := storeURL.String()
url1, _ := url.Parse(storageURLStr + "/id1")
url2, _ := url.Parse(storageURLStr + "/id2")
url3, _ := url.Parse(storageURLStr + "/id3")
url4, _ := url.Parse(storageURLStr + "/id4")
scratchURL, _ := url.Parse(storageURLStr + constants.ScratchLayerID)
img1 := &Image{ID: "id1", SelfLink: url1, ParentLink: scratchURL, Store: storeURL}
img2 := &Image{ID: "id2", SelfLink: url2, ParentLink: url1, Store: storeURL}
img3 := &Image{ID: "id3", SelfLink: url3, ParentLink: url2, Store: storeURL}
img4 := &Image{ID: "id4", SelfLink: url4, ParentLink: url3, Store: storeURL}
scratchImg := &Image{
ID: constants.ScratchLayerID,
SelfLink: scratchURL,
ParentLink: scratchURL,
Store: storeURL,
}
// Order does matter for some reason.
imageMap := map[string]*Image{
img1.ID: img1,
img4.ID: img4,
img2.ID: img2,
img3.ID: img3,
scratchImg.ID: scratchImg,
}
st.db[*storeURL] = imageMap
imageCache := NewLookupCache(st)
imageCache.GetImageStore(op, "testStore")
// Check if all images are available.
imageIds := []string{"id1", "id2", "id3", "id4"}
for _, imageID := range imageIds {
v, _ := imageCache.GetImage(op, storeURL, imageID)
assert.NotNil(t, v)
}
}

View File

@@ -0,0 +1,73 @@
// Copyright 2016 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.
package image
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/portlayer/util"
)
func TestImageCopy(t *testing.T) {
storeName := "testStore"
ID := "testImageID"
imageURL, err := util.ImageURL(storeName, ID)
if !assert.NoError(t, err) {
return
}
parentURL, err := util.ImageURL(storeName, constants.ScratchLayerID)
if !assert.NoError(t, err) {
return
}
img, err := Parse(imageURL)
if !assert.NoError(t, err) || !assert.NotNil(t, img) {
return
}
storeURL, err := util.ImageStoreNameToURL(storeName)
if !assert.NoError(t, err) {
return
}
expected := &Image{
ID: ID,
SelfLink: imageURL,
ParentLink: parentURL,
Store: storeURL,
Metadata: map[string][]byte{
"1": {byte(1)},
"2": {byte(2)},
"3": []byte("three"),
},
}
actual := expected.Copy().(*Image)
if !assert.Equal(t, expected, actual) {
return
}
actual.Metadata["4"] = []byte("four")
if !assert.NotEqual(t, expected, actual) {
return
}
}

View File

@@ -0,0 +1,133 @@
// 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.
package image
import (
"fmt"
"net/url"
"path"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/lib/portlayer/storage/volume"
"github.com/vmware/vic/pkg/trace"
)
func Join(op trace.Operation, handle *exec.Handle, id, imgID, repoName string, img *Image) (*exec.Handle, error) {
defer trace.End(trace.Begin(img.ID, op))
// if _, ok := handle.ExecConfig.Mounts[volume.ID]; ok {
// return nil, fmt.Errorf("Volume with ID %s is already in container %s's mountspec config", volume.ID, handle.ExecConfig.ID)
// }
// //constuct MountSpec for the tether
// mountSpec := createMountSpec(volume, mountPath, diskOpts)
// //append a device addition spec change to the container config
// diskDevice := createVolumeVirtualDisk(volume)
// config := createDeviceConfigSpec(diskDevice)
// handle.Spec.DeviceChange = append(handle.Spec.DeviceChange, config)
// if handle.ExecConfig.Mounts == nil {
// handle.ExecConfig.Mounts = make(map[string]executor.MountSpec)
// }
// handle.ExecConfig.Mounts[volume.ID] = mountSpec
// NOTE: from lib/spec/disk.go
// set the rw layer name
// NOTE: this is a POOR assumption - I'm not clear on how it's functioning on vSAN at all in shipping code given the assumption that
// "[ds] id/id.vmdk" is a legitimate path. Some vsphere magic path adjustment?
rwlayer := fmt.Sprintf("%s/%s.vmdk", path.Dir(handle.Spec.VMPathName()), id)
disk := handle.Guest.NewDisk()
moref := handle.Spec.Datastore.Reference()
// NOTE: this spec construction really should be captured in one place down in the disk layer. That code is currently biased towards
// the appliance disk flows so couples spec creation with disk creation/attach.
// TODO: we absolutely shouldn't be mixing the handle.Spec.Datastore (wtf does this come from) and the DatastorePath for the disk
disk.GetVirtualDevice().Backing = &types.VirtualDiskFlatVer2BackingInfo{
DiskMode: string(types.VirtualDiskModePersistent),
ThinProvisioned: types.NewBool(true),
VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
FileName: rwlayer,
Datastore: &moref,
},
Parent: &types.VirtualDiskFlatVer2BackingInfo{
VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
FileName: img.DatastorePath.String(),
},
},
}
handle.Spec.AddVirtualDisk(disk)
// record the repo name and image ID that resolved to the layer in question
// NOTE: these really shouldn't be recorded directly like this, and are 1:1 with the image, not with the ExecConfig.
// I suspect there's some tech-debt reason they got dropped into the main configuration like this.
// I do recall that the repoName at least was recorded because many names/tags can point to the same layer so it's the
// point-and-time-of-use name that we're recording. I assume the same is true for the imageID whereas the layerID is actually
// stable
handle.ExecConfig.LayerID = img.ID
handle.ExecConfig.ImageID = imgID
handle.ExecConfig.RepoName = repoName
return handle, nil
}
func createVolumeVirtualDisk(volume *volume.Volume) *types.VirtualDisk {
unitNumber := int32(-1)
diskDevice := &types.VirtualDisk{
CapacityInKB: 0,
VirtualDevice: types.VirtualDevice{
Key: -1,
ControllerKey: 100, //FIXME: This is hardcoded for now and should be located from the config spec in the future.
UnitNumber: &unitNumber,
Backing: &types.VirtualDiskFlatVer2BackingInfo{
DiskMode: string(types.VirtualDiskModeIndependent_persistent),
VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
FileName: volume.Device.DiskPath().Path,
},
},
},
}
return diskDevice
}
func createDeviceConfigSpec(diskDevice *types.VirtualDisk) *types.VirtualDeviceConfigSpec {
config := &types.VirtualDeviceConfigSpec{
Device: diskDevice,
Operation: types.VirtualDeviceConfigSpecOperationAdd,
FileOperation: "", //blank for existing disk
}
return config
}
func createMountSpec(volume *volume.Volume, mountPath string, diskOpts map[string]string) executor.MountSpec {
deviceMode := diskOpts[constants.Mode]
newMountSpec := executor.MountSpec{
Source: url.URL{
Scheme: "label",
Path: volume.Label,
},
Path: mountPath,
Mode: deviceMode,
CopyMode: volume.CopyMode,
}
return newMountSpec
}

View File

@@ -0,0 +1,111 @@
// 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.
package vsphere
import (
"fmt"
"io"
"net/url"
"os"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/pkg/trace"
)
// Export reads the delta between child and parent image layers, returning
// the difference as a tar archive.
//
// id - must inherit from ancestor if ancestor is specified
// ancestor - the layer up the chain against which to diff
// spec - describes filters on paths found in the data (include, exclude, rebase, strip)
// data - set to true to include file data in the tar archive, false to include headers only
func (i *ImageStore) Export(op trace.Operation, id, ancestor string, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
l, err := i.NewDataSource(op, id)
if err != nil {
return nil, err
}
if ancestor == "" {
return l.Export(op, spec, data)
}
// for now we assume ancestor instead of entirely generic left/right
// this allows us to assume it's an image
r, err := i.NewDataSource(op, ancestor)
if err != nil {
op.Debugf("Unable to get datasource for ancestor: %s", err)
l.Close()
return nil, err
}
closers := func() error {
op.Debugf("Callback to io.Closer function for image delta export")
l.Close()
r.Close()
return nil
}
ls := l.Source()
rs := r.Source()
fl, lok := ls.(*os.File)
fr, rok := rs.(*os.File)
if !lok || !rok {
go closers()
return nil, fmt.Errorf("mismatched datasource types: %T, %T", ls, rs)
}
// if we want data, exclude the xattrs, otherwise assume diff
tar, err := archive.Diff(op, fl.Name(), fr.Name(), spec, data, !data)
if err != nil {
go closers()
return nil, err
}
return &storage.ProxyReadCloser{
ReadCloser: tar,
Closer: closers,
}, nil
}
func (i *ImageStore) NewDataSource(op trace.Operation, id string) (storage.DataSource, error) {
url, err := i.URL(op, id)
if err != nil {
return nil, err
}
return i.newDataSource(op, url)
}
func (i *ImageStore) newDataSource(op trace.Operation, url *url.URL) (storage.DataSource, error) {
mountPath, cleanFunc, err := i.Mount(op, url, false)
if err != nil {
return nil, err
}
f, err := os.Open(mountPath)
if err != nil {
cleanFunc()
return nil, err
}
op.Debugf("Created mount data source for access to %s at %s", url, mountPath)
return storage.NewMountDataSource(op, f, cleanFunc), nil
}

View File

@@ -0,0 +1,65 @@
// 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.
package vsphere
import (
"io"
"net/url"
"os"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/pkg/trace"
)
func (i *ImageStore) Import(op trace.Operation, id string, spec *archive.FilterSpec, tarStream io.ReadCloser) error {
l, err := i.NewDataSink(op, id)
if err != nil {
return err
}
return l.Import(op, spec, tarStream)
}
// NewDataSink creates and returns an DataSource associated with image storage
func (i *ImageStore) NewDataSink(op trace.Operation, id string) (storage.DataSink, error) {
uri, err := i.URL(op, id)
if err != nil {
return nil, err
}
// there is no online fail over path for images
// we should probably have a check in here as to whether the image is "sealed" and can no longer
// be modified.
return i.newDataSink(op, uri)
}
func (i *ImageStore) newDataSink(op trace.Operation, url *url.URL) (storage.DataSink, error) {
mountPath, cleanFunc, err := i.Mount(op, url, true)
if err != nil {
return nil, err
}
f, err := os.Open(mountPath)
if err != nil {
cleanFunc()
return nil, err
}
return &storage.MountDataSink{
Path: f,
Clean: cleanFunc,
}, nil
}

View File

@@ -0,0 +1,788 @@
// 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.
package vsphere
import (
"crypto/sha256"
"errors"
"fmt"
"io"
"net/url"
"os"
"path"
"strings"
"syscall"
docker "github.com/docker/docker/pkg/archive"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/storage/image"
"github.com/vmware/vic/lib/portlayer/storage/vsphere"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/lib/tether/shared"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
"github.com/vmware/vic/pkg/vsphere/disk"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/vm"
)
var (
// Set to false for unit tests
DetachAll = true
FileForMinOS = map[string]os.FileMode{
"/etc/hostname": 0644,
"/etc/hosts": 0644,
"/etc/resolv.conf": 0644,
}
SymLinkForMinOS = map[string]string{
"/etc/mtab": "/proc/mounts",
}
// Here the permission of .tether should be drwxrwxrwt.
// The sticky bit 't' is added when mounting the tmpfs in bootstrap
DirForMinOS = map[string]os.FileMode{
"/etc": 0755,
"/lib/modules": 0755,
"/proc": 0555,
"/sys": 0555,
"/run": 0755,
"/.tether": 0777,
}
)
const (
StorageImageDir = "images"
// scratchDiskLabel labels the root image for the disk chain
scratchDiskLabel = "scratch"
defaultDiskSizeInKB = 8 * 1024 * 1024
metaDataDir = "imageMetadata"
manifest = "manifest"
)
type ImageStore struct {
disk.Vmdk
}
func NewImageStore(op trace.Operation, s *session.Session, u *url.URL) (*ImageStore, error) {
dm, err := disk.NewDiskManager(op, s, storage.Config.ContainerView)
if err != nil {
return nil, err
}
if DetachAll {
if err = dm.DetachAll(op); err != nil {
return nil, err
}
}
datastores, err := s.Finder.DatastoreList(op, u.Host)
if err != nil {
return nil, fmt.Errorf("Host returned error when trying to locate provided datastore %s: %s", u.String(), err.Error())
}
if len(datastores) != 1 {
return nil, fmt.Errorf("Found %d datastores with provided datastore path %s. Cannot create image store.", len(datastores), u)
}
ds, err := datastore.NewHelper(op, s, datastores[0], path.Join(u.Path, constants.StorageParentDir))
if err != nil {
return nil, err
}
vis := &ImageStore{
Vmdk: disk.Vmdk{
Manager: dm,
Helper: ds,
Session: s,
},
}
return vis, nil
}
// Returns the path to a given image store. Currently this is the UUID of the VCH.
// `/VIC/imageStoreName (currently the vch uuid)/images`
func (v *ImageStore) imageStorePath(storeName string) string {
return path.Join(storeName, StorageImageDir)
}
// Returns the path to the image relative to the given
// store. The dir structure for an image in the datastore is
// `/VIC/imageStoreName (currently the vch uuid)/imageName/imageName.vmkd`
func (v *ImageStore) imageDirPath(storeName, imageName string) string {
return path.Join(v.imageStorePath(storeName), imageName)
}
func (v *ImageStore) imageDiskPath(storeName, imageName string) string {
return path.Join(v.imageDirPath(storeName, imageName), imageName+".vmdk")
}
// Returns the path to the vmdk itself in datastore url format
func (v *ImageStore) imageDiskDSPath(storeName, imageName string) *object.DatastorePath {
return &object.DatastorePath{
Datastore: v.Helper.RootURL.Datastore,
Path: path.Join(v.Helper.RootURL.Path, v.imageDiskPath(storeName, imageName)),
}
}
// Returns the path to the metadata directory for an image
func (v *ImageStore) imageMetadataDirPath(storeName, imageName string) string {
return path.Join(v.imageDirPath(storeName, imageName), metaDataDir)
}
// Returns the path to the manifest file. This file is our "done" file.
func (v *ImageStore) manifestPath(storeName, imageName string) string {
return path.Join(v.imageDirPath(storeName, imageName), manifest)
}
func (v *ImageStore) CreateImageStore(op trace.Operation, storeName string) (*url.URL, error) {
// convert the store name to a port layer url.
u, err := util.ImageStoreNameToURL(storeName)
if err != nil {
return nil, err
}
if _, err = v.Mkdir(op, true, v.imageStorePath(storeName)); err != nil {
return nil, err
}
return u, nil
}
// DeleteImageStore deletes the image store top level directory
func (v *ImageStore) DeleteImageStore(op trace.Operation, storeName string) error {
op.Infof("Cleaning up image store %s", storeName)
return v.Rm(op, v.imageStorePath(storeName))
}
// GetImageStore checks to see if the image store exists on disk and returns an
// error or the store's URL.
func (v *ImageStore) GetImageStore(op trace.Operation, storeName string) (*url.URL, error) {
u, err := util.ImageStoreNameToURL(storeName)
if err != nil {
return nil, err
}
p := v.imageStorePath(storeName)
info, err := v.Stat(op, p)
if err != nil {
return nil, err
}
_, ok := info.(*types.FolderFileInfo)
if !ok {
return nil, fmt.Errorf("Stat error: path doesn't exist (%s)", p)
}
// This is startup. Look for image directories without manifest files and
// nuke them.
if err := v.cleanup(op, u); err != nil {
return nil, err
}
return u, nil
}
func (v *ImageStore) ListImageStores(op trace.Operation) ([]*url.URL, error) {
op.Debugf("Listing image stores under %s", v.Helper.RootURL)
res, err := v.Ls(op, "")
if err != nil {
op.Errorf("Error listing image stores: %s", err.Error())
return nil, err
}
stores := []*url.URL{}
for _, f := range res.File {
path := f.GetFileInfo().Path
_, ok := f.(*types.FolderFileInfo)
if !ok {
op.Debugf("Skipping directory element %s as it's not a folder: %T", path, f)
continue
}
u, err := util.ImageStoreNameToURL(path)
if err != nil {
op.Errorf("Error converting image store name to URL: %s", err.Error())
return nil, err
}
op.Debugf("Mapped image store name %s to %s", path, u.String())
stores = append(stores, u)
}
return stores, nil
}
// WriteImage creates a new image layer from the given parent.
// Eg parentImage + newLayer = new Image built from parent
//
// parent - The parent image to create the new image from.
// ID - textual ID for the image to be written
// meta - metadata associated with the image
// Tag - the tag of the image to be written
func (v *ImageStore) WriteImage(op trace.Operation, parent *image.Image, ID string, meta map[string][]byte, sum string, r io.Reader) (*image.Image, error) {
storeName, err := util.ImageStoreName(parent.Store)
if err != nil {
return nil, err
}
imageURL, err := util.ImageURL(storeName, ID)
if err != nil {
return nil, err
}
var dsk *disk.VirtualDisk
// If this is scratch, then it's the root of the image store. All images
// will be descended from this created and prepared fs.
if ID == constants.ScratchLayerID {
// Create the scratch layer
if dsk, err = v.scratch(op, storeName); err != nil {
return nil, err
}
} else {
if parent.ID == "" {
return nil, fmt.Errorf("parent ID is empty")
}
dsk, err = v.writeImage(op, storeName, parent.ID, ID, meta, sum, r)
if err != nil {
return nil, err
}
}
newImage := &image.Image{
ID: ID,
SelfLink: imageURL,
ParentLink: parent.SelfLink,
Store: parent.Store,
Metadata: meta,
Disk: dsk,
DatastorePath: dsk.DatastoreURI,
}
return newImage, nil
}
// URL returns a url to the disk image represented by `id`
// This is a "ds://" URL so cannot be used as input to most of the ImageStore methods that
// take URLs.
func (v *ImageStore) URL(op trace.Operation, id string) (*url.URL, error) {
stores, err := v.ListImageStores(op)
if err != nil {
return nil, err
}
if len(stores) < 1 {
detail := "expected to find at least one image store available"
op.Errorf("Listing image stores: %s", detail)
return nil, errors.New(detail)
}
storeName, err := util.ImageStoreName(stores[0])
if err != nil {
op.Infof("Error getting image store name for %s: %s", stores[0], err.Error())
return nil, err
}
url := util.ImageDatastoreURL(v.imageDiskDSPath(storeName, id))
if err != nil {
op.Infof("Error getting image URL: %s", err.Error())
return nil, err
}
op.Debugf("Mapped image %s to %s", id, url)
return url, err
}
// Owners returns a list of VMs that are using the disk specified by `url`
func (v *ImageStore) Owners(op trace.Operation, url *url.URL, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
return nil, nil
}
// cleanup safely on error
func (v *ImageStore) cleanupDisk(op trace.Operation, ID, storeName string, vmdisk *disk.VirtualDisk) {
op.Errorf("Cleaning up failed image %s", ID)
if vmdisk != nil {
if vmdisk.Mounted() {
op.Debugf("Unmounting abandoned disk")
// #nosec: Errors unhandled.
vmdisk.Unmount(op)
}
if vmdisk.Attached() {
op.Debugf("Detaching abandoned disk")
// #nosec: Errors unhandled.
v.Detach(op, vmdisk.VirtualDiskConfig)
}
}
// #nosec: Errors unhandled.
v.deleteImage(op, storeName, ID)
}
// Create the image directory, create a temp vmdk in this directory,
// attach/mount the disk, unpack the tar, check the checksum. If the data
// doesn't match the expected checksum, abort by nuking the image directory.
// If everything matches, move the tmp vmdk to ID.vmdk. The unwind path is a
// bit convoluted here; we need to clean up on the way out in the error case
func (v *ImageStore) writeImage(op trace.Operation, storeName, parentID, ID string, meta map[string][]byte,
sum string, r io.Reader) (*disk.VirtualDisk, error) {
// Create a temp image directory in the store.
imageDir := v.imageDirPath(storeName, ID)
_, err := v.Mkdir(op, true, imageDir)
if err != nil {
return nil, err
}
// Write the metadata to the datastore
metaDataDir := v.imageMetadataDirPath(storeName, ID)
err = vsphere.WriteMetadata(op, v.Helper, metaDataDir, meta)
if err != nil {
return nil, err
}
// datastore path to the parent
parentDiskDsURI := v.imageDiskDSPath(storeName, parentID)
// datastore path to the disk we're creating
diskDsURI := v.imageDiskDSPath(storeName, ID)
op.Infof("Creating image %s (%s)", ID, diskDsURI)
var vmdisk *disk.VirtualDisk
// On error, unmount if mounted, detach if attached, and nuke the image directory
defer func() {
if err == nil {
return
}
v.cleanupDisk(op, ID, storeName, vmdisk)
}()
config := disk.NewPersistentDisk(diskDsURI).WithParent(parentDiskDsURI)
// Create the disk
vmdisk, err = v.CreateAndAttach(op, config)
if err != nil {
return nil, err
}
err = vmdisk.SetLabel(op, ID)
if err != nil {
return nil, err
}
dir, err := vmdisk.Mount(op, nil)
if err != nil {
return nil, err
}
h := sha256.New()
t := io.TeeReader(r, h)
// Untar the archive
var n int64
if n, err = docker.ApplyLayer(dir, t); err != nil {
return nil, err
}
op.Debugf("%s wrote %d bytes", ID, n)
actualSum := fmt.Sprintf("sha256:%x", h.Sum(nil))
if actualSum != sum {
err = fmt.Errorf("Failed to validate image checksum. Expected %s, got %s", sum, actualSum)
return nil, err
}
if err = vmdisk.Unmount(op); err != nil {
return nil, err
}
if err = v.Detach(op, vmdisk.VirtualDiskConfig); err != nil {
return nil, err
}
// Write our own bookkeeping manifest file to the image's directory. We
// treat the manifest file like a done file. Its existence means this vmdk
// is consistent. Previously we were writing the vmdk to a tmp vmdk file
// then moving it (using the MoveDatastoreFile or MoveVirtualDisk calls).
// However(!!) this flattens the vmdk. Also mkdir foo && ls -l foo fails
// on VSAN (see
// https://github.com/vmware/vic/pull/1764#issuecomment-237093424 for
// detail). We basically can't trust any of the datastore calls to help us
// with atomic operations. Touching an empty file seems to work well
// enough.
if err = v.writeManifest(op, storeName, ID, nil); err != nil {
return nil, err
}
return vmdisk, nil
}
func (v *ImageStore) scratch(op trace.Operation, storeName string) (*disk.VirtualDisk, error) {
var (
vmdisk *disk.VirtualDisk
size int64
err error
)
// Create the image directory in the store.
imageDir := v.imageDirPath(storeName, constants.ScratchLayerID)
if _, err := v.Mkdir(op, false, imageDir); err != nil {
return nil, err
}
// Write the metadata to the datastore
metaDataDir := v.imageMetadataDirPath(storeName, constants.ScratchLayerID)
if err := vsphere.WriteMetadata(op, v.Helper, metaDataDir, nil); err != nil {
return nil, err
}
imageDiskDsURI := v.imageDiskDSPath(storeName, constants.ScratchLayerID)
op.Infof("Creating image %s (%s)", constants.ScratchLayerID, imageDiskDsURI)
size = defaultDiskSizeInKB
if storage.Config.ScratchSize != 0 {
size = storage.Config.ScratchSize
}
defer func() {
if err == nil {
return
}
v.cleanupDisk(op, constants.ScratchLayerID, storeName, vmdisk)
}()
config := disk.NewPersistentDisk(imageDiskDsURI).WithCapacity(size).WithUUID(shared.ScratchUUID)
// Create the disk
vmdisk, err = v.CreateAndAttach(op, config)
if err != nil {
op.Errorf("CreateAndAttach(%s) error: %s", imageDiskDsURI, err)
return nil, err
}
op.Debugf("Scratch disk created with size %d", storage.Config.ScratchSize)
// Make the filesystem and set its label to defaultDiskLabel
if err = vmdisk.Mkfs(op, scratchDiskLabel); err != nil {
op.Errorf("Failed to create scratch filesystem: %s", err)
return nil, err
}
if err = createBaseStructure(op, vmdisk); err != nil {
op.Errorf("Failed to create base filesystem structure: %s", err)
return nil, err
}
if err = v.Detach(op, vmdisk.VirtualDiskConfig); err != nil {
op.Errorf("Failed to detach scratch image: %s", err)
return nil, err
}
if err = v.writeManifest(op, storeName, constants.ScratchLayerID, nil); err != nil {
op.Errorf("Failed to create manifest for scratch image: %s", err)
return nil, err
}
return vmdisk, nil
}
func (v *ImageStore) GetImage(op trace.Operation, store *url.URL, ID string) (*image.Image, error) {
defer trace.End(trace.Begin(store.String() + "/" + ID))
storeName, err := util.ImageStoreName(store)
if err != nil {
return nil, err
}
imageURL, err := util.ImageURL(storeName, ID)
if err != nil {
return nil, err
}
if err = v.verifyImage(op, storeName, ID); err != nil {
return nil, err
}
// get the metadata
metaDataDir := v.imageMetadataDirPath(storeName, ID)
meta, err := vsphere.GetMetadata(op, v.Helper, metaDataDir)
if err != nil {
return nil, err
}
diskDsURI := v.imageDiskDSPath(storeName, ID)
var s = *store
config := disk.NewPersistentDisk(diskDsURI)
dsk, err := v.Get(op, config)
if err != nil {
return nil, err
}
var parentURL *url.URL
if dsk.ParentDatastoreURI != nil {
vmdk := path.Base(dsk.ParentDatastoreURI.Path)
parentURL, err = util.ImageURL(storeName, strings.TrimSuffix(vmdk, path.Ext(vmdk)))
if err != nil {
return nil, err
}
}
newImage := &image.Image{
ID: ID,
SelfLink: imageURL,
Store: &s,
ParentLink: parentURL,
Metadata: meta,
Disk: dsk,
DatastorePath: diskDsURI,
}
op.Debugf("GetImage(%s) has parent %s", newImage.SelfLink, newImage.Parent())
return newImage, nil
}
func (v *ImageStore) ListImages(op trace.Operation, store *url.URL, IDs []string) ([]*image.Image, error) {
storeName, err := util.ImageStoreName(store)
if err != nil {
return nil, err
}
res, err := v.Ls(op, v.imageStorePath(storeName))
if err != nil {
return nil, err
}
images := []*image.Image{}
for _, f := range res.File {
file, ok := f.(*types.FolderFileInfo)
if !ok {
continue
}
ID := file.Path
// filter out scratch
if ID == constants.ScratchLayerID {
continue
}
// GetImage verifies the image is good by calling verifyImage.
img, err := v.GetImage(op, store, ID)
if err != nil {
return nil, err
}
images = append(images, img)
}
return images, nil
}
// DeleteImage deletes an image from the image store. If the image is in
// use either by way of inheritance or because it's attached to a
// container, this will return an error.
func (v *ImageStore) DeleteImage(op trace.Operation, image *image.Image) (*image.Image, error) {
// check if the image is in use.
if err := imagesInUse(op, image.ID); err != nil {
op.Errorf("ImageStore: delete image error: %s", err.Error())
return nil, err
}
storeName, err := util.ImageStoreName(image.Store)
if err != nil {
return nil, err
}
return image, v.deleteImage(op, storeName, image.ID)
}
func (v *ImageStore) deleteImage(op trace.Operation, storeName, ID string) error {
// Delete in order of manifest (the done file), the vmdk (because VC honors
// the deletable flag in the vmdk file), then the directory to get
// everything else.
paths := []string{
v.manifestPath(storeName, ID),
v.imageDiskPath(storeName, ID),
v.imageDirPath(storeName, ID),
}
for _, pth := range paths {
err := v.Rm(op, pth)
// not exist is ok
if err == nil || types.IsFileNotFound(err) {
continue
}
// something isn't right. bale.
op.Errorf("ImageStore: delete image error: %s", err.Error())
return err
}
return nil
}
// Find any image directories without the manifest file and remove them.
func (v *ImageStore) cleanup(op trace.Operation, store *url.URL) error {
defer trace.End(trace.Begin(fmt.Sprintf("Checking for inconsistent images on %s", store.String()), op))
storeName, err := util.ImageStoreName(store)
if err != nil {
return err
}
res, err := v.Ls(op, v.imageStorePath(storeName))
if err != nil {
return err
}
// We could call v.ListImages here but that results in calling GetImage,
// which pulls and unmarshalls the metadata. We don't need that.
for _, f := range res.File {
file, ok := f.(*types.FolderFileInfo)
if !ok {
continue
}
ID := file.Path
if err := v.verifyImage(op, storeName, ID); err != nil {
if ID == constants.ScratchLayerID {
op.Errorf("Failed to verify scratch image - skipping deletion so as not to invalidate image chain but this is probably non-functional")
continue
}
if err = v.deleteImage(op, storeName, ID); err != nil {
// deleteImage logs the error in the event there is one.
return err
}
}
}
return nil
}
// Manifest file for the image.
func (v *ImageStore) writeManifest(op trace.Operation, storeName, ID string, r io.Reader) error {
if err := v.Upload(op, r, v.manifestPath(storeName, ID)); err != nil {
return err
}
return nil
}
// check for the manifest file AND the vmdk
func (v *ImageStore) verifyImage(op trace.Operation, storeName, ID string) error {
// Check for the manifiest file and the vmdk
for _, p := range []string{v.manifestPath(storeName, ID), v.imageDiskPath(storeName, ID)} {
if _, err := v.Stat(op, p); err != nil {
return err
}
}
return nil
}
// XXX TODO This should be tied to an interface so we don't have to import exec
// here (or wherever the cache lives).
func imagesInUse(op trace.Operation, ID string) error {
// XXX Why doesnt this ever return an error? Strange.
// Gather all containers
conts := exec.Containers.Containers(nil)
if len(conts) == 0 {
return nil
}
for _, cont := range conts {
layerID := cont.ExecConfig.LayerID
if layerID == ID {
return &image.ErrImageInUse{
Msg: fmt.Sprintf("image %s in use by %s", ID, cont.ExecConfig.ID),
}
}
}
return nil
}
// populate the scratch with minimum OS structure defined in FileForMinOS and DirForMinOS
func createBaseStructure(op trace.Operation, vmdisk *disk.VirtualDisk) (err error) {
dir, err := vmdisk.Mount(op, nil)
if err != nil {
op.Errorf("Failed to mount device %s to dir %s", vmdisk.DevicePath, dir)
return err
}
defer func() {
e2 := vmdisk.Unmount(op)
if e2 != nil {
op.Errorf("Failed to unmount device: %s", e2)
if err == nil {
err = e2
}
}
}()
for dname, dmode := range DirForMinOS {
dirPath := path.Join(dir, dname)
if err = os.MkdirAll(dirPath, dmode); err != nil {
op.Errorf("Failed to create directory %s: %s", dirPath, err)
return err
}
}
op.Infof("Creating base file structure on disk")
// The directory has to exist before creating the new file
for fname, fmode := range FileForMinOS {
filePath := path.Join(dir, fname)
f, err := os.OpenFile(filePath, os.O_CREATE, fmode)
if err != nil {
op.Errorf("Failed to open file %s: %s", filePath, err)
return err
}
err = f.Close()
if err != nil {
op.Errorf("Failed to close file %s: %s", filePath, err)
return err
}
}
for fname, target := range SymLinkForMinOS {
filePath := path.Join(dir, fname)
err := syscall.Symlink(target, filePath)
if err != nil {
op.Errorf("Failed to create symlink %s->%s: %s", filePath, target, err)
return err
}
}
return nil
}

View File

@@ -0,0 +1,625 @@
// 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.
package vsphere
import (
"archive/tar"
"bytes"
"context"
"crypto/sha256"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"sync"
"testing"
"github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/lib/portlayer/storage/image"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
"github.com/vmware/vic/pkg/vsphere/disk"
"github.com/vmware/vic/pkg/vsphere/session"
)
func setup(t *testing.T) (*image.NameLookupCache, *session.Session, string, error) {
logrus.SetLevel(logrus.DebugLevel)
trace.Logger.Level = logrus.DebugLevel
DetachAll = false
client := datastore.Session(context.TODO(), t)
if client == nil {
return nil, nil, "", fmt.Errorf("skip")
}
storeURL := &url.URL{
Path: datastore.TestName("imageTests"),
Host: client.DatastorePath}
op := trace.NewOperation(context.Background(), "setup")
vsImageStore, err := NewImageStore(op, client, storeURL)
if err != nil {
if err.Error() == "can't find the hosting vm" {
t.Skip("Skipping: test must be run in a VM")
} else {
t.Log(err.Error())
}
return nil, nil, "", err
}
s := image.NewLookupCache(vsImageStore)
return s, client, storeURL.Path, nil
}
func TestRestartImageStore(t *testing.T) {
t.Skip("this test needs TLC")
// Start the image store once
cacheStore, client, parentPath, err := setup(t)
if !assert.NoError(t, err) {
return
}
origVsStore := cacheStore.DataStore.(*ImageStore)
defer cleanup(t, client, origVsStore, parentPath)
storeName := "bogusStoreName"
op := trace.NewOperation(context.Background(), "test")
origStore, err := cacheStore.CreateImageStore(op, storeName)
if !assert.NoError(t, err) || !assert.NotNil(t, origStore) {
return
}
imageStoreURL := &url.URL{
Path: constants.StorageParentDir,
Host: client.DatastorePath}
// now start it again
restartedVsStore, err := NewImageStore(op, client, imageStoreURL)
if !assert.NoError(t, err) || !assert.NotNil(t, restartedVsStore) {
return
}
// Check we didn't create a new UUID directory (relevant if vsan)
if !assert.Equal(t, origVsStore.RootURL, restartedVsStore.RootURL) {
return
}
restartedStore, err := restartedVsStore.GetImageStore(op, storeName)
if !assert.NoError(t, err) || !assert.NotNil(t, restartedStore) {
return
}
if !assert.Equal(t, origStore.String(), restartedStore.String()) {
return
}
}
// Create an image store then test it exists
func TestCreateAndGetImageStore(t *testing.T) {
vsis, client, parentPath, err := setup(t)
if !assert.NoError(t, err) {
return
}
// Nuke the parent image store directory
defer rm(t, client, client.Datastore.Path(parentPath))
storeName := "bogusStoreName"
op := trace.NewOperation(context.Background(), "test")
u, err := vsis.CreateImageStore(op, storeName)
if !assert.NoError(t, err) || !assert.NotNil(t, u) {
return
}
u, err = vsis.GetImageStore(op, storeName)
if !assert.NoError(t, err) || !assert.NotNil(t, u) {
return
}
// Negative test. Check for a dir that doesn't exist
u, err = vsis.GetImageStore(op, storeName+"garbage")
if !assert.Error(t, err) || !assert.Nil(t, u) {
return
}
// Test for a store that already exists
u, err = vsis.CreateImageStore(op, storeName)
if !assert.Error(t, err) || !assert.Nil(t, u) || !assert.Equal(t, err, os.ErrExist) {
return
}
}
func TestListImageStore(t *testing.T) {
vsis, client, parentPath, err := setup(t)
if !assert.NoError(t, err) {
return
}
// Nuke the parent image store directory
defer rm(t, client, client.Datastore.Path(parentPath))
op := trace.NewOperation(context.Background(), "test")
count := 3
for i := 0; i < count; i++ {
storeName := fmt.Sprintf("storeName%d", i)
u, err := vsis.CreateImageStore(op, storeName)
if !assert.NoError(t, err) || !assert.NotNil(t, u) {
return
}
}
images, err := vsis.ListImageStores(op)
if !assert.NoError(t, err) || !assert.Equal(t, len(images), count) {
return
}
}
// Creates a tar archive in memory for each layer and uses this to test image creation of layers
func TestCreateImageLayers(t *testing.T) {
numLayers := 4
cacheStore, client, parentPath, err := setup(t)
if !assert.NoError(t, err) {
return
}
vsStore := cacheStore.DataStore.(*ImageStore)
defer cleanup(t, client, vsStore, parentPath)
op := trace.NewOperation(context.Background(), "test")
storeURL, err := cacheStore.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) {
return
}
// Get an image that doesn't exist and check for error
grbg, err := cacheStore.GetImage(op, storeURL, "garbage")
if !assert.Error(t, err) || !assert.Nil(t, grbg) {
return
}
// base this image off scratch
parent, err := cacheStore.GetImage(op, storeURL, constants.ScratchLayerID)
if !assert.NoError(t, err) {
return
}
// Keep a list of all files we're extracting via layers so we can verify
// they exist in the leaf layer. Ext adds lost+found, so add it here.
expectedFilesOnDisk := []string{"lost+found"}
// Keep a list of images we created
expectedImages := make(map[string]*image.Image)
expectedImages[parent.ID] = parent
for layer := 0; layer < numLayers; layer++ {
dirName := fmt.Sprintf("dir%d", layer)
// Add some files to the archive.
var files = []tarFile{
{dirName, tar.TypeDir, ""},
{dirName + "/readme.txt", tar.TypeReg, "This archive contains some text files."},
{dirName + "/gopher.txt", tar.TypeReg, "Gopher names:\nGeorge\nGeoffrey\nGonzo"},
{dirName + "/todo.txt", tar.TypeReg, "Get animal handling license."},
}
for _, i := range files {
expectedFilesOnDisk = append(expectedFilesOnDisk, i.Name)
}
// meta for the image
meta := make(map[string][]byte)
meta[dirName+"_meta"] = []byte("Some Meta")
meta[dirName+"_moreMeta"] = []byte("Some More Meta")
meta[dirName+"_scorpions"] = []byte("Here I am, rock you like a hurricane")
// Tar the files
buf, terr := tarFiles(files)
if !assert.NoError(t, terr) {
return
}
// Calculate the checksum
h := sha256.New()
h.Write(buf.Bytes())
sum := fmt.Sprintf("sha256:%x", h.Sum(nil))
// Write the image via the cache (which writes to the vsphere impl)
writtenImage, terr := cacheStore.WriteImage(op, parent, dirName, meta, sum, buf)
if !assert.NoError(t, terr) || !assert.NotNil(t, writtenImage) {
return
}
expectedImages[dirName] = writtenImage
// Get the image directly via the vsphere image store impl.
vsImage, terr := vsStore.GetImage(op, parent.Store, dirName)
if !assert.NoError(t, terr) || !assert.NotNil(t, vsImage) {
return
}
assert.Equal(t, writtenImage, vsImage)
// make the next image a child of the one we just created
parent = writtenImage
}
// Test list images on the datastore
listedImages, err := vsStore.ListImages(op, parent.Store, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, listedImages) {
return
}
for _, img := range listedImages {
if !assert.Equal(t, expectedImages[img.ID].Store.String(), img.Store.String()) {
return
}
if !assert.Equal(t, expectedImages[img.ID].SelfLink.String(), img.SelfLink.String()) {
return
}
}
// verify the disk's data by attaching the last layer rdonly
roDisk, err := mountLayerRO(vsStore, parent)
if !assert.NoError(t, err) {
return
}
p, err := roDisk.MountPath()
if !assert.NoError(t, err) {
return
}
rodiskcleanupfunc := func() {
if roDisk != nil {
if roDisk.Mounted() {
roDisk.Unmount(op)
}
if roDisk.Attached() {
vsStore.Detach(op, roDisk.VirtualDiskConfig)
}
}
os.RemoveAll(p)
}
filesFoundOnDisk := []string{}
// Diff the contents of the RO file of the last child (with all of the contents)
err = filepath.Walk(p, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
f := path[len(p):]
if f != "" {
// strip the slash
filesFoundOnDisk = append(filesFoundOnDisk, f[1:])
}
return nil
})
if !assert.NoError(t, err) {
return
}
rodiskcleanupfunc()
sort.Strings(filesFoundOnDisk)
sort.Strings(expectedFilesOnDisk)
if !assert.Equal(t, expectedFilesOnDisk, filesFoundOnDisk) {
return
}
// Try to delete an intermediate image (should fail)
exec.NewContainerCache()
_, err = cacheStore.DeleteImage(op, expectedImages["dir1"])
if !assert.Error(t, err) || !assert.True(t, image.IsErrImageInUse(err)) {
return
}
// Try to delete a leaf (should pass)
leaf := expectedImages["dir"+strconv.Itoa(numLayers-1)]
_, err = cacheStore.DeleteImage(op, leaf)
if !assert.NoError(t, err) {
return
}
// Get the delete image directly via the vsphere image store impl.
deletedImage, err := vsStore.GetImage(op, parent.Store, leaf.ID)
if !assert.Error(t, err) || !assert.Nil(t, deletedImage) || !assert.True(t, os.IsNotExist(err)) {
return
}
}
func TestBrokenPull(t *testing.T) {
cacheStore, client, parentPath, err := setup(t)
if !assert.NoError(t, err) {
return
}
vsStore := cacheStore.DataStore.(*ImageStore)
defer cleanup(t, client, vsStore, parentPath)
op := trace.NewOperation(context.Background(), "test")
storeURL, err := cacheStore.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) {
return
}
// base this image off scratch
parent, err := cacheStore.GetImage(op, storeURL, constants.ScratchLayerID)
if !assert.NoError(t, err) {
return
}
imageID := "dir0"
// Add some files to the archive.
var files = []tarFile{
{imageID, tar.TypeDir, ""},
{imageID + "/readme.txt", tar.TypeReg, "This archive contains some text files."},
{imageID + "/gopher.txt", tar.TypeReg, "Gopher names:\nGeorge\nGeoffrey\nGonzo"},
{imageID + "/todo.txt", tar.TypeReg, "Get animal handling license."},
}
// meta for the image
meta := make(map[string][]byte)
meta[imageID+"_meta"] = []byte("Some Meta")
meta[imageID+"_moreMeta"] = []byte("Some More Meta")
meta[imageID+"_scorpions"] = []byte("Here I am, rock you like a hurricane")
// Tar the files
buf, terr := tarFiles(files)
if !assert.NoError(t, terr) {
return
}
// Calculate the checksum
h := sha256.New()
h.Write(buf.Bytes())
actualsum := fmt.Sprintf("sha256:%x", h.Sum(nil))
// Write the image via the cache (which writes to the vsphere impl). We're passing a bogus sum so the image should fail to save.
writtenImage, err := cacheStore.WriteImage(op, parent, imageID, meta, "bogusSum", new(bytes.Buffer))
if !assert.Error(t, err) || !assert.Nil(t, writtenImage) {
return
}
// Now try again with the right sum and there shouldn't be an error.
writtenImage, err = cacheStore.WriteImage(op, parent, imageID, meta, actualsum, buf)
if !assert.NoError(t, err) || !assert.NotNil(t, writtenImage) {
return
}
}
// Creates numLayers layers in parallel using the same parent to exercise parallel reconfigures
func TestParallel(t *testing.T) {
numLayers := 10
cacheStore, client, parentPath, err := setup(t)
if !assert.NoError(t, err) {
return
}
vsStore := cacheStore.DataStore.(*ImageStore)
defer cleanup(t, client, vsStore, parentPath)
op := trace.NewOperation(context.Background(), "test")
storeURL, err := cacheStore.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) {
return
}
// base this image off scratch
parent, err := cacheStore.GetImage(op, storeURL, constants.ScratchLayerID)
if !assert.NoError(t, err) {
return
}
wg := sync.WaitGroup{}
wg.Add(numLayers)
for i := 0; i < numLayers; i++ {
go func(idx int) {
defer wg.Done()
imageID := fmt.Sprintf("testStore-%d", idx)
op := trace.NewOperation(context.Background(), imageID)
// Write the image via the cache (which writes to the vsphere impl). We're passing a bogus sum so the image should fail to save.
writtenImage, err := cacheStore.WriteImage(op, parent, imageID, nil, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", new(bytes.Buffer))
if !assert.NoError(t, err) || !assert.NotNil(t, writtenImage) {
t.FailNow()
return
}
}(i)
}
wg.Wait()
}
func TestInProgressCleanup(t *testing.T) {
cacheStore, client, parentPath, err := setup(t)
if !assert.NoError(t, err) {
return
}
vsStore := cacheStore.DataStore.(*ImageStore)
defer cleanup(t, client, vsStore, parentPath)
op := trace.NewOperation(context.Background(), "test")
storeURL, err := cacheStore.CreateImageStore(op, "testStore")
if !assert.NoError(t, err) {
return
}
// base this image off scratch
parent, err := cacheStore.GetImage(op, storeURL, constants.ScratchLayerID)
if !assert.NoError(t, err) {
return
}
// create a test image
imageID := "testImage"
// meta for the image
meta := make(map[string][]byte)
meta[imageID+"_meta"] = []byte("Some Meta")
// Tar the files
buf, err := tarFiles([]tarFile{})
if !assert.NoError(t, err) {
return
}
// Calculate the checksum
h := sha256.New()
h.Write(buf.Bytes())
sum := fmt.Sprintf("sha256:%x", h.Sum(nil))
writtenImage, err := cacheStore.WriteImage(op, parent, imageID, meta, sum, buf)
if !assert.NoError(t, err) || !assert.NotNil(t, writtenImage) {
return
}
// nuke the done file.
rm(t, client, path.Join(vsStore.RootURL.String(), vsStore.imageDirPath("testStore", imageID), manifest))
// ensure GetImage doesn't find this image now
if _, err = vsStore.GetImage(op, storeURL, imageID); !assert.Error(t, err) {
return
}
// call cleanup
if err = vsStore.cleanup(op, storeURL); !assert.NoError(t, err) {
return
}
// Make sure list is now empty.
listedImages, err := vsStore.ListImages(op, parent.Store, nil)
if !assert.NoError(t, err) || !assert.Equal(t, len(listedImages), 1) || !assert.Equal(t, listedImages[0].ID, constants.ScratchLayerID) {
return
}
}
type tarFile struct {
Name string
Type byte
Body string
}
func tarFiles(files []tarFile) (*bytes.Buffer, error) {
// Create a buffer to write our archive to.
buf := new(bytes.Buffer)
// Create a new tar archive.
tw := tar.NewWriter(buf)
// Write data to the tar as if it came from the hub
for _, file := range files {
hdr := &tar.Header{
Name: file.Name,
Mode: 0777,
Typeflag: file.Type,
Size: int64(len(file.Body)),
}
if err := tw.WriteHeader(hdr); err != nil {
return nil, err
}
if file.Type == tar.TypeDir {
continue
}
if _, err := tw.Write([]byte(file.Body)); err != nil {
return nil, err
}
}
// Make sure to check the error on Close.
if err := tw.Close(); err != nil {
return nil, err
}
return buf, nil
}
func mountLayerRO(v *ImageStore, parent *image.Image) (*disk.VirtualDisk, error) {
roName := v.imageDiskDSPath("testStore", parent.ID)
roName.Path = roName.Path + "-ro.vmdk"
parentDsURI := v.imageDiskDSPath("testStore", parent.ID)
op := trace.NewOperation(context.TODO(), "ro")
config := disk.NewNonPersistentDisk(roName).WithParent(parentDsURI)
roDisk, err := v.CreateAndAttach(op, config)
if err != nil {
return nil, err
}
_, err = roDisk.Mount(op, nil)
if err != nil {
return nil, err
}
return roDisk, nil
}
func rm(t *testing.T, client *session.Session, name string) {
t.Logf("deleting %s", name)
fm := object.NewFileManager(client.Vim25())
task, err := fm.DeleteDatastoreFile(context.TODO(), name, client.Datacenter)
if !assert.NoError(t, err) {
return
}
_, _ = task.WaitForResult(context.TODO(), nil)
}
// Nuke the files and then the parent dir. Unfortunately, because this is
// vsan, we need to delete the files in the directories first (maybe
// because they're linked vmkds) before we can delete the parent directory.
func cleanup(t *testing.T, client *session.Session, vsStore *ImageStore, parentPath string) {
res, err := vsStore.LsDirs(context.TODO(), "")
if err != nil {
t.Logf("error: %s", err)
return
}
for _, dir := range res.HostDatastoreBrowserSearchResults {
for _, f := range dir.File {
fpath := f.GetFileInfo().Path
rm(t, client, path.Join(dir.FolderPath, fpath))
}
rm(t, client, dir.FolderPath)
}
rm(t, client, client.Datastore.Path(parentPath))
}

View File

@@ -0,0 +1,92 @@
// Copyright 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.
package storage
import (
"errors"
"io"
"os"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/pkg/trace"
)
// MountDataSink implements the DataSink interface for mounted devices
// This is a single use mechanism and will be tidied up on exit from MountDataSink.Import
type MountDataSink struct {
Path *os.File
Clean func()
cleanOp trace.Operation
}
// NewMountDataSink creates a new data sink associated with a specific mount, with the mount
// point being the path argument.
// The cleanup function is invoked once the import is complete.
func NewMountDataSink(op trace.Operation, path *os.File, cleanup func()) *MountDataSink {
if path == nil {
return nil
}
op.Debugf("Created mount data sink at %s", path.Name())
return &MountDataSink{
Path: path,
Clean: cleanup,
cleanOp: trace.FromOperation(op, "clean up from new mount sink"),
}
}
// Sink returns the data source associated with the DataSink
func (m *MountDataSink) Sink() interface{} {
return m.Path
}
// Import writes `data` to the data source associated with this DataSource
// This will call MountDataSink.Close on exit, irrespective of success or error
func (m *MountDataSink) Import(op trace.Operation, spec *archive.FilterSpec, data io.ReadCloser) error {
// reparent cleanup to Export operation
m.cleanOp = trace.FromOperation(op, "clean up from export")
// ensure that mounts are tidied up - a data sink is a single use mechanism.
defer m.Close()
name := m.Path.Name()
fi, err := m.Path.Stat()
if err != nil {
op.Errorf("Unable to stat mount path %s for data sink: %s", name, err)
return err
}
if !fi.IsDir() {
return errors.New("Path must be a directory")
}
// This assumes that m.Path was opened with a useful path (i.e. absolute) as that argument is what's
// returned by Name.
op.Infof("Importing supplied data stream to %s", name)
return archive.Unpack(op, data, spec, name)
}
func (m *MountDataSink) Close() error {
m.cleanOp.Infof("cleaning up after import")
// #nosec: Errors unhandled.
m.Path.Close()
if m.Clean != nil {
m.Clean()
}
return nil
}

View File

@@ -0,0 +1,138 @@
// Copyright 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.
package storage
import (
"errors"
"io"
"os"
"path/filepath"
"sync"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/pkg/trace"
)
// MountDataSource implements the DataSource interface for mounted devices
type MountDataSource struct {
Path *os.File
Clean func()
cleanOp trace.Operation
cancel func()
done sync.WaitGroup
}
// NewMountDataSource creates a new data source associated with a specific mount, with the mount
// point being the path argument.
// The cleanup function is invoked with the Close of the ReadCloser from Export, or explicitly
func NewMountDataSource(op trace.Operation, path *os.File, cleanup func()) *MountDataSource {
if path == nil {
return nil
}
op.Debugf("Created mount data source at %s", path.Name())
return &MountDataSource{
Path: path,
Clean: cleanup,
cleanOp: trace.FromOperation(op, "clean up from new mount source"),
}
}
// Source returns the data source associated with the DataSource
func (m *MountDataSource) Source() interface{} {
return m.Path
}
// Export reads data from the associated data source and returns it as a tar archive
func (m *MountDataSource) Export(op trace.Operation, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
// reparent cleanup to Export operation
m.cleanOp = trace.FromOperation(op, "clean up from export")
notifyOp := trace.WithValue(&op, archive.CancelNotifyKey{}, &m.done, "with cancel notifier")
cop, cancel := trace.WithCancel(&notifyOp, "cancellable export from mount")
m.cancel = cancel
name := m.Path.Name()
fi, err := m.Path.Stat()
if err != nil {
op.Errorf("Unable to stat mount path %s for data source: %s", name, err)
return nil, err
}
if !fi.IsDir() {
return nil, errors.New("path must be a directory")
}
op.Infof("Exporting data from %s", name)
// Diff is supplied "" to indicate that we are performing a read against a single target.
rc, err := archive.Diff(cop, name, "", spec, data, false)
// return the proxy regardless of error so that Close can be called
return &ProxyReadCloser{
rc,
m.Close,
}, err
}
// Stat stats the filesystem target indicated by the last entry in the given Filterspecs inclusion map
func (m *MountDataSource) Stat(op trace.Operation, spec *archive.FilterSpec) (*FileStat, error) {
// retrieve relative path
var targetPath string
for path := range spec.Inclusions {
targetPath = path
}
filePath := filepath.Join(m.Path.Name(), targetPath)
fileInfo, err := os.Lstat(filePath)
if err != nil {
// Does not exist is an expected result so no errors logged
if !os.IsNotExist(err) {
op.Errorf("failed to stat file: %s", err)
}
return nil, err
}
var linkTarget string
// check for symlink
if fileInfo.Mode()&os.ModeSymlink != 0 {
linkTarget, err = os.Readlink(filePath)
if err != nil {
return nil, err
}
}
return &FileStat{linkTarget, uint32(fileInfo.Mode()), fileInfo.Name(), fileInfo.Size(), fileInfo.ModTime()}, nil
}
func (m *MountDataSource) Close() error {
m.cleanOp.Infof("cleaning up after export - waiting for cancelation completion if necessary")
// trigger cancelation of any ongoing operations
if m.cancel != nil {
m.cancel()
}
// wait for cancelation to take effect
m.done.Wait()
m.Path.Close()
if m.Clean != nil {
m.cleanOp.Debugf("calling specified cleaner function")
m.Clean()
}
return nil
}

View File

@@ -0,0 +1,33 @@
// Copyright 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.
package storage
import (
"io"
)
// ProxyReadCloser is a read closer that provides for wrapping the Close with
// a custom Close call. The original ReadCloser.Close function will be invoked
// after the custom call. Errors from the custom call with be ignored.
type ProxyReadCloser struct {
io.ReadCloser
Closer func() error
}
func (p *ProxyReadCloser) Close() error {
/* #nosec - no useful way to handle this error */
p.Closer()
return p.ReadCloser.Close()
}

View File

@@ -0,0 +1,207 @@
// Copyright 2016 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.
package storage
import (
"context"
"io"
"net/url"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/view"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/vm"
)
var (
once sync.Once
importers map[string]Importer
exporters map[string]Exporter
)
type FileStat struct {
LinkTarget string
Mode uint32
Name string
Size int64
ModTime time.Time
}
func init() {
importers = make(map[string]Importer)
exporters = make(map[string]Exporter)
}
func create(ctx context.Context, session *session.Session, pool *object.ResourcePool) error {
var err error
mngr := view.NewManager(session.Vim25())
// Create view of VirtualMachine objects under the VCH's resource pool
Config.ContainerView, err = mngr.CreateContainerView(ctx, pool.Reference(), []string{"VirtualMachine"}, true)
if err != nil {
return err
}
return nil
}
// Init performs basic initialization, including population of storage.Config
func Init(ctx context.Context, session *session.Session, pool *object.ResourcePool, source extraconfig.DataSource, _ extraconfig.DataSink) error {
defer trace.End(trace.Begin(""))
var err error
once.Do(func() {
// Grab the storage layer config blobs from extra config
extraconfig.Decode(source, &Config)
log.Debugf("Decoded VCH config for storage: %#v", Config)
err = create(ctx, session, pool)
})
return err
}
// RegisterImporter registers the specified importer against the provided store for later retrieval.
func RegisterImporter(op trace.Operation, store string, i Importer) {
op.Infof("Registering importer: %s => %T", store, i)
importers[store] = i
}
// RegisterExporter registers the specified exporter against the provided store for later retrieval.
func RegisterExporter(op trace.Operation, store string, e Exporter) {
op.Infof("Registering exporter: %s => %T", store, e)
exporters[store] = e
}
// GetImporter retrieves an importer registered with the provided store.
// Will return nil, false if the store is not found.
func GetImporter(store string) (Importer, bool) {
i, ok := importers[store]
return i, ok
}
// GetExporter retrieves an exporter registered with the provided store.
// Will return nil, false if the store is not found.
func GetExporter(store string) (Exporter, bool) {
e, ok := exporters[store]
return e, ok
}
// GetImporters returns the set of known importers.
func GetImporters() []string {
keys := make([]string, 0, len(importers))
for key := range importers {
keys = append(keys, key)
}
return keys
}
// GetExporters returns the set of known importers.
func GetExporters() []string {
keys := make([]string, 0, len(exporters))
for key := range exporters {
keys = append(keys, key)
}
return keys
}
// Resolver defines methods for mapping ids to URLS, and urls to owners of that device
type Resolver interface {
// URL returns a url to the data source representing `id`
// For historic reasons this is not the same URL that other parts of the storage component use, but an actual
// URL suited for locating the storage element without having additional precursor knowledge.
URL(op trace.Operation, id string) (*url.URL, error)
// Owners returns a list of VMs that are using the resource specified by `url`
Owners(op trace.Operation, url *url.URL, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error)
}
// DataSource defines the methods for exporting data from a specific storage element as a tar stream
type DataSource interface {
// Close releases all resources associated with this source. Shared resources should be reference counted.
io.Closer
// Export performs an export of the specified files, returning the data as a tar stream. This is single use; once
// the export has completed it should not be assumed that the source remains functional.
//
// spec: specifies which files will be included/excluded in the export and allows for path rebasing/stripping
// data: if true the actual file data is included, if false only the file headers are present
Export(op trace.Operation, spec *archive.FilterSpec, data bool) (io.ReadCloser, error)
// Source returns the mechanism by which the data source is accessed
// Examples:
// vmdk mounted locally: *os.File
// nfs volume: XDR-client
// via guesttools: toolbox client
Source() interface{}
// Stat stats the filesystem target indicated by the last entry in the given Filterspecs inclusion map
Stat(op trace.Operation, spec *archive.FilterSpec) (*FileStat, error)
}
// DataSink defines the methods for importing data to a specific storage element from a tar stream
type DataSink interface {
// Close releases all resources associated with this sink. Shared resources should be reference counted.
io.Closer
// Import performs an import of the tar stream to the source held by this DataSink. This is single use; once
// the export has completed it should not be assumed that the sink remains functional.
//
// spec: specifies which files will be included/excluded in the import and allows for path rebasing/stripping
// tarStream: the tar stream to from which to import data
Import(op trace.Operation, spec *archive.FilterSpec, tarStream io.ReadCloser) error
// Sink returns the mechanism by which the data sink is accessed
// Examples:
// vmdk mounted locally: *os.File
// nfs volume: XDR-client
// via guesttools: toolbox client
Sink() interface{}
}
// Importer defines the methods needed to write data into a storage element. This should be implemented by the various
// store types.
type Importer interface {
// Import allows direct construction and invocation of a data sink for the specified ID.
Import(op trace.Operation, id string, spec *archive.FilterSpec, tarStream io.ReadCloser) error
// NewDataSink constructs a data sink for the specified ID within the context of the Importer. This is a single
// use sink which may hold resources until Closed.
NewDataSink(op trace.Operation, id string) (DataSink, error)
}
// Exporter defines the methods needed to read data from a storage element, optionally diff with an ancestor. This
// shoiuld be implemented by the various store types.
type Exporter interface {
// Export allows direct construction and invocation of a data source for the specified ID.
Export(op trace.Operation, id, ancestor string, spec *archive.FilterSpec, data bool) (io.ReadCloser, error)
// NewDataSource constructs a data source for the specified ID within the context of the Exporter. This is a single
// use source which may hold resources until Closed.
NewDataSource(op trace.Operation, id string) (DataSource, error)
}

View File

@@ -0,0 +1,50 @@
// Copyright 2016-2018 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.
package volume
type ErrVolumeInUse struct {
Msg string
}
func (e *ErrVolumeInUse) Error() string {
return e.Msg
}
func IsErrVolumeInUse(err error) bool {
if err == nil {
return false
}
_, ok := err.(*ErrVolumeInUse)
return ok
}
// VolumeStoreNotFoundError : custom error type for when we fail to find a target volume store
type VolumeStoreNotFoundError struct {
Msg string
}
func (e VolumeStoreNotFoundError) Error() string {
return e.Msg
}
// VolumeExistsError : custom error type for when a create operation targets and already occupied ID
type VolumeExistsError struct {
Msg string
}
func (e VolumeExistsError) Error() string {
return e.Msg
}

View File

@@ -0,0 +1,57 @@
// Copyright 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.
package nfs
import (
"net/url"
"path"
)
// Volume identifies an NFS based volume
type Volume struct {
// VS Host + Path to the actual volume
Host *url.URL
// Path of the volume from the volumestore target
Path string
}
func NewVolume(host *url.URL, NFSPath string) Volume {
volumeLocation := &url.URL{
Scheme: host.Scheme,
Host: host.Host,
Path: path.Join(host.Path, NFSPath),
}
v := Volume{
Host: volumeLocation,
Path: NFSPath,
}
return v
}
func (v Volume) MountPath() (string, error) {
return v.Path, nil
}
// DiskPath includes the url to the nfs directory for the container to mount,
func (v Volume) DiskPath() url.URL {
if v.Host == nil {
return url.URL{}
}
return *v.Host
}

View File

@@ -0,0 +1,71 @@
// Copyright 2017-2018 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.
package nfs
import (
"fmt"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/lib/portlayer/storage/volume"
"github.com/vmware/vic/pkg/trace"
)
const (
//Below is a list of options included in the mount options and a brief reason why.
// rw : on by default, for ro the syscall.MS_READONLY flag must be set instead of putting it in the options pointer of the mount call.
// "noatime" : this option prevents read's from triggering a write to update the accesstime of an inode. Helps with efficient reads. (Specified as a flag during the tether operation.)
// "vers=3" : we want to use NFSv3 it is simpler to implement and we do not need all the features of NFSv4 at this time
// "rsize=131072" : indicates the maximum read size for data on the NFS server. If the rsize is too big for either the client or server a negotion will occur to determine a supportable size. the value chosen is a default for /bin/mount for ubuntu 16.04
// "wsize=131072" : indicates the maximum write size for data on the NFS server. Like rsize it is negotiated if the value is too large. the value chosen is a default for /bin/mount for ubuntu 16.04
// "hard" : implies that we will retry indefinitely upon a failed transmission of data. this was agreed upon to indicate that problems have occurred with the mount if a hang on a write occurs.
// "proto=tcp" : tcp is a realiable protocol. The client used by the VCH also uses TCP as it's protocol. meaning we gain consistency in the communication between the tether->nfs and portlayer->nfs
// "timeo=600" : 600 deciseconds. This means a 60 second timout. With "hard" enabled this option likely does not matter.
// "sec=sys" : this means the NFS client uses the AUTH_SYS security flavor for all NFS requests on this mount point. This requires UID and GID of the user for permissions. also allows squashing permissions
// "mountvers=3" : this is listed as the RPC bind version. However, it is listed as a default by /bin/mount even when RPC is not the protocol used.
// "mountProto=TCP" : since the VCH uses TCP we should be using it as well here on the tether. Additionally, the mountProto does effect the initial protocol used for interacting with an nfs server. Keeping everything as the same protocol makes protocol issues easier to detect.
nfsMountOptions = "vers=3,rsize=131072,wsize=131072,hard,proto=tcp,timeo=600,sec=sys,mountvers=3,mountproto=tcp,nolock"
)
func VolumeJoin(op trace.Operation, handle *exec.Handle, volume *volume.Volume, mountPath string, diskOpts map[string]string) (*exec.Handle, error) {
defer trace.End(trace.Begin(fmt.Sprintf("handle.ID(%s), volume(%s), mountPath(%s), diskPath(%#v)", handle.ExecConfig.ID, volume.ID, mountPath, volume.Device.DiskPath())))
if _, ok := handle.ExecConfig.Mounts[volume.ID]; ok {
return nil, fmt.Errorf("Volume with ID %s is already in container %s's mountspec config", volume.ID, handle.ExecConfig.ID)
}
// construct MountSpec for the tether
mountSpec := createMountSpec(volume, mountPath, diskOpts)
if handle.ExecConfig.Mounts == nil {
handle.ExecConfig.Mounts = make(map[string]executor.MountSpec)
}
handle.ExecConfig.Mounts[volume.ID] = *mountSpec
return handle, nil
}
func createMountSpec(volume *volume.Volume, mountPath string, diskOpts map[string]string) *executor.MountSpec {
host := volume.Device.DiskPath()
deviceMode := nfsMountOptions + ",addr=" + host.Host
// Note: rw mode is not specified in the device node since the syscall.Mount defaults to rw. Additional, "noatime" must be indicated with the flag syscall.MS_NOATIME
newMountSpec := executor.MountSpec{
Source: host,
Path: mountPath,
Mode: deviceMode,
CopyMode: volume.CopyMode,
}
return &newMountSpec
}

View File

@@ -0,0 +1,320 @@
// Copyright 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.
package nfs
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/storage/volume"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/vm"
)
const (
// The directory created in the NFS VolumeStore which we create volumes under
volumesDir = "volumes"
// path that namespaces the metadata for a specific volume. It lives beside the Volumes Directory.
metadataDir = "volumes_metadata"
// Stock permissions that are set, In the future we may pass these in.
defaultPermissions = 0755
DefaultUID = 1000
nfsFilesystemTypeString = "nfs"
)
// VolumeStore this is nfs related volume store definition
type VolumeStore struct {
// volume store name
Name string
// Service is the interface to the nfs target.
Service MountServer
// Service selflink to volume store.
SelfLink *url.URL
// Archiver defines WriteArchive and Export interface methods
archive.Archiver
}
func NewVolumeStore(op trace.Operation, storeName string, mount MountServer) (*VolumeStore, error) {
// #nosec: Errors unhandled.
u, _ := mount.URL()
op.Infof("Creating nfs volumestore %s on %s", storeName, u.String())
target, err := mount.Mount(op)
if err != nil {
op.Errorf("error occurred while attempting to mount volumestore (%s). err: (%s)", storeName, err.Error())
return nil, err
}
defer mount.Unmount(op)
selfLink, err := util.VolumeStoreNameToURL(storeName)
if err != nil {
op.Errorf("Failed to construct URL for volume store %s: %s", storeName, err)
return nil, err
}
v := &VolumeStore{
Name: storeName,
Service: mount,
SelfLink: selfLink,
}
// we assume that nfsTargetURL.path already exists.
// make volumes directory
if _, err := target.Mkdir(volumesDir, defaultPermissions); err != nil && !os.IsExist(err) {
op.Errorf("Failed to create volumes directory '%s': %s", volumesDir, err)
return nil, err
}
// make metadata directory
if _, err := target.Mkdir(metadataDir, defaultPermissions); err != nil && !os.IsExist(err) {
op.Errorf("Failed to create metadata directory '%s': %s", metadataDir, err)
return nil, err
}
return v, nil
}
// Returns the path to the vol relative to the given store. The dir structure
// for a vol in a nfs store is `<configured nfs server path>/volumes/<vol ID>/<volume contents>`.
func (v *VolumeStore) volDirPath(ID string) string {
return path.Join(volumesDir, ID)
}
// Returns the path to the metadata directory for a volume
func (v *VolumeStore) volMetadataDirPath(ID string) string {
return path.Join(metadataDir, ID)
}
// Creates a volume directory and volume object for NFS based volumes
func (v *VolumeStore) VolumeCreate(op trace.Operation, ID string, store *url.URL, capacityKB uint64, info map[string][]byte) (*volume.Volume, error) {
target, err := v.Service.Mount(op)
if err != nil {
return nil, err
}
defer v.Service.Unmount(op)
dir := v.volDirPath(ID)
if _, err := target.Mkdir(dir, defaultPermissions); err != nil {
op.Errorf("Failed to create volume path '%s' on store %s: %s", dir, v.Name, err)
return nil, err
}
// #nosec: Errors unhandled.
u, _ := v.Service.URL()
if u.Scheme != nfsFilesystemTypeString {
op.Errorf("URL from nfs mount target had scheme (%s) instead of nfs for volume store (%s)", u.Scheme, v.Name)
return nil, fmt.Errorf("Unexpected scheme (%s) for volume store (%s)", u.Scheme, v.Name)
}
vol, err := volume.NewVolume(v.SelfLink, ID, info, NewVolume(u, v.volDirPath(ID)), executor.CopyNew)
if err != nil {
op.Errorf("Created volume directory but failed to create volume: %s", err)
return nil, err
}
if err := v.writeMetadata(op, ID, info, target); err != nil {
return nil, err
}
op.Infof("nfs volume (%s) successfully created on volume store (%s)", ID, v.Name)
return vol, nil
}
// VolumeDestroy Removes a volume and all of its contents from the nfs store. We already know via the cache if it is in use.
func (v *VolumeStore) VolumeDestroy(op trace.Operation, vol *volume.Volume) error {
target, err := v.Service.Mount(op)
if err != nil {
return err
}
defer v.Service.Unmount(op)
op.Infof("Attempting to remove volume (%s) and its metadata from volume store (%s)", vol.ID, v.Name)
// remove volume directory and children
if err := target.RemoveAll(v.volDirPath(vol.ID)); err != nil {
op.Errorf("failed to remove volume (%s) on volume store (%s) due to error (%s)", vol.ID, v.Name, err)
return err
}
// remove volume metadata directory and children
if err := target.RemoveAll(v.volMetadataDirPath(vol.ID)); err != nil {
op.Errorf("failed to remove metadata for volume (%s) at path (%q) on volume store (%s)", vol.ID, v.volDirPath(vol.ID), v.Name)
}
op.Infof("Successfully removed volume (%s) from volumestore (%s)", vol.ID, v.Name)
return nil
}
func (v *VolumeStore) VolumesList(op trace.Operation) ([]*volume.Volume, error) {
target, err := v.Service.Mount(op)
if err != nil {
return nil, err
}
defer v.Service.Unmount(op)
volFileInfo, err := target.ReadDir(volumesDir)
if err != nil {
return nil, err
}
var volumes []*volume.Volume
var fetchErr error
for _, fileInfo := range volFileInfo {
if fileInfo.Name() == "." || fileInfo.Name() == ".." {
continue
}
volMetadata, err := v.getMetadata(op, fileInfo.Name(), target)
if err != nil {
op.Errorf("getting metadata for %s: %s", fileInfo.Name(), err.Error())
fetchErr = err
continue
}
// #nosec: Errors unhandled.
u, _ := v.Service.URL()
vol, err := volume.NewVolume(v.SelfLink, fileInfo.Name(), volMetadata, NewVolume(u, v.volDirPath(fileInfo.Name())), executor.CopyNew)
if err != nil {
op.Errorf("Failed to create volume struct from volume directory (%s)", fileInfo.Name())
return nil, err
}
volumes = append(volumes, vol)
}
if fetchErr != nil {
return nil, err
}
return volumes, nil
}
// Import takes a tar archive stream and extracts it into the target volume
func (v *VolumeStore) Import(op trace.Operation, id string, spec *archive.FilterSpec, tarStream io.ReadCloser) error {
return fmt.Errorf("Write for nfs volumes is not Implemented")
}
// Export creates and returns a tar archive containing data found between an nfs layer one or all of its ancestors
func (v *VolumeStore) Export(op trace.Operation, id, ancestor string, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
return nil, fmt.Errorf("vSphere Integrated Containers does not yet implement Export for nfs volumes")
}
func (v *VolumeStore) NewDataSource(op trace.Operation, id string) (storage.DataSource, error) {
return nil, errors.New("NFS VolumeStore does not yet implement NewDataSource")
}
func (v *VolumeStore) NewDataSink(op trace.Operation, id string) (storage.DataSink, error) {
return nil, errors.New("NFS VolumeStore does not yet implement NewDataSink")
}
func (v *VolumeStore) URL(op trace.Operation, id string) (*url.URL, error) {
return nil, errors.New("NFS VolumeStore does not yet implement URL")
}
func (v *VolumeStore) Owners(op trace.Operation, url *url.URL, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
return nil, errors.New("NFS VolumeStore does not yet implement Owners")
}
func (v *VolumeStore) writeMetadata(op trace.Operation, ID string, info map[string][]byte, target Target) error {
// write metadata into the metadata directory by key (filename) / value
// (data), namespaced by volume id
//
// <root>/volume_matadata/<id>/<key>
metadataPath := v.volMetadataDirPath(ID)
_, err := target.Mkdir(metadataPath, defaultPermissions)
if err != nil {
op.Errorf("Failed to create metadata directory '%s': %s", metadataPath, err)
return err
}
op.Infof("Writing metadata to (%s)", metadataPath)
for fileName, data := range info {
targetPath := path.Join(metadataPath, fileName)
blobFile, err := target.OpenFile(targetPath, defaultPermissions)
if err != nil {
op.Errorf("openning file %s: %s", targetPath, err.Error())
return err
}
defer blobFile.Close()
_, err = blobFile.Write(data)
if err != nil {
op.Errorf("failed to write metadata to '%s': %s", targetPath, err.Error())
return err
}
defer blobFile.Close()
}
op.Infof("Successfully wrote metadata to (%s)", metadataPath)
return nil
}
func (v *VolumeStore) getMetadata(op trace.Operation, ID string, target Target) (map[string][]byte, error) {
metadataPath := v.volMetadataDirPath(ID)
op.Debugf("Attempting to retrieve volume metadata for (%s) at (%s)", ID, metadataPath)
dataKeys, err := target.ReadDir(metadataPath)
if err != nil {
op.Errorf("readdir(%s): %s", metadataPath, err.Error())
return nil, err
}
info := make(map[string][]byte)
for _, keyFile := range dataKeys {
if keyFile.Name() == "." || keyFile.Name() == ".." {
continue
}
pth := path.Join(metadataPath, keyFile.Name())
f, err := target.Open(pth)
if err != nil {
op.Errorf("open(%s): %s", pth, err.Error())
return nil, err
}
defer f.Close()
dataBlob, err := ioutil.ReadAll(f)
if err != nil {
op.Errorf("readall(%s): %s", pth, err.Error())
return nil, err
}
info[keyFile.Name()] = dataBlob
}
op.Infof("Successfully read volume metadata at (%s)", metadataPath)
return info, nil
}

View File

@@ -0,0 +1,489 @@
// Copyright 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.
package nfs
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path"
"testing"
"github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/pkg/trace"
)
const (
nfsTestDir = "NFSVolumeStoreTests"
)
type MockMount struct {
Path string
}
func (m MockMount) Mount(op trace.Operation) (Target, error) {
return NewMocktarget(m.Path), nil
}
func (m MockMount) Unmount(op trace.Operation) error {
return nil
}
func (m MockMount) URL() (*url.URL, error) {
return url.Parse("nfs://localhost/some/interesting/dir")
}
type MockTarget struct {
dirPath string
}
func NewMocktarget(pth string) MockTarget {
return MockTarget{dirPath: pth}
}
func (v MockTarget) Open(pth string) (io.ReadCloser, error) {
pth = path.Join(v.dirPath, pth)
logrus.Infof("open(%s)", pth)
return os.Open(pth)
}
func (v MockTarget) OpenFile(pth string, mode os.FileMode) (io.ReadWriteCloser, error) {
pth = path.Join(v.dirPath, pth)
logrus.Infof("openfile(%s)", pth)
return os.OpenFile(pth, os.O_RDWR|os.O_CREATE, mode)
}
func (v MockTarget) Mkdir(pth string, perm os.FileMode) ([]byte, error) {
pth = path.Join(v.dirPath, pth)
logrus.Infof("mkdir(%s)", pth)
return nil, os.Mkdir(pth, perm)
}
func (v MockTarget) RemoveAll(pth string) error {
pth = path.Join(v.dirPath, pth)
logrus.Infof("RemoveAll(%s)", pth)
return os.RemoveAll(pth)
}
func (v MockTarget) ReadDir(pth string) ([]os.FileInfo, error) {
pth = path.Join(v.dirPath, pth)
logrus.Infof("readdir(%s)", pth)
dir, err := os.Open(pth)
defer dir.Close()
if err != nil {
return nil, err
}
return dir.Readdir(0)
}
func (v MockTarget) Lookup(pth string) (os.FileInfo, []byte, error) {
pth = path.Join(v.dirPath, pth)
logrus.Infof("stat(%s)", pth)
info, err := os.Stat(pth)
return info, nil, err
}
var (
expected Target
mnt MountServer
)
func TestMain(m *testing.M) {
nfsurl := os.Getenv("NFS_TEST_URL")
if nfsurl != "" {
u, err := url.Parse(nfsurl)
if err != nil {
logrus.Errorf(err.Error())
os.Exit(-1)
}
logrus.Infof("testing nfs against %#v", u)
mnt = NewMount(u, "hasselhoff", 1001, 10001)
expected, err = mnt.Mount(trace.NewOperation(context.TODO(), "mount"))
if err != nil {
logrus.Errorf("error mounting %s: %s", u.String(), err.Error())
os.Exit(-1)
}
} else {
testdir, err := ioutil.TempDir(os.TempDir(), nfsTestDir)
if err != nil {
logrus.Errorf("error creating tmpdir: %s", err.Error())
os.Exit(-1)
}
os.Mkdir(testdir, 0755)
defer os.RemoveAll(testdir)
// We can twiddle the target directly via expected and use it to verify the
// right things are happening.
mnt = &MockMount{testdir}
expected = &MockTarget{testdir}
}
result := m.Run()
os.Exit(result)
}
func TestSimpleVolumeStoreOperations(t *testing.T) {
op := trace.NewOperation(context.TODO(), "TestOp")
// Create a Volume Store
vs, err := NewVolumeStore(op, "testStore", mnt)
if !assert.NoError(t, err, "Failed during call to NewVolumeStore with err (%s)", err) {
return
}
_, _, err = expected.Lookup(volumesDir)
if !assert.NoError(t, err, "Could not find the initial volume store directory after creation of volume store. err (%s)", err) {
return
}
if !assert.NotNil(t, vs, "Volume Store created with nil err, but return is also nil") {
return
}
info := make(map[string][]byte)
testInfoKey := "junk"
info[testInfoKey] = make([]byte, 20)
// Create a Volume
testVolName := "testVolume"
vol, err := vs.VolumeCreate(op, testVolName, vs.SelfLink, 0 /*we do not use this*/, info)
if !assert.NoError(t, err, "Failed during call to VolumeCreate with err (%s)", err) {
return
}
if !assert.Equal(t, testVolName, vol.ID, "expected volume ID (%s) got ID (%s)", testVolName, vol.ID) {
return
}
_, ok := vol.Info[testInfoKey]
if !assert.True(t, ok, "TestInfoKey did not exist in the return metadata map") {
return
}
// Check Metadata Pathing
metaDirEntries, err := expected.ReadDir(metadataDir)
if !assert.NoError(t, err, "Failed to read the metadata directory with err (%s)", err) {
return
}
if !assert.Len(t, metaDirEntries, 1) {
return
}
volumeDirEntries, err := expected.ReadDir(volumesDir)
if !assert.NoError(t, err, "Failed to read the volume data directory with err (%s)", err) {
return
}
if !assert.Len(t, volumeDirEntries, 1) {
return
}
// Remove the Volume
err = vs.VolumeDestroy(op, vol)
if !assert.NoError(t, err, "Failed during a call to VolumeDestroy with err (%s)", err) {
return
}
// should throw an error since the directory got nuked
metaDirEntries, err = expected.ReadDir(path.Join(metadataDir, vol.ID))
if !assert.Error(t, err) {
return
}
if !assert.Equal(t, len(metaDirEntries), 0, "expected metadata directory to have 1 entry and it had (%s)", len(metaDirEntries)) {
return
}
// Should throw an error on the volume directory
volumeDirEntries, err = expected.ReadDir(path.Join(volumesDir, vol.ID))
if !assert.Error(t, err) {
return
}
if !assert.Equal(t, len(volumeDirEntries), 0, "expected metadata directory to have 1 entry and it had (%s)", len(volumeDirEntries)) {
return
}
volToCheck, err := vs.VolumeCreate(op, testVolName, vs.SelfLink, 0, info)
if !assert.NoError(t, err, "Failed during call to VolumeCreate with err (%s)", err) {
return
}
volumeList, err := vs.VolumesList(op)
if !assert.NoError(t, err, "Failed during call to VolumesList with err (%s)", err) {
return
}
if !assert.Equal(t, 1, len(volumeList)) {
return
}
if !assert.Equal(t, volumeList[0].ID, volToCheck.ID, "Failed due to VolumeList returning an unexpected volume %#v when volume %#v was expected.", volumeList[0], volToCheck) {
return
}
RetrievedInfo := volumeList[0].Info
CreatedInfo := volToCheck.Info
if !assert.Equal(t, len(RetrievedInfo), len(CreatedInfo), "Length mismatch between the created volume(%s) and the volume returned from VolumeList(%s)", len(CreatedInfo), len(RetrievedInfo)) {
return
}
if !assert.Equal(t, RetrievedInfo[testInfoKey], CreatedInfo[testInfoKey], "Failed due to mismatch in metadata between the content of the Created volume(%s) and the volume return from VolumesList", CreatedInfo[testInfoKey], RetrievedInfo[testInfoKey]) {
return
}
err = vs.VolumeDestroy(op, volToCheck)
if !assert.NoError(t, err, "Failed during a call to VolumeDestroy with err (%s)", err) {
return
}
volumeList, err = vs.VolumesList(op)
if !assert.NoError(t, err, "Failed during a call to VolumesListwith err (%s)", err) {
return
}
if !assert.Equal(t, len(volumeList), 0, "Expected %s volumes, VolumesList returned %s", 0, len(volumeList)) {
return
}
}
func TestMultipleVolumes(t *testing.T) {
op := trace.NewOperation(context.TODO(), "TestOp")
//Create a Volume Store
vs, err := NewVolumeStore(op, "testStore", mnt)
if !assert.NoError(t, err, "Failed during call to NewVolumeStore with err (%s)", err) {
return
}
if !assert.NotNil(t, vs, "Volume Store created with nil err, but return is also nil") {
return
}
_, _, err = expected.Lookup(volumesDir)
if !assert.NoError(t, err, "Could not find the initial volume store directory after creation of volume store. err (%s)", err) {
return
}
// setup volume inputs
testVolNameOne := "test1"
infoOne := make(map[string][]byte)
testOneInfoKey := "junk"
infoOne[testOneInfoKey] = make([]byte, 20)
testVolNameTwo := "test2"
testTwoInfoKey := "important"
infoTwo := make(map[string][]byte)
infoTwo[testTwoInfoKey] = []byte("42")
testTwoInfoKeyTwo := "lessImportant"
infoTwo[testTwoInfoKeyTwo] = []byte("41")
testVolNameThree := "test3"
infoThree := make(map[string][]byte)
testThreeInfoKey := "lotsOfStuff"
infoThree[testThreeInfoKey] = []byte("importantData")
testThreeInfoKeyTwo := "someMoreStuff"
infoThree[testThreeInfoKeyTwo] = []byte("maybeSomeLabels")
//make volume one
volOne, err := vs.VolumeCreate(op, testVolNameOne, vs.SelfLink, 0 /*we do not use this*/, infoOne)
if !assert.NoError(t, err, "Failed during call to VolumeCreate with err (%s)", err) {
return
}
if !assert.Equal(t, testVolNameOne, volOne.ID, "expected volume ID (%s) got ID (%s)", testVolNameOne, volOne.ID) {
return
}
valOne, ok := volOne.Info[testOneInfoKey]
if !assert.True(t, ok, "TestInfoKey did not exist in the return metadata map") {
return
}
if !assert.Equal(t, valOne, volOne.Info[testOneInfoKey], "TestVolOne expected to have data (%s) and (%s) was found", infoOne, valOne) {
return
}
// make volume two
volTwo, err := vs.VolumeCreate(op, testVolNameTwo, vs.SelfLink, 0 /*we do not use this*/, infoTwo)
if !assert.NoError(t, err, "Failed during call to VolumeCreate with err (%s)", err) {
return
}
if !assert.Equal(t, testVolNameTwo, volTwo.ID, "expected volume ID (%s) got ID (%s)", testVolNameTwo, volTwo.ID) {
return
}
valOne, ok = volTwo.Info[testTwoInfoKey]
if !assert.True(t, ok, "TestInfoKey did not exist in the return metadata map") {
return
}
if !assert.Equal(t, []byte("42"), valOne, "TestVolTwo expected to have data (%s) and (%s) was found", []byte("42"), valOne) {
return
}
valTwo, ok := volTwo.Info[testTwoInfoKeyTwo]
if !assert.True(t, ok, "TestTwoInfoKeyTwo did not exist in the return metadata map for volTwo") {
return
}
if !assert.Equal(t, []byte("41"), valTwo, "TestVolTwo expected to have data (%s) and (%s) was found", []byte("41"), valTwo) {
return
}
// make volume three
volThree, err := vs.VolumeCreate(op, testVolNameThree, vs.SelfLink, 0 /*we do not use this*/, infoThree)
if !assert.NoError(t, err, "Failed during call to VolumeCreate with err (%s)", err) {
return
}
if !assert.Equal(t, testVolNameThree, volThree.ID, "expected volume ID (%s) got ID (%s)", testVolNameThree, volThree.ID) {
return
}
valOne, ok = volThree.Info[testThreeInfoKey]
if !assert.True(t, ok, "TestInfoKey did not exist in the return metadata map") {
return
}
if !assert.Equal(t, []byte("importantData"), valOne, "TestVolThree expected to have data (%s) and (%s) was found", []byte("importantData"), valOne) {
return
}
valTwo, ok = volThree.Info[testThreeInfoKeyTwo]
if !assert.True(t, ok, "TestThreeInfoKeyTwo did not exist in the return metadata map for volThree") {
return
}
if !assert.Equal(t, []byte("maybeSomeLabels"), valTwo, "TestVolThree expected to have data (%s) and (%s) was found", []byte("maybeSomeLabels"), valTwo) {
return
}
// list volumes
volumes, err := vs.VolumesList(op)
if !assert.NoError(t, err, "Failed during a call to VolumesList with err (%s)", err) {
return
}
volCount := len(volumes)
if !assert.Equal(t, volCount, 3, "VolumesList returned unexpected volume count. expected (%s), but received (%s) ", 3, volCount) {
return
}
// check metadatas
metaDirEntries, err := expected.ReadDir(metadataDir)
if !assert.NoError(t, err) {
return
}
if !assert.Equal(t, len(metaDirEntries), 3, "expected metadata directory to have 1 entry and it had (%s)", len(metaDirEntries)) {
return
}
volumeDirEntries, err := expected.ReadDir(volumesDir)
if !assert.NoError(t, err) {
return
}
if !assert.Equal(t, len(volumeDirEntries), 3, "expected metadata directory to have 1 entry and it had (%s)", len(volumeDirEntries)) {
return
}
verify := func(vols map[string]int) error {
for name, num := range vols {
metadataFiles, err := expected.ReadDir(path.Join(metadataDir, name))
if err != nil {
return err
}
if len(metadataFiles) != num {
return fmt.Errorf("len metadata %d != %d", len(metadataFiles), num)
}
}
return nil
}
// check and individual metadata dir
volmap := map[string]int{
testVolNameThree: 2,
testVolNameTwo: 2,
testVolNameOne: 1,
}
if !assert.NoError(t, verify(volmap)) {
return
}
// remove volume one
err = vs.VolumeDestroy(op, volOne)
if !assert.NoError(t, err, "Failed during a call to VolumeDestroy with error (%s)", err) {
return
}
// assert it's gone
_, _, err = expected.Lookup(path.Join(metadataDir, volOne.ID))
if !assert.Error(t, err) {
return
}
_, _, err = expected.Lookup(path.Join(volumesDir, volOne.ID))
if !assert.Error(t, err) {
return
}
// check that volume two and three exist with appropriate metadata
delete(volmap, testVolNameOne)
if !assert.NoError(t, verify(volmap)) {
return
}
// remove the rest of the volumes
err = vs.VolumeDestroy(op, volTwo)
if !assert.NoError(t, err, "Failed during a call to VolumeDestroy with error (%s)", err) {
return
}
err = vs.VolumeDestroy(op, volThree)
if !assert.NoError(t, err, "Failed during a call to VolumeDestroy with error (%s)", err) {
return
}
// verify they're gone
vols, err := vs.VolumesList(op)
if !assert.Len(t, vols, 0) {
return
}
return
}

View File

@@ -0,0 +1,152 @@
// Copyright 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.
package nfs
import (
"io"
"net/url"
"os"
nfsClient "github.com/vmware/go-nfs-client/nfs"
"github.com/vmware/go-nfs-client/nfs/rpc"
"github.com/vmware/vic/pkg/trace"
)
// MountServer is an interface used to communicate with network attached storage.
type MountServer interface {
// Mount executes the mount program on the Target.
Mount(op trace.Operation) (Target, error)
// Unmount terminates the Mount on the Target.
Unmount(op trace.Operation) error
URL() (*url.URL, error)
}
// Target is the filesystem interface for performing actions against attached storage.
type Target interface {
// Open opens a file on the Target in RD_ONLY
Open(path string) (io.ReadCloser, error)
// OpenFile opens a file on the Target with the given mode
OpenFile(path string, perm os.FileMode) (io.ReadWriteCloser, error)
// Mkdir creates a directory at the given path
Mkdir(path string, perm os.FileMode) ([]byte, error)
// RemoveAll deletes Directory recursively
RemoveAll(Path string) error
// ReadDir reads the dirents in the given directory
ReadDir(path string) ([]os.FileInfo, error)
// Lookup reads os.FileInfo for the given path
Lookup(path string) (os.FileInfo, []byte, error)
}
// NfsMount is used to wrap a MountServer to do the Mount()/Unmount() and Close()
type NfsMount struct {
// Hostname is the name to authenticate with to the target as
Hostname string
// UID and GID are the user id and group id to authenticate with the target
UID, GID uint32
// The URL (host + path) of the NFS server and target path
TargetURL *url.URL
s *nfsClient.Mount
}
func NewMount(t *url.URL, hostname string, uid, gid uint32) *NfsMount {
return &NfsMount{
Hostname: hostname,
UID: uid,
GID: gid,
TargetURL: t,
}
}
func (m *NfsMount) Mount(op trace.Operation) (Target, error) {
op.Debugf("Mounting %s", m.TargetURL.String())
s, err := nfsClient.DialMount(m.TargetURL.Host)
if err != nil {
return nil, err
}
m.s = s
defer func() {
if err != nil {
// #nosec: Errors unhandled.
m.s.Close()
}
}()
auth := rpc.NewAuthUnix(m.Hostname, m.UID, m.GID)
mnt, err := s.Mount(m.TargetURL.Path, auth.Auth())
if err != nil {
op.Errorf("unable to mount volume: %v", err)
return nil, err
}
op.Infof("Mounted %s", m.TargetURL.String())
return &target{mnt}, nil
}
func (m *NfsMount) Unmount(op trace.Operation) error {
op.Debugf("Unmounting %s", m.TargetURL.String())
if err := m.s.Unmount(); err != nil {
return err
}
if err := m.s.Close(); err != nil {
return err
}
op.Debugf("Unmounted %s", m.TargetURL.String())
m.s = nil
return nil
}
func (m *NfsMount) URL() (*url.URL, error) {
return m.TargetURL, nil
}
// wrap ReadDir to return a slice of os.FileInfo
type target struct {
*nfsClient.Target
}
func (t *target) ReadDir(path string) ([]os.FileInfo, error) {
entries, err := t.ReadDirPlus(path)
if err != nil {
return nil, err
}
var e []os.FileInfo
for i := 0; i < len(entries); i++ {
// filter out . and ..
name := entries[i].Name()
if name == "." || name == ".." {
continue
}
e = append(e, os.FileInfo(entries[i]))
}
return e, nil
}

View File

@@ -0,0 +1,150 @@
// Copyright 2016-2018 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.
package volume
import (
"crypto/md5" // #nosec: Use of weak cryptographic primitive
"errors"
"fmt"
"net/url"
"path/filepath"
"strings"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/pkg/trace"
)
type Disk interface {
// Path to this disk on the VCH
MountPath() (string, error)
// Path to the disk on the datastore
DiskPath() url.URL
}
// VolumeStorer is an interface to create, remove, enumerate, and get Volumes.
type VolumeStorer interface {
// Creates a volume on the given volume store, of the given size, with the given metadata.
VolumeCreate(op trace.Operation, ID string, store *url.URL, capacityKB uint64, info map[string][]byte) (*Volume, error)
// Destroys a volume
VolumeDestroy(op trace.Operation, vol *Volume) error
// Lists all volumes
VolumesList(op trace.Operation) ([]*Volume, error)
// The interfaces necessary for Import and Export
storage.Resolver
storage.Importer
storage.Exporter
}
// Volume is the handle to identify a volume on the backing store. The URI
// namespace used to identify the Volume in the storage layer has the following
// path scheme:
//
// `/storage/volumes/<volume store identifier, usually the vch uuid>/<volume id>`
//
type Volume struct {
// Identifies the volume
ID string
// Label is the computed label of the Volume. This is set by the runtime.
Label string
// The volumestore the volume lives on. (e.g the datastore + vch + configured vol directory)
Store *url.URL
// Metadata the volume is included with. Is persisted along side the volume vmdk.
Info map[string][]byte
// Namespace in the storage layer to look up this volume.
SelfLink *url.URL
// Backing device
Device Disk
CopyMode executor.CopyMode
}
// NewVolume creates a Volume
func NewVolume(store *url.URL, ID string, info map[string][]byte, device Disk, copyMode executor.CopyMode) (*Volume, error) {
storeName, err := util.VolumeStoreName(store)
if err != nil {
return nil, err
}
selflink, err := util.VolumeURL(storeName, ID)
if err != nil {
return nil, err
}
// Set the label to the md5 of the ID
vol := &Volume{
ID: ID,
Label: Label(ID),
Store: store,
SelfLink: selflink,
Device: device,
Info: info,
CopyMode: copyMode,
}
return vol, nil
}
// given an ID, compute the volume's label
func Label(ID string) string {
// e2label's manpage says the label size is 16 chars
// #nosec: Use of weak cryptographic primitive
m := md5.Sum([]byte(ID))
return fmt.Sprintf("%x", m)[:16]
}
func (v *Volume) Parse(u *url.URL) error {
// Check the path isn't malformed.
if !filepath.IsAbs(u.Path) {
return errors.New("invalid uri path")
}
segments := strings.Split(filepath.Clean(u.Path), "/")[1:]
if segments[0] != util.StorageURLPath {
return errors.New("not a storage path")
}
if len(segments) < 3 {
return errors.New("uri path mismatch")
}
store, err := util.VolumeStoreNameToURL(segments[2])
if err != nil {
return err
}
id := segments[3]
var SelfLink url.URL
SelfLink = *u
v.ID = id
v.SelfLink = &SelfLink
v.Store = store
return nil
}

View File

@@ -0,0 +1,307 @@
// Copyright 2016-2018 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.
package volume
import (
"fmt"
"io"
"net/url"
"os"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/pkg/trace"
)
// VolumeLookupCache caches Volume references to volumes in the system.
type VolumeLookupCache struct {
// Maps IDs to Volumes.
//
// id -> Volume
vlc map[string]Volume
vlcLock sync.RWMutex
// Maps the service url of the volume store to the underlying data storage implementation
volumeStores map[string]VolumeStorer
}
func NewVolumeLookupCache(op trace.Operation) *VolumeLookupCache {
v := &VolumeLookupCache{
vlc: make(map[string]Volume),
volumeStores: make(map[string]VolumeStorer),
}
return v
}
func (v *VolumeLookupCache) GetVolumeStore(op trace.Operation, storeName string) (*url.URL, error) {
u, err := util.VolumeStoreNameToURL(storeName)
if err != nil {
return nil, err
}
return u, nil
}
// AddStore adds a volumestore by name. The url returned is the service url to the volume store.
func (v *VolumeLookupCache) AddStore(op trace.Operation, storeName string, vs VolumeStorer) (*url.URL, error) {
v.vlcLock.Lock()
defer v.vlcLock.Unlock()
// get the service url
u, err := util.VolumeStoreNameToURL(storeName)
if err != nil {
return nil, err
}
storeURLStr := u.String()
if _, ok := v.volumeStores[storeURLStr]; ok {
return nil, fmt.Errorf("volumestore (%s) already added", storeURLStr)
}
v.volumeStores[storeURLStr] = vs
return u, v.addVolumesToCache(op, storeURLStr, vs)
}
func (v *VolumeLookupCache) volumeStore(store *url.URL) (VolumeStorer, error) {
// find the datastore
vs, ok := v.volumeStores[store.String()]
if !ok {
err := VolumeStoreNotFoundError{
Msg: fmt.Sprintf("volume store (%s) not found", store.String()),
}
return nil, err
}
return vs, nil
}
// VolumeStoresList returns a list of volume store names
func (v *VolumeLookupCache) VolumeStoresList(op trace.Operation) ([]string, error) {
v.vlcLock.RLock()
defer v.vlcLock.RUnlock()
stores := make([]string, 0, len(v.volumeStores))
for u := range v.volumeStores {
// from the storage url, get the store name
storeURL, err := url.Parse(u)
if err != nil {
return nil, err
}
storeName, err := util.VolumeStoreName(storeURL)
if err != nil {
return nil, err
}
stores = append(stores, storeName)
}
return stores, nil
}
func (v *VolumeLookupCache) VolumeCreate(op trace.Operation, ID string, store *url.URL, capacityKB uint64, info map[string][]byte) (*Volume, error) {
v.vlcLock.Lock()
defer v.vlcLock.Unlock()
// check if it exists
_, ok := v.vlc[ID]
if ok {
return nil, os.ErrExist
}
vs, err := v.volumeStore(store)
if err != nil {
return nil, err
}
vol, err := vs.VolumeCreate(op, ID, store, capacityKB, info)
if err != nil {
return nil, err
}
// Add it to the cache.
v.vlc[vol.ID] = *vol
return vol, nil
}
func (v *VolumeLookupCache) VolumeDestroy(op trace.Operation, ID string) error {
v.vlcLock.Lock()
defer v.vlcLock.Unlock()
// Check if it exists
vol, ok := v.vlc[ID]
if !ok {
return os.ErrNotExist
}
if err := volumeInUse(vol.ID); err != nil {
op.Errorf("VolumeStore: delete error: %s", err.Error())
return err
}
vs, err := v.volumeStore(vol.Store)
if err != nil {
return err
}
// remove it from the volumestore
if err := vs.VolumeDestroy(op, &vol); err != nil {
return err
}
delete(v.vlc, vol.ID)
return nil
}
func (v *VolumeLookupCache) VolumeGet(op trace.Operation, ID string) (*Volume, error) {
v.vlcLock.RLock()
defer v.vlcLock.RUnlock()
// look in the cache
vol, ok := v.vlc[ID]
if !ok {
return nil, os.ErrNotExist
}
return &vol, nil
}
func (v *VolumeLookupCache) VolumesList(op trace.Operation) ([]*Volume, error) {
v.vlcLock.RLock()
defer v.vlcLock.RUnlock()
// look in the cache, return the list
l := make([]*Volume, 0, len(v.vlc))
for _, vol := range v.vlc {
// this is idiotic
var e Volume
e = vol
l = append(l, &e)
}
return l, nil
}
// addVolumesToCache finds the volumes in the input volume store and adds them to the cache.
func (v *VolumeLookupCache) addVolumesToCache(op trace.Operation, storeURLStr string, vs VolumeStorer) error {
op.Infof("Adding volumes in volume store %s to volume cache", storeURLStr)
vols, err := vs.VolumesList(op)
if err != nil {
return err
}
for _, vol := range vols {
log.Infof("Volumestore: Found vol %s on store %s", vol.ID, vol.Store)
// Add it to the cache.
v.vlc[vol.ID] = *vol
}
return nil
}
func volumeInUse(ID string) error {
conts := exec.Containers.Containers(nil)
if len(conts) == 0 {
return nil
}
for _, cont := range conts {
if cont.ExecConfig.Mounts == nil {
continue
}
if _, mounted := cont.ExecConfig.Mounts[ID]; mounted {
return &ErrVolumeInUse{
Msg: fmt.Sprintf("volume %s in use by %s", ID, cont.ExecConfig.ID),
}
}
}
return nil
}
// Import is a fake store import so that we can do a late lookup of the actual store - this is a work around for the fact that the store
// URL isn't available in useful form outside of the volumeCache
func (v *VolumeLookupCache) Import(op trace.Operation, id string, spec *archive.FilterSpec, tarStream io.ReadCloser) error {
volume, err := v.VolumeGet(op, id)
if err != nil {
return err
}
store, err := v.volumeStore(volume.Store)
if err != nil {
return err
}
// relay call to actual store
return store.Import(op, id, spec, tarStream)
}
func (v *VolumeLookupCache) NewDataSink(op trace.Operation, id string) (storage.DataSink, error) {
volume, err := v.VolumeGet(op, id)
if err != nil {
return nil, err
}
store, err := v.volumeStore(volume.Store)
if err != nil {
return nil, err
}
// relay call to actual store
return store.NewDataSink(op, id)
}
// Export is a fake store export so that we can do a late lookup of the actual store - this is a work around for the fact that the store
// URL isn't available in useful form outside of the volumeCache
func (v *VolumeLookupCache) Export(op trace.Operation, id, ancestor string, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
volume, err := v.VolumeGet(op, id)
if err != nil {
return nil, err
}
store, err := v.volumeStore(volume.Store)
if err != nil {
return nil, err
}
// relay call to actual store
return store.Export(op, id, ancestor, spec, data)
}
func (v *VolumeLookupCache) NewDataSource(op trace.Operation, id string) (storage.DataSource, error) {
volume, err := v.VolumeGet(op, id)
if err != nil {
return nil, err
}
store, err := v.volumeStore(volume.Store)
if err != nil {
return nil, err
}
// relay call to actual store
return store.NewDataSource(op, id)
}

View File

@@ -0,0 +1,297 @@
// Copyright 2016-2018 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.
package volume
import (
"fmt"
"io"
"net/url"
"os"
"sync"
"testing"
"context"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/vm"
)
type MockVolumeStore struct {
// id -> volume
db map[string]*Volume
}
func NewMockVolumeStore() *MockVolumeStore {
m := &MockVolumeStore{
db: make(map[string]*Volume),
}
return m
}
func (m *MockVolumeStore) VolumeStoresList(op trace.Operation) (map[string]url.URL, error) {
return nil, nil
}
// Creates a volume on the given volume store, of the given size, with the given metadata.
func (m *MockVolumeStore) VolumeCreate(op trace.Operation, ID string, store *url.URL, capacityKB uint64, info map[string][]byte) (*Volume, error) {
storeName, err := util.VolumeStoreName(store)
if err != nil {
return nil, err
}
selfLink, err := util.VolumeURL(storeName, ID)
if err != nil {
return nil, err
}
vol := &Volume{
ID: ID,
Store: store,
SelfLink: selfLink,
}
m.db[ID] = vol
return vol, nil
}
// Get an existing volume via it's ID and volume store.
func (m *MockVolumeStore) VolumeGet(op trace.Operation, ID string) (*Volume, error) {
vol, ok := m.db[ID]
if !ok {
return nil, os.ErrNotExist
}
return vol, nil
}
// Destroys a volume
func (m *MockVolumeStore) VolumeDestroy(op trace.Operation, vol *Volume) error {
if _, ok := m.db[vol.ID]; !ok {
return os.ErrNotExist
}
delete(m.db, vol.ID)
return nil
}
// VolumesList lists all volumes on the given volume store.
func (m *MockVolumeStore) VolumesList(op trace.Operation) ([]*Volume, error) {
var i int
list := make([]*Volume, len(m.db))
for _, v := range m.db {
t := *v
list[i] = &t
i++
}
return list, nil
}
func (m *MockVolumeStore) Export(op trace.Operation, child, ancestor string, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
return nil, nil
}
func (m *MockVolumeStore) Import(op trace.Operation, id string, spec *archive.FilterSpec, tarstream io.ReadCloser) error {
return nil
}
func (m *MockVolumeStore) NewDataSink(op trace.Operation, id string) (storage.DataSink, error) {
return nil, nil
}
func (m *MockVolumeStore) NewDataSource(op trace.Operation, id string) (storage.DataSource, error) {
return nil, nil
}
func (m *MockVolumeStore) URL(op trace.Operation, id string) (*url.URL, error) {
return nil, nil
}
func (m *MockVolumeStore) Owners(op trace.Operation, url *url.URL, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
return nil, nil
}
func TestVolumeCreateGetListAndDelete(t *testing.T) {
op := trace.NewOperation(context.Background(), "test")
exec.NewContainerCache()
mvs := NewMockVolumeStore()
v := NewVolumeLookupCache(op)
storeURL, err := v.AddStore(op, "testStore", mvs)
if !assert.NoError(t, err) {
return
}
inVols := make(map[string]*Volume)
inVolsM := &sync.Mutex{}
wg := &sync.WaitGroup{}
createFn := func(i int) {
defer wg.Done()
id := fmt.Sprintf("ID-%d", i)
// Write to the datastore
vol, err := v.VolumeCreate(op, id, storeURL, 0, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, vol) {
return
}
inVolsM.Lock()
inVols[id] = vol
inVolsM.Unlock()
}
// Create a set of volumes
numVolumes := 5
wg.Add(numVolumes)
for i := 0; i < numVolumes; i++ {
go createFn(i)
}
wg.Wait()
getFn := func(inVol *Volume) {
vol, err := v.VolumeGet(op, inVol.ID)
if !assert.NoError(t, err) || !assert.NotNil(t, vol) {
return
}
if !assert.Equal(t, inVol, vol) {
return
}
wg.Done()
}
wg.Add(numVolumes)
for _, inVol := range inVols {
getFn(inVol)
}
wg.Wait()
volumeList, err := v.VolumesList(op)
if !assert.NoError(t, err) || !assert.Equal(t, numVolumes, len(volumeList)) {
return
}
// Test that the list returned by VolumeList matches our inVols list. Then
// delete each vol via the cache, then check the datastore to ensure it's
// empty
for _, outVol := range volumeList {
if !assert.Equal(t, inVols[outVol.ID], outVol) {
return
}
if err = v.VolumeDestroy(op, outVol.ID); !assert.NoError(t, err) {
return
}
}
// check the datastore is empty.
if !assert.Empty(t, mvs.db) {
return
}
}
// createVolumes is a test helper that creates a set of num volumes on the input volume cache and volume store.
func createVolumes(t *testing.T, op trace.Operation, v *VolumeLookupCache, storeURL *url.URL, num int) map[string]*Volume {
vols := make(map[string]*Volume)
for i := 1; i <= num; i++ {
id := fmt.Sprintf("ID-%d", i)
// Write to the datastore
vol, err := v.VolumeCreate(op, id, storeURL, 0, nil)
if !assert.NoError(t, err) || !assert.NotNil(t, vol) {
return nil
}
vols[id] = vol
}
return vols
}
func TestAddVolumesToCache(t *testing.T) {
mvs1 := NewMockVolumeStore()
op := trace.NewOperation(context.Background(), "test")
v := NewVolumeLookupCache(op)
storeURL, err := util.VolumeStoreNameToURL("testStore")
assert.NotNil(t, storeURL)
storeURLStr := storeURL.String()
v.volumeStores[storeURLStr] = mvs1
// Create 50 volumes on the volume store.
vols := createVolumes(t, op, v, storeURL, 50)
// Clear the volume map after it has been filled during volume creation.
v.vlc = make(map[string]Volume)
err = v.addVolumesToCache(op, storeURLStr, mvs1)
assert.Nil(t, err)
// Check that the volume map is intact again in the cache.
for _, expectedVol := range vols {
vol, err := v.VolumeGet(op, expectedVol.ID)
if !assert.NoError(t, err) || !assert.Equal(t, expectedVol, vol) {
return
}
}
}
// Create 2 store caches but use the same backing datastore. Create images
// with the first cache, then get the image with the second. This simulates
// restart since the second cache is empty and has to go to the backing store.
func TestVolumeCacheRestart(t *testing.T) {
mvs := NewMockVolumeStore()
op := trace.NewOperation(context.Background(), "test")
firstCache := NewVolumeLookupCache(op)
storeURL, err := firstCache.AddStore(op, "testStore", mvs)
if !assert.NoError(t, err) || !assert.NotNil(t, storeURL) {
return
}
// Create a set of 50 volumes.
inVols := createVolumes(t, op, firstCache, storeURL, 50)
secondCache := NewVolumeLookupCache(op)
if !assert.NotNil(t, secondCache) {
return
}
storeURL, err = secondCache.AddStore(op, "testStore", mvs)
if !assert.NoError(t, err) || !assert.NotNil(t, storeURL) {
return
}
// get the vols from the second cache to ensure it goes to the ds
for _, expectedVol := range inVols {
vol, err := secondCache.VolumeGet(op, expectedVol.ID)
if !assert.NoError(t, err) || !assert.Equal(t, expectedVol, vol) {
return
}
}
}

View File

@@ -0,0 +1,54 @@
// Copyright 2016-2018 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.
package volume
import (
"net/url"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/portlayer/util"
)
func TestVolumeParseURL(t *testing.T) {
util.DefaultHost, _ = url.Parse("http://foo.com/")
in, _ := url.Parse(util.DefaultHost.String())
in.Path = "/" + path.Join("storage", "volumes", "volStore", "volName")
v := &Volume{}
err := v.Parse(in)
if !assert.NoError(t, err) {
return
}
if !assert.Equal(t, "volName", v.ID) {
return
}
volStore, _ := url.Parse(util.DefaultHost.String())
util.AppendDir(volStore, "/storage/volumes/volStore")
if !assert.Equal(t, volStore.String(), v.Store.String()) {
return
}
util.AppendDir(volStore, "volName")
if !assert.Equal(t, volStore.String(), v.SelfLink.String()) {
return
}
}

View File

@@ -0,0 +1,136 @@
// 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.
package vsphere
import (
"errors"
"fmt"
"io"
"net/url"
"os"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/storage/volume"
"github.com/vmware/vic/lib/portlayer/storage/vsphere"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/disk"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// Export reads the delta between child and parent volume layers, returning
// the difference as a tar archive.
//
// store - the volume store containing the two layers
// id - must inherit from ancestor if ancestor is specified
// ancestor - the volume layer up the chain against which to diff
// spec - describes filters on paths found in the data (include, exclude, strip)
// data - set to true to include file data in the tar archive, false to include headers only
func (v *VolumeStore) Export(op trace.Operation, id, ancestor string, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
if ancestor != "" {
return nil, fmt.Errorf("volume diff is not supported in this volume store: %s", v.SelfLink.String())
}
l, err := v.NewDataSource(op, id)
if err != nil {
return nil, err
}
return l.Export(op, spec, data)
}
// NewDataSource creates and returns an DataSource associated with container storage
func (v *VolumeStore) NewDataSource(op trace.Operation, id string) (storage.DataSource, error) {
uri, err := v.URL(op, id)
if err != nil {
return nil, err
}
offlineAttempt := 0
offline:
offlineAttempt++
// offline disk attempt
source, err := v.newDataSource(op, uri)
if err == nil {
return source, err
}
// check for vmdk locked error here
if !disk.IsLockedError(err) {
op.Warnf("Unable to mount %s and do not know how to recover from error")
// continue anyway because maybe there's an online option
}
// online - Owners() should filter out the appliance VM
// #nosec: Errors unhandled.
owners, _ := v.Owners(op, uri, disk.LockedVMDKFilter)
if len(owners) == 0 {
op.Infof("No online owners were found for %s", id)
return nil, errors.New("unable to create offline data source and no online owners found")
}
for _, o := range owners {
// sanity check to see if we are the owner - this should catch transitions
// from container running to diff or commit for example between the offline attempt and here
uuid, err := o.UUID(op)
if err == nil {
// check if the vm is appliance VM if we can successfully get its UUID
// #nosec: Errors unhandled.
self, _ := guest.IsSelf(op, uuid)
if self && offlineAttempt < 2 {
op.Infof("Appliance is owner of online vmdk - retrying offline source path")
goto offline
}
}
online, err := v.newOnlineDataSource(op, o, id)
if online != nil {
return online, err
}
op.Debugf("Failed to create online datasource with owner %s: %s", o.Reference(), err)
}
return nil, errors.New("unable to create online or offline data source")
}
func (v *VolumeStore) newDataSource(op trace.Operation, url *url.URL) (storage.DataSource, error) {
// This is persistent to avoid issues with concurrent Stat/Import calls
mountPath, cleanFunc, err := v.Mount(op, url, true)
if err != nil {
return nil, err
}
f, err := os.Open(mountPath)
if err != nil {
cleanFunc()
return nil, err
}
op.Debugf("Created mount data source for access to %s at %s", url, mountPath)
return storage.NewMountDataSource(op, f, cleanFunc), nil
}
func (v *VolumeStore) newOnlineDataSource(op trace.Operation, owner *vm.VirtualMachine, id string) (storage.DataSource, error) {
op.Debugf("Constructing toolbox data source: %s.%s", owner.Reference(), id)
return &vsphere.ToolboxDataSource{
VM: owner,
ID: volume.Label(id),
Clean: func() { return },
}, nil
}

View File

@@ -0,0 +1,121 @@
// 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.
package vsphere
import (
"errors"
"io"
"net/url"
"os"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/storage/volume"
"github.com/vmware/vic/lib/portlayer/storage/vsphere"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/disk"
"github.com/vmware/vic/pkg/vsphere/vm"
)
func (v *VolumeStore) Import(op trace.Operation, id string, spec *archive.FilterSpec, tarstream io.ReadCloser) error {
l, err := v.NewDataSink(op, id)
if err != nil {
return err
}
return l.Import(op, spec, tarstream)
}
// NewDataSource creates and returns an DataSource associated with container storage
func (v *VolumeStore) NewDataSink(op trace.Operation, id string) (storage.DataSink, error) {
uri, err := v.URL(op, id)
if err != nil {
return nil, err
}
offlineAttempt := 0
offline:
offlineAttempt++
source, err := v.newDataSink(op, uri)
if err == nil {
return source, err
}
// check for vmdk locked error here
if !disk.IsLockedError(err) {
op.Warnf("Unable to mount %s and do not know how to recover from error")
// continue anyway because maybe there's an online option
}
// online - Owners() should filter out the appliance VM
// #nosec: Errors unhandled.
owners, _ := v.Owners(op, uri, disk.LockedVMDKFilter)
if len(owners) == 0 {
op.Infof("No online owners were found for %s", id)
return nil, errors.New("unable to create offline data sink and no online owners found")
}
for _, o := range owners {
// sanity check to see if we are the owner - this should catch transitions
// from container running to diff or commit for example between the offline attempt and here
uuid, err := o.UUID(op)
if err == nil {
// check if the vm is appliance VM if we can successfully get its UUID
// #nosec: Errors unhandled.
self, _ := guest.IsSelf(op, uuid)
if self && offlineAttempt < 2 {
op.Infof("Appliance is owner of online vmdk - retrying offline source path")
goto offline
}
}
online, err := v.newOnlineDataSink(op, o, id)
if online != nil {
return online, err
}
op.Debugf("Failed to create online sink with owner %s: %s", o.Reference(), err)
}
return nil, errors.New("unable to create online or offline data sink")
}
func (v *VolumeStore) newDataSink(op trace.Operation, url *url.URL) (storage.DataSink, error) {
mountPath, cleanFunc, err := v.Mount(op, url, true)
if err != nil {
return nil, err
}
f, err := os.Open(mountPath)
if err != nil {
cleanFunc()
return nil, err
}
op.Debugf("Created mount data sink for access to %s at %s", url, mountPath)
return storage.NewMountDataSink(op, f, cleanFunc), nil
}
func (v *VolumeStore) newOnlineDataSink(op trace.Operation, owner *vm.VirtualMachine, id string) (storage.DataSink, error) {
op.Debugf("Constructing toolbox data sink: %s.%s", owner.Reference(), id)
return &vsphere.ToolboxDataSink{
VM: owner,
ID: volume.Label(id),
Clean: func() { return },
}, nil
}

View File

@@ -0,0 +1,79 @@
// 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.
package vsphere
import (
"fmt"
"net/url"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/portlayer/exec"
"github.com/vmware/vic/lib/portlayer/storage/volume"
"github.com/vmware/vic/pkg/trace"
)
func VolumeJoin(op trace.Operation, handle *exec.Handle, volume *volume.Volume, mountPath string, diskOpts map[string]string) (*exec.Handle, error) {
defer trace.End(trace.Begin("vsphere.VolumeJoin", op))
if _, ok := handle.ExecConfig.Mounts[volume.ID]; ok {
return nil, fmt.Errorf("Volume with ID %s is already in container %s's mountspec config", volume.ID, handle.ExecConfig.ID)
}
if handle.ExecConfig.Mounts == nil {
handle.ExecConfig.Mounts = make(map[string]executor.MountSpec)
}
//constuct MountSpec for the tether
mountSpec := createMountSpec(volume, mountPath, diskOpts)
handle.ExecConfig.Mounts[volume.ID] = mountSpec
//append a device addition spec change to the container config
disk := handle.Guest.NewDisk()
configureVolumeVirtualDisk(disk, volume)
handle.Spec.AddVirtualDevice(disk)
return handle, nil
}
func configureVolumeVirtualDisk(disk *types.VirtualDisk, volume *volume.Volume) {
// the unit number hack may no longer be needed
unitNumber := int32(-1)
disk.CapacityInKB = 0
disk.UnitNumber = &unitNumber
disk.Backing = &types.VirtualDiskFlatVer2BackingInfo{
DiskMode: string(types.VirtualDiskModeIndependent_persistent),
VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
FileName: volume.Device.DiskPath().Path,
},
}
}
func createMountSpec(volume *volume.Volume, mountPath string, diskOpts map[string]string) executor.MountSpec {
deviceMode := diskOpts[constants.Mode]
newMountSpec := executor.MountSpec{
Source: url.URL{
Scheme: "label",
Path: volume.Label,
},
Path: mountPath,
Mode: deviceMode,
CopyMode: volume.CopyMode,
}
return newMountSpec
}

View File

@@ -0,0 +1,224 @@
// 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.
package vsphere
import (
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/constants"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/lib/portlayer/storage/volume"
"github.com/vmware/vic/lib/portlayer/storage/vsphere"
"github.com/vmware/vic/lib/portlayer/util"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
"github.com/vmware/vic/pkg/vsphere/disk"
"github.com/vmware/vic/pkg/vsphere/session"
)
const (
// TODO: this was shared with image store hence the disjoint naming. Should be updated
// but migration/upgrade implications are unclear
metaDataDir = "imageMetadata"
)
var (
// Set to false for unit tests
DetachAll = true
)
// VolumeStore caches Volume references to volumes in the system.
type VolumeStore struct {
disk.Vmdk
// Service url to this VolumeStore
SelfLink *url.URL
}
func NewVolumeStore(op trace.Operation, storeName string, s *session.Session, ds *datastore.Helper) (*VolumeStore, error) {
// Create the volume dir if it doesn't already exist
if _, err := ds.Mkdir(op, true, constants.VolumesDir); err != nil && !os.IsExist(err) {
return nil, err
}
dm, err := disk.NewDiskManager(op, s, storage.Config.ContainerView)
if err != nil {
return nil, err
}
if DetachAll {
if err = dm.DetachAll(op); err != nil {
return nil, err
}
}
u, err := util.VolumeStoreNameToURL(storeName)
if err != nil {
return nil, err
}
v := &VolumeStore{
Vmdk: disk.Vmdk{
Manager: dm,
Helper: ds,
Session: s,
},
SelfLink: u,
}
return v, nil
}
// Returns the path to the vol relative to the given store. The dir structure
// for a vol in the datastore is `<configured datastore path>/volumes/<vol ID>/<vol ID>.vmkd`.
// Everything up to "volumes" is taken care of by the datastore wrapper.
func (v *VolumeStore) volDirPath(ID string) string {
return path.Join(constants.VolumesDir, ID)
}
// Returns the path to the metadata directory for a volume
func (v *VolumeStore) volMetadataDirPath(ID string) string {
return path.Join(v.volDirPath(ID), metaDataDir)
}
// Returns the path to the vmdk itself (in datastore URL format)
func (v *VolumeStore) volDiskDSPath(ID string) *object.DatastorePath {
return &object.DatastorePath{
Datastore: v.Helper.RootURL.Datastore,
Path: path.Join(v.Helper.RootURL.Path, v.volDirPath(ID), ID+".vmdk"),
}
}
func (v *VolumeStore) VolumeCreate(op trace.Operation, ID string, store *url.URL, capacityKB uint64, info map[string][]byte) (*volume.Volume, error) {
// Create the volume directory in the store.
if _, err := v.Mkdir(op, false, v.volDirPath(ID)); err != nil {
return nil, err
}
// Get the path to the disk in datastore uri format
volDiskDSPath := v.volDiskDSPath(ID)
config := disk.NewPersistentDisk(volDiskDSPath).WithCapacity(int64(capacityKB))
// Create the disk
vmdisk, err := v.CreateAndAttach(op, config)
if err != nil {
return nil, err
}
defer v.Detach(op, vmdisk.VirtualDiskConfig)
vol, err := volume.NewVolume(store, ID, info, vmdisk, executor.CopyNew)
if err != nil {
return nil, err
}
// Make the filesystem and set its label
if err = vmdisk.Mkfs(op, vol.Label); err != nil {
return nil, err
}
// mask lost+found from containerVM
opts := []string{"noatime"}
path, err := vmdisk.Mount(op, opts)
if err != nil {
return nil, err
}
defer vmdisk.Unmount(op)
// #nosec
err = os.Mkdir(filepath.Join(path, disk.VolumeDataDir), 0755)
if err != nil {
return nil, err
}
// Persist the metadata
metaDataDir := v.volMetadataDirPath(ID)
if err = vsphere.WriteMetadata(op, v.Helper, metaDataDir, info); err != nil {
return nil, err
}
op.Infof("volumestore: %s (%s)", ID, vol.SelfLink)
return vol, nil
}
func (v *VolumeStore) VolumeDestroy(op trace.Operation, vol *volume.Volume) error {
volDir := v.volDirPath(vol.ID)
op.Infof("VolumeStore: Deleting %s", volDir)
if err := v.Rm(op, volDir); err != nil {
op.Errorf("VolumeStore: delete error: %s", err.Error())
return err
}
return nil
}
func (v *VolumeStore) VolumeGet(op trace.Operation, ID string) (*volume.Volume, error) {
// We can't get the volume directly without looking up what datastore it's on.
return nil, fmt.Errorf("not supported: use VolumesList")
}
func (v *VolumeStore) VolumesList(op trace.Operation) ([]*volume.Volume, error) {
volumes := []*volume.Volume{}
res, err := v.Ls(op, constants.VolumesDir)
if err != nil {
return nil, fmt.Errorf("error listing vols: %s", err)
}
for _, f := range res.File {
ID := f.GetFileInfo().Path
// Get the path to the disk in datastore uri format
volDiskDSPath := v.volDiskDSPath(ID)
config := disk.NewPersistentDisk(volDiskDSPath)
dev, err := disk.NewVirtualDisk(op, config, v.Manager.Disks)
if err != nil {
return nil, err
}
metaDataDir := v.volMetadataDirPath(ID)
meta, err := vsphere.GetMetadata(op, v.Helper, metaDataDir)
if err != nil {
return nil, err
}
vol, err := volume.NewVolume(v.SelfLink, ID, meta, dev, executor.CopyNew)
if err != nil {
return nil, err
}
volumes = append(volumes, vol)
}
return volumes, nil
}
func (v *VolumeStore) URL(op trace.Operation, id string) (*url.URL, error) {
path := v.volDiskDSPath(id).String()
if path == "" {
return nil, fmt.Errorf("unable to translate %s into datastore path", id)
}
return &url.URL{
Scheme: "ds",
Path: path,
}, nil
}

View File

@@ -0,0 +1,151 @@
// 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.
package vsphere
import (
"context"
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/portlayer/storage/volume"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
"github.com/vmware/vic/pkg/vsphere/tasks"
)
func TestVolumeCreateListAndRestart(t *testing.T) {
client := datastore.Session(context.TODO(), t)
if client == nil {
return
}
op := trace.NewOperation(context.Background(), "test")
// Root our datastore
testStorePath := datastore.TestName("voltest")
ds, err := datastore.NewHelper(op, client, client.Datastore, testStorePath)
if !assert.NoError(t, err) || !assert.NotNil(t, ds) {
return
}
// Create the backing store on vsphere
DetachAll = false
vsVolumeStore, err := NewVolumeStore(op, "testStoreName", client, ds)
if !assert.NoError(t, err) || !assert.NotNil(t, vsVolumeStore) {
return
}
// Clean up the mess
defer func() {
fm := object.NewFileManager(client.Vim25())
tasks.WaitForResult(context.TODO(), func(ctx context.Context) (tasks.Task, error) {
return fm.DeleteDatastoreFile(ctx, client.Datastore.Path(testStorePath), client.Datacenter)
})
}()
// Create the cache
cache := volume.NewVolumeLookupCache(op)
if !assert.NotNil(t, cache) {
return
}
// add the vs to the cache and assert the url matches
storeURL, err := cache.AddStore(op, "testStoreName", vsVolumeStore)
if !assert.NoError(t, err) || !assert.Equal(t, vsVolumeStore.SelfLink, storeURL) {
return
}
// test we can list it
m, err := cache.VolumeStoresList(op)
if !assert.NoError(t, err) || !assert.Len(t, m, 1) || !assert.Equal(t, m[0], "testStoreName") {
return
}
// Create the volumes (in parallel)
numVols := 5
wg := &sync.WaitGroup{}
wg.Add(numVols)
volumes := make(map[string]*volume.Volume)
for i := 0; i < numVols; i++ {
go func(idx int) {
defer wg.Done()
ID := fmt.Sprintf("testvolume-%d", idx)
// add some metadata if i is even
var info map[string][]byte
if idx%2 == 0 {
info = make(map[string][]byte)
info[ID] = []byte(ID)
}
outVol, err := cache.VolumeCreate(op, ID, storeURL, 10240, info)
if !assert.NoError(t, err) || !assert.NotNil(t, outVol) {
return
}
volumes[ID] = outVol
}(i)
}
wg.Wait()
// list using the datastore (skipping the cache)
outVols, err := vsVolumeStore.VolumesList(op)
if !assert.NoError(t, err) || !assert.NotNil(t, outVols) || !assert.Equal(t, numVols, len(outVols)) {
return
}
for _, outVol := range outVols {
if !assert.Equal(t, volumes[outVol.ID], outVol) {
return
}
}
// Test restart
// Create a new vs and cache to the same datastore (simulating restart) and compare
secondVStore, err := NewVolumeStore(op, "testStoreName", client, ds)
if !assert.NoError(t, err) || !assert.NotNil(t, vsVolumeStore) {
return
}
secondCache := volume.NewVolumeLookupCache(op)
if !assert.NotNil(t, secondCache) {
return
}
_, err = secondCache.AddStore(op, "testStore", secondVStore)
if !assert.NoError(t, err) {
return
}
secondOutVols, err := secondCache.VolumesList(op)
if !assert.NoError(t, err) || !assert.NotNil(t, secondOutVols) || !assert.Equal(t, numVols, len(secondOutVols)) {
return
}
for _, outVol := range secondOutVols {
// XXX we could compare the Volumes, but the paths are different the
// second time around on vsan since the vsan UUID is not included.
if !assert.NotEmpty(t, volumes[outVol.ID].Device.DiskPath()) {
return
}
}
}

View File

@@ -0,0 +1,91 @@
// Copyright 2016 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.
package vsphere
import (
"bytes"
"io/ioutil"
"path"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
)
// Write the opaque metadata blobs (by name).
// Each blob in the metadata map is written to a file with the corresponding
// name. Likewise, when we read it back (on restart) we populate the map
// accordingly.
func WriteMetadata(op trace.Operation, ds *datastore.Helper, dir string, meta map[string][]byte) error {
// XXX this should be done via disklib so this meta follows the disk in
// case of motion.
if meta != nil && len(meta) != 0 {
for name, value := range meta {
r := bytes.NewReader(value)
pth := path.Join(dir, name)
op.Infof("Writing metadata %s", pth)
if err := ds.Upload(op, r, pth); err != nil {
return err
}
}
} else {
if _, err := ds.Mkdir(op, false, dir); err != nil {
return err
}
}
return nil
}
// Read the metadata from the given dir
func GetMetadata(op trace.Operation, ds *datastore.Helper, dir string) (map[string][]byte, error) {
res, err := ds.Ls(op, dir)
if err != nil {
return nil, err
}
if len(res.File) == 0 {
op.Infof("No meta found for %s", dir)
return nil, nil
}
meta := make(map[string][]byte)
for _, f := range res.File {
// we're only interested in files, not folders
finfo, ok := f.(*types.FileInfo)
if !ok {
continue
}
p := path.Join(dir, finfo.Path)
op.Infof("Getting metadata %s", p)
rc, err := ds.Download(op, p)
if err != nil {
return nil, err
}
defer rc.Close()
buf, err := ioutil.ReadAll(rc)
if err != nil {
return nil, err
}
meta[finfo.Path] = buf
}
return meta, nil
}

View File

@@ -0,0 +1,158 @@
// Copyright 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.
package vsphere
import (
"net"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/vmware/govmomi/guest"
"github.com/vmware/govmomi/guest/toolbox"
"github.com/vmware/govmomi/task"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/tether/shared"
"github.com/vmware/vic/pkg/retry"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/vm"
)
const (
VixEToolsNotRunning = "(3016, 0)"
)
var (
toolboxRetryConf *retry.BackoffConfig
)
func init() {
toolboxRetryConf = retry.NewBackoffConfig()
// These numbers are somewhat arbitrary best guesses
toolboxRetryConf.MaxElapsedTime = time.Second * 30
toolboxRetryConf.InitialInterval = time.Millisecond * 500
toolboxRetryConf.MaxInterval = time.Second * 5
}
// Parse Archive builds an archive url with disklabel, filtersec, recursive, and data booleans.
func BuildArchiveURL(op trace.Operation, disklabel, target string, fs *archive.FilterSpec, recurse, data bool) (string, error) {
encodedSpec, err := archive.EncodeFilterSpec(op, fs)
if err != nil {
return "", err
}
target = path.Join("/archive:/", target)
// if diskLabel is longer than 16 characters, then the function was passed a containerID
// use containerfs as the diskLabel
if len(disklabel) > 16 {
disklabel = "containerfs"
}
// note that the query parameters a SkipX for recurse and data so values are inverted
target += "?" + (url.Values{
shared.DiskLabelQueryName: []string{disklabel},
shared.FilterSpecQueryName: []string{*encodedSpec},
shared.SkipRecurseQueryName: []string{strconv.FormatBool(!recurse)},
shared.SkipDataQueryName: []string{strconv.FormatBool(!data)},
}).Encode()
op.Debugf("OnlineData* Url: %s", target)
return target, nil
}
// GetToolboxClient returns a toolbox client given a vm and id
func GetToolboxClient(op trace.Operation, vm *vm.VirtualMachine, id string) (*toolbox.Client, error) {
opmgr := guest.NewOperationsManager(vm.Session.Client.Client, vm.Reference())
pm, err := opmgr.ProcessManager(op)
if err != nil {
op.Debugf("Failed to create new process manager ")
return nil, err
}
fm, err := opmgr.FileManager(op)
if err != nil {
op.Debugf("Failed to create new file manager ")
return nil, err
}
return &toolbox.Client{
ProcessManager: pm,
FileManager: fm,
Authentication: &types.NamePasswordAuthentication{
Username: id,
},
}, nil
}
// isInvalidStateError is used to identify whether the supplied error is an InvalidState fault
func isInvalidStateError(err error) bool {
if soap.IsSoapFault(err) {
switch soap.ToSoapFault(err).VimFault().(type) {
case types.InvalidState:
return true
}
}
if soap.IsVimFault(err) {
switch soap.ToVimFault(err).(type) {
case *types.InvalidState:
return true
}
}
switch err := err.(type) {
case task.Error:
switch err.Fault().(type) {
case *types.InvalidState:
return true
}
}
return false
}
// IsToolBoxConflictErr checks for conflictError for online import
func IsToolBoxStateChangeErr(err error) bool {
// check if error has to do with toolbox state changes
if soap.IsSoapFault(err) {
switch soap.ToSoapFault(err).VimFault().(type) {
case types.InvalidState:
return true
case types.InvalidPowerState:
return true
case types.GuestOperationsUnavailable:
return true
case types.SystemError:
return strings.Contains(err.Error(), VixEToolsNotRunning)
}
}
switch err.(type) {
case *url.Error:
err = err.(*url.Error).Err
switch err.(type) {
case *net.OpError:
// can check for error message as well
return true
}
}
// NOTE: on certain failures toolbox only returns 500 which can be caused by state change in the middle
// but can also be caused by invalid command. There is no way to tell unless toolbox returns more information.
return false
}

View File

@@ -0,0 +1,89 @@
// Copyright 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.
package vsphere
import (
"bytes"
"io"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/pkg/retry"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// ToolboxDataSink implements the DataSink interface for mounted devices
type ToolboxDataSink struct {
ID string
VM *vm.VirtualMachine
Clean func()
}
// Sink returns the data sink associated with the DataSink
func (t *ToolboxDataSink) Sink() interface{} {
return t.VM
}
// Import writes `data` to the data sink associated with this DataSink
func (t *ToolboxDataSink) Import(op trace.Operation, spec *archive.FilterSpec, data io.ReadCloser) error {
defer trace.End(trace.Begin("toolbox import"))
client, err := GetToolboxClient(op, t.VM, t.ID)
if err != nil {
op.Debugf("Cannot get toolbox client: %s", err.Error())
return err
}
target, err := BuildArchiveURL(op, t.ID, spec.RebasePath, spec, true, true)
if err != nil {
op.Debugf("Cannot build archive url: %s", err.Error())
return err
}
// buffer the data - needed to allow retry or the Upload drains the reader before the failure
// and we lose the data
// TODO: should look into chunking so that we can support copy of very large files.
// NOW: need a check that size doesn't exceed available memory - and error recommending offline
// copy as alternative
buf := new(bytes.Buffer)
_, err = io.Copy(buf, data)
if err != nil {
op.Errorf("Unable to buffer archive data for upload")
return err
}
// upload the gzip archive.
p := soap.DefaultUpload
retryFunc := func() error {
return client.Upload(op, buf, target, p, &types.GuestPosixFileAttributes{}, true)
}
err = retry.DoWithConfig(retryFunc, isInvalidStateError, toolboxRetryConf)
if err != nil {
op.Debugf("Upload error: %s", err.Error())
}
return err
}
func (t *ToolboxDataSink) Close() error {
t.Clean()
return nil
}

View File

@@ -0,0 +1,156 @@
// Copyright 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.
package vsphere
import (
"archive/tar"
"io"
"io/ioutil"
"os"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/portlayer/storage"
"github.com/vmware/vic/pkg/retry"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// ToolboxDataSource implements the DataSource interface for mounted devices
type ToolboxDataSource struct {
ID string
VM *vm.VirtualMachine
Clean func()
}
// Source returns the data source associated with the DataSource
func (t *ToolboxDataSource) Source() interface{} {
return t.VM
}
// Export reads data from the associated data source and returns it as a tar archive
func (t *ToolboxDataSource) Export(op trace.Operation, spec *archive.FilterSpec, data bool) (io.ReadCloser, error) {
defer trace.End(trace.Begin("toolbox export"))
client, err := GetToolboxClient(op, t.VM, t.ID)
if err != nil {
op.Errorf("Cannot get toolbox client: %s", err.Error())
return nil, err
}
var readers []io.Reader
for inclusion := range spec.Inclusions {
// build a proper target
target, err := BuildArchiveURL(op, t.ID, inclusion, spec, true, true)
if err != nil {
op.Errorf("Cannot build archive url: %s", err.Error())
return nil, err
}
var tar io.ReadCloser
var contentLength int64
retryFunc := func() error {
var retryErr error
tar, contentLength, retryErr = client.Download(op, target)
return retryErr
}
err = retry.DoWithConfig(retryFunc, isInvalidStateError, toolboxRetryConf)
if err != nil {
op.Errorf("Download error: %s", err.Error())
return nil, err
}
op.Debugf("Downloaded from %s with size %d", target, contentLength)
readers = append(readers, tar)
}
return ioutil.NopCloser(io.MultiReader(readers...)), nil
}
// Stat returns file stats of the destination header determined but the filterspec inclusion path
func (t *ToolboxDataSource) Stat(op trace.Operation, spec *archive.FilterSpec) (*storage.FileStat, error) {
defer trace.End(trace.Begin("toolbox stat"))
client, err := GetToolboxClient(op, t.VM, t.ID)
if err != nil {
op.Errorf("Cannot get toolbox client: %s", err.Error())
return nil, err
}
// should only find a single path to stat, but make sure here.
if len(spec.Inclusions) != 1 {
op.Errorf("Stat called on more than one path: %+v", spec.Inclusions)
}
var statPath string
inclusions := len(spec.Inclusions)
if inclusions == 0 {
op.Debugf("filter spec for stat operation has no inclusion specified : %#v", *spec)
}
if inclusions > 1 {
op.Debugf("filter spec for stat operation had multiple inclusion paths : %#v", *spec)
}
for inclusion := range spec.Inclusions {
statPath = inclusion
}
target, err := BuildArchiveURL(op, t.ID, statPath, spec, false, false)
if err != nil {
op.Errorf("Cannot build archive url: %s", err.Error())
return nil, err
}
var statTar io.ReadCloser
retryFunc := func() error {
var retryErr error
statTar, _, retryErr = client.Download(op, target)
return retryErr
}
err = retry.DoWithConfig(retryFunc, isInvalidStateError, toolboxRetryConf)
if err != nil {
op.Errorf("Download error: %s", err.Error())
return nil, err
}
defer statTar.Close()
// decode from guest tools
header, err := tar.NewReader(statTar).Next()
if err == io.EOF {
// special case - unable to get a single header translates to Not Found
return nil, os.ErrNotExist
}
if err != nil {
return nil, err
}
stat := &storage.FileStat{
Mode: uint32(header.FileInfo().Mode()),
Name: header.Name,
Size: header.Size,
ModTime: header.ModTime,
LinkTarget: header.Linkname,
}
return stat, nil
}
func (t *ToolboxDataSource) Close() error {
t.Clean()
return nil
}

Some files were not shown because too many files have changed in this diff Show More