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

341
vendor/github.com/vmware/vic/lib/tether/cmd_test.go generated vendored Normal file
View File

@@ -0,0 +1,341 @@
// 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 tether
import (
"fmt"
"os/exec"
"testing"
log "github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
)
/////////////////////////////////////////////////////////////////////////////////////
// TestPathLookup constructs the spec for a Session where the binary path must be
// resolved from the PATH environment variable - this is a variation from normal
// Cmd handling where that is done during creation of Cmd
//
func TestPathLookup(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "pathlookup",
Name: "tether_test_executor",
},
Sessions: map[string]*executor.SessionConfig{
"pathlookup": {
Common: executor.Common{
ID: "pathlookup",
Name: "tether_test_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test relative path
Path: "date",
Args: []string{"date", "--reference=/"},
Env: []string{"PATH=/bin"},
Dir: "/bin",
},
},
},
}
_, src, err := RunTether(t, &cfg, mocker)
assert.NoError(t, err, "Didn't expected error from runTether")
result := ExecutorConfig{}
extraconfig.Decode(src, &result)
assert.Equal(t, "true", result.Sessions["pathlookup"].Started, "Expected command to have been started successfully")
assert.Equal(t, 0, result.Sessions["pathlookup"].ExitStatus, "Expected command to have exited cleanly")
}
func TestRelativePath(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "relpath",
Name: "tether_test_executor",
},
Sessions: map[string]*executor.SessionConfig{
"relpath": {
Common: executor.Common{
ID: "relpath",
Name: "tether_test_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test relative path
Path: "./date",
Args: []string{"./date", "--reference=/"},
Env: []string{"PATH="},
Dir: "/bin",
},
},
},
}
_, src, err := RunTether(t, &cfg, mocker)
assert.NoError(t, err, "Didn't expected error from RunTether")
result := ExecutorConfig{}
extraconfig.Decode(src, &result)
assert.Equal(t, "true", result.Sessions["relpath"].Started, "Expected command to have been started successfully")
assert.Equal(t, 0, result.Sessions["relpath"].ExitStatus, "Expected command to have exited cleanly")
}
func TestAbsPath(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "abspath",
Name: "tether_test_executor",
},
Sessions: map[string]*executor.SessionConfig{
"abspath": {
Common: executor.Common{
ID: "abspath",
Name: "tether_test_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test abs path
Path: "/bin/date",
Args: []string{"date", "--reference=/"},
Env: []string{},
Dir: "/",
},
},
},
}
_, src, err := RunTether(t, &cfg, mocker)
assert.NoError(t, err, "Didn't expected error from RunTether")
result := ExecutorConfig{}
extraconfig.Decode(src, &result)
assert.Equal(t, "true", result.Sessions["abspath"].Started, "Expected command to have been started successfully")
assert.Equal(t, 0, result.Sessions["abspath"].ExitStatus, "Expected command to have exited cleanly")
// read the output from the session
log := mocker.SessionLogBuffer.Bytes()
// block until tether exits
<-mocker.Cleaned
// run the command directly
out, err := exec.Command("/bin/date", "--reference=/").Output()
if err != nil {
fmt.Printf("Failed to run date for comparison data: %s", err)
t.Error(err)
return
}
if !assert.Equal(t, out, log) {
return
}
}
func TestHalt(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "abspath",
Name: "tether_test_executor",
},
Sessions: map[string]*executor.SessionConfig{
"abspath": {
Common: executor.Common{
ID: "abspath",
Name: "tether_test_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test abs path
Path: "/bin/date",
Args: []string{"date", "--reference=/"},
Env: []string{},
Dir: "/",
},
},
},
}
_, src, err := RunTether(t, &cfg, mocker)
assert.NoError(t, err, "Didn't expected error from RunTether")
// block until tether exits
<-mocker.Cleaned
result := ExecutorConfig{}
extraconfig.Decode(src, &result)
assert.Equal(t, "true", result.Sessions["abspath"].Started, "Expected command to have been started successfully")
assert.Equal(t, 0, result.Sessions["abspath"].ExitStatus, "Expected command to have exited cleanly")
// read the output from the session
log := mocker.SessionLogBuffer.Bytes()
// run the command directly
out, err := exec.Command("/bin/date", "--reference=/").Output()
if err != nil {
fmt.Printf("Failed to run date for comparison data: %s", err)
t.Error(err)
return
}
if !assert.Equal(t, out, log) {
return
}
}
func TestAbsPathRepeat(t *testing.T) {
log.SetLevel(log.WarnLevel)
for i := 0; i < 2000 && !t.Failed(); i++ {
TestAbsPath(t)
}
defer log.SetLevel(log.DebugLevel)
}
//
/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
// TestMissingBinaryConfig constructs the spec for a Session with invalid binary path
//
func TestMissingBinary(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "missing",
Name: "tether_test_executor",
},
Sessions: map[string]*executor.SessionConfig{
"missing": {
Common: executor.Common{
ID: "missing",
Name: "tether_test_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test relative path
Path: "/not/there",
Args: []string{"/not/there"},
Env: []string{"PATH=/not"},
Dir: "/",
},
},
},
}
_, src, err := RunTether(t, &cfg, mocker)
assert.Error(t, err, "Expected error from RunTether")
// refresh the cfg with current data
extraconfig.Decode(src, &cfg)
// check the launch status was failed
status := cfg.Sessions["missing"].Started
assert.Equal(t, "stat /not/there: no such file or directory", status, "Expected status to have a command not found error message")
}
//
/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
// TestMissingRelativeBinaryConfig constructs the spec for a Session with invalid binary path
//
func TestMissingRelativeBinary(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "missing",
Name: "tether_test_executor",
},
Sessions: map[string]*executor.SessionConfig{
"missing": {
Common: executor.Common{
ID: "missing",
Name: "tether_test_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test relative path
Path: "notthere",
Args: []string{"notthere"},
Env: []string{"PATH=/not"},
Dir: "/",
},
},
},
}
_, src, err := RunTether(t, &cfg, mocker)
assert.Error(t, err, "Expected error from RunTether")
// refresh the cfg with current data
extraconfig.Decode(src, &cfg)
// check the launch status was failed
status := cfg.Sessions["missing"].Started
assert.Equal(t, "notthere: no such executable in PATH", status, "Expected status to have a command not found error message")
}
//
/////////////////////////////////////////////////////////////////////////////////////

184
vendor/github.com/vmware/vic/lib/tether/config.go generated vendored Normal file
View File

@@ -0,0 +1,184 @@
// 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 tether
import (
"io"
"net"
"os"
"os/exec"
"sync"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/pkg/dio"
"github.com/vmware/vic/pkg/ip"
)
type ExecutorConfig struct {
// allow us to lock the maps when config is being updated
// subelements tend to have their own locks
sync.Mutex
// The name of the system
Name string `vic:"0.1" scope:"read-only" key:"common/name"`
// ID corresponds to that of the primary session
ID string `vic:"0.1" scope:"read-only" key:"common/id"`
// Debug is a numeric level that controls extent of debugging
DebugLevel int `vic:"0.1" scope:"read-only" key:"diagnostics/debug"`
// Exclusive access to childPidTable
pidMutex sync.Mutex `vic:"0.1" scope:"read-only" recurse:"depth=0"`
// Set of child PIDs created by us.
pids map[int]*SessionConfig `vic:"0.1" scope:"read-only" recurse:"depth=0"`
// Sessions is the set of sessions currently hosted by this executor
// These are keyed by session ID
Sessions map[string]*SessionConfig `vic:"0.1" scope:"read-only" key:"sessions"`
// Execs is the set of non-persistent sessions hosted by this executor
Execs map[string]*SessionConfig `vic:"0.1" scope:"read-only,non-persistent" key:"execs"`
// Maps the mount name to the detail mount specification
Mounts map[string]executor.MountSpec `vic:"0.1" scope:"read-only" key:"mounts"`
// This describes an executors presence on a network, and contains sufficient
// information to configure the interface in the guest.
Networks map[string]*NetworkEndpoint `vic:"0.1" scope:"read-only" key:"networks"`
// Key is the host key used during communicate back with the Interaction endpoint if any
// Used if the in-guest tether is responsible for authenticating the connection
Key []byte `vic:"0.1" scope:"read-only" key:"key"`
// AsymmetricRouting is set to true if the VCH needs to be setup for asymmetric routing
AsymmetricRouting bool `vic:"0.1" scope:"read-only" key:"asymrouting"`
// Hostname and domainname provided by personality
Hostname string `vic:"0.1" scope:"read-only" key:"hostname"`
Domainname string `vic:"0.1" scope:"read-only" key:"domainname"`
}
// SessionConfig defines the content of a session - this maps to the root of a process tree
// inside an executor
// This is close to but not perfectly aligned with the new docker/docker/daemon/execdriver/driver:CommonProcessConfig
type SessionConfig struct {
// Protects the structure
sync.Mutex `vic:"0.1" scope:"read-only" recurse:"depth=0"`
// The primary session may have the same ID as the executor owning it
executor.Common `vic:"0.1" scope:"read-only" key:"common"`
executor.Detail `vic:"0.1" scope:"read-write" key:"detail"`
// Diagnostics holds basic diagnostics data
Diagnostics executor.Diagnostics `vic:"0.1" scope:"read-write" key:"diagnostics"`
// The primary process for the session
Cmd exec.Cmd `vic:"0.1" scope:"read-only" key:"cmd" recurse:"depth=2,nofollow"`
// The exit status of the process, if any
ExitStatus int `vic:"0.1" scope:"read-write" key:"status"`
Started string `vic:"0.1" scope:"read-write" key:"started"`
// Allow attach
Attach bool `vic:"0.1" scope:"read-only" key:"attach"`
OpenStdin bool `vic:"0.1" scope:"read-only" key:"openstdin"`
// Delay launching the Cmd until an attach request comes
RunBlock bool `vic:"0.1" scope:"read-write" key:"runblock"`
// Should this config be activated or not
Active bool `vic:"0.1" scope:"read-only" key:"active"`
// Allocate a tty or not
Tty bool `vic:"0.1" scope:"read-only" key:"tty"`
// Restart controls whether a process gets relaunched if it exists
Restart bool `vic:"0.1" scope:"read-only" key:"restart"`
// StopSignal is the signal name or number used to stop a container
StopSignal string `vic:"0.1" scope:"read-only" key:"stopSignal"`
// User and group for setuid programs
User string `vic:"0.1" scope:"read-only" key:"user"`
Group string `vic:"0.1" scope:"read-only" key:"group"`
// if there's a pty then we need additional management data
Pty *os.File `vic:"0.1" scope:"read-only" recurse:"depth=0"`
Outwriter dio.DynamicMultiWriter `vic:"0.1" scope:"read-only" recurse:"depth=0"`
Errwriter dio.DynamicMultiWriter `vic:"0.1" scope:"read-only" recurse:"depth=0"`
Reader dio.DynamicMultiReader `vic:"0.1" scope:"read-only" recurse:"depth=0"`
StdinPipe io.WriteCloser `vic:"0.1" scope:"read-only" recurse:"depth=0"`
StdoutPipe io.ReadCloser `vic:"0.1" scope:"read-only" recurse:"depth=0"`
StderrPipe io.ReadCloser `vic:"0.1" scope:"read-only" recurse:"depth=0"`
// Blocks launching the process.
// The channel contains no value; were only interested in its closed property.
ClearToLaunch chan struct{} `vic:"0.1" scope:"read-only" recurse:"depth=0"`
wait *sync.WaitGroup
// record the config prefix associated with this session - allows partial encoding
// without hardcoding the sessions parent in the config.
// This is basically a workaround for:
// a. not having integrated Exec sessions in to the Sessions map
// b. having only an implementation of CalculateKeys that requires a prefix instead
// of performing a search
extraconfigKey string
}
type NetworkEndpoint struct {
// Common.Name - the nic alias requested (only one name and one alias possible in linux)
// Common.ID - pci slot of the vnic allowing for interface identifcation in-guest
executor.Common
// Whether this endpoint's IP was specified by the client (true if it was)
Static bool `vic:"0.1" scope:"read-only" key:"static"`
// IP address to assign
IP *net.IPNet `vic:"0.1" scope:"read-only" key:"ip"`
// Actual IP address assigned
Assigned net.IPNet `vic:"0.1" scope:"read-write" key:"assigned"`
// The network in which this information should be interpreted. This is embedded directly rather than
// as a pointer so that we can ensure the data is consistent
Network executor.ContainerNetwork `vic:"0.1" scope:"read-only" key:"network"`
// DHCP runtime info
DHCP *DHCPInfo `vic:"0.1" scope:"read-only" recurse:"depth=0"`
Ports []string `vic:"0.1" scope:"read-only" key:"ports"`
// is this endpoint connected to an internal network?
Internal bool `vic:"0.1" scope:"read-only" key:"internal"`
// whether the network config was successfully applied
configured bool `vic:"0.1" scope:"read-only" recurse:"depth=0"`
}
func (e *NetworkEndpoint) IsDynamic() bool {
return !e.Static && (e.IP == nil || ip.IsUnspecifiedIP(e.IP.IP))
}
type DHCPInfo struct {
Assigned net.IPNet
Nameservers []net.IP
Gateway net.IPNet
}

101
vendor/github.com/vmware/vic/lib/tether/config_test.go generated vendored Normal file
View File

@@ -0,0 +1,101 @@
// 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 tether
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/pkg/ip"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
)
var (
localhost, lmask, _ = net.ParseCIDR("127.0.0.2/24")
gateway, gmask, _ = net.ParseCIDR("127.0.0.1/24")
)
func TestToExtraConfig(t *testing.T) {
exec := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "deadbeef",
Name: "configtest",
},
Sessions: map[string]*executor.SessionConfig{
"deadbeef": {
Cmd: executor.Cmd{
Path: "/bin/bash",
Args: []string{"/bin/bash", "-c", "echo hello"},
Dir: "/",
Env: []string{"HOME=/", "PATH=/bin"},
},
},
"beefed": {
Cmd: executor.Cmd{
Path: "/bin/bash",
Args: []string{"/bin/bash", "-c", "echo goodbye"},
Dir: "/",
Env: []string{"HOME=/", "PATH=/bin"},
},
},
},
Networks: map[string]*executor.NetworkEndpoint{
"eth0": {
Static: true,
IP: &net.IPNet{IP: localhost, Mask: lmask.Mask},
Network: executor.ContainerNetwork{
Common: executor.Common{
Name: "notsure",
},
Gateway: net.IPNet{IP: gateway, Mask: gmask.Mask},
Destinations: []net.IPNet{},
Nameservers: []net.IP{},
Pools: []ip.Range{},
Aliases: []string{},
},
},
},
}
exec.Networks["eth0"].Network.Assigned.Nameservers = []net.IP{}
// encode exec package's ExecutorConfig
encoded := map[string]string{}
extraconfig.Encode(extraconfig.MapSink(encoded), exec)
// decode into this package's ExecutorConfig
var decoded ExecutorConfig
extraconfig.Decode(extraconfig.MapSource(encoded), &decoded)
// the source and destination structs are different - we're doing a sparse comparison
expectedNet := exec.Networks["eth0"]
actualNet := decoded.Networks["eth0"]
assert.Equal(t, expectedNet.Common, actualNet.Common)
assert.Equal(t, expectedNet.Static, actualNet.Static)
assert.Equal(t, expectedNet.Assigned, actualNet.Assigned)
assert.Equal(t, expectedNet.Network, actualNet.Network)
expectedSession := exec.Sessions["deadbeef"]
actualSession := decoded.Sessions["deadbeef"]
assert.Equal(t, expectedSession.Cmd.Path, actualSession.Cmd.Path)
assert.Equal(t, expectedSession.Cmd.Args, actualSession.Cmd.Args)
assert.Equal(t, expectedSession.Cmd.Dir, actualSession.Cmd.Dir)
assert.Equal(t, expectedSession.Cmd.Env, actualSession.Cmd.Env)
}

317
vendor/github.com/vmware/vic/lib/tether/exec_test.go generated vendored Normal file
View File

@@ -0,0 +1,317 @@
// 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 tether
import (
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
)
func TestExec(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
dir, err := ioutil.TempDir("", "tetherTestExec")
if !assert.Nil(t, err, "Unable to create temp file") {
t.FailNow()
}
flagFile := path.Join(dir, "flagfile")
defer os.RemoveAll(flagFile)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "primary",
Name: "tether_test_executor",
},
Sessions: map[string]*executor.SessionConfig{
"primary": {
Common: executor.Common{
ID: "primary",
Name: "tether_test_session",
},
Tty: true,
Active: true,
OpenStdin: true,
Cmd: executor.Cmd{
// test abs path
Path: "/bin/bash",
Args: []string{"/bin/bash", "-c", fmt.Sprintf("until /usr/bin/test -e %s;do /bin/sleep 0.1;done", flagFile)},
Env: []string{},
Dir: "/",
},
},
},
}
tthr, src, sink := StartTether(t, &cfg, mocker)
<-mocker.Started
// at this point the primary process should be up and running, so grab the pid
result := ExecutorConfig{}
extraconfig.Decode(src, &result)
for result.Sessions["primary"].Started != "true" {
time.Sleep(200 * time.Millisecond)
extraconfig.Decode(src, &result)
}
// configure a command to kill the primary
cfg.Execs = map[string]*executor.SessionConfig{
"touch": {
Common: executor.Common{
ID: "touch",
Name: "tether_test_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test abs path
Path: "/bin/touch",
Args: []string{"touch", flagFile},
Env: []string{},
Dir: "/",
},
},
}
// update the active config
extraconfig.Encode(sink, cfg)
// trigger tether reload
tthr.Reload()
<-mocker.Reloaded
// block until tether exits - exit within test timeout is a pass given the pairing of processes in the test
<-mocker.Cleaned
}
func TestMissingBinaryNonFatal(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
dir, err := ioutil.TempDir("", "tetherTestExec")
if !assert.Nil(t, err, "Unable to create temp file") {
t.FailNow()
}
flagFile := path.Join(dir, "flagfile")
defer os.RemoveAll(flagFile)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "primary",
Name: "tether_test_executor",
},
Diagnostics: executor.Diagnostics{
DebugLevel: 2,
},
Sessions: map[string]*executor.SessionConfig{
"primary": {
Common: executor.Common{
ID: "primary",
Name: "tether_test_session",
},
Tty: true,
Active: true,
OpenStdin: true,
Cmd: executor.Cmd{
// test abs path
Path: "/bin/bash",
Args: []string{"/bin/bash", "-c", fmt.Sprintf("until /usr/bin/test -e %s;do /bin/sleep 0.1;done", flagFile)},
Env: []string{},
Dir: "/",
},
},
},
}
tthr, src, sink := StartTether(t, &cfg, mocker)
<-mocker.Started
// at this point the primary process should be up and running, so grab the pid
extraconfig.Decode(src, &cfg)
for cfg.Sessions["primary"].Started != "true" {
time.Sleep(time.Second)
extraconfig.Decode(src, &cfg)
}
// configure a command to kill the primary
cfg.Execs = map[string]*executor.SessionConfig{
"nonfatal": {
Common: executor.Common{
ID: "nonfatal",
Name: "nonfatal_exec_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test abs path
Path: "/not/there",
Args: []string{"/not/there"},
Env: []string{},
Dir: "/",
},
},
}
// update the active config
extraconfig.Encode(sink, cfg)
// trigger tether reload
tthr.Reload()
// wait for the reload to be processed
<-mocker.Reloaded
for cfg.Execs["nonfatal"].Started == "" {
time.Sleep(time.Second)
extraconfig.Decode(src, &cfg)
}
// reconfigure the missing process to make it inactive
cfg.Execs["nonfatal"].Active = false
// configure a command to kill the primary
cfg.Execs["touch"] = &executor.SessionConfig{
Common: executor.Common{
ID: "touch",
Name: "tether_test_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test abs path
Path: "/bin/touch",
Args: []string{"touch", flagFile},
Env: []string{},
Dir: "/",
},
ExitStatus: -1,
}
// update the active config
extraconfig.Encode(sink, cfg)
// trigger tether reload
tthr.Reload()
// block until tether exits - exit within test timeout is a pass given the pairing of processes in the test
<-mocker.Cleaned
// update config - into existing data
extraconfig.Decode(src, &cfg)
// check that nonfatal error was as expected
status := cfg.Execs["nonfatal"].Started
assert.Equal(t, "stat /not/there: no such file or directory", status, "Expected nonfatal status to have a command not found error message")
status = cfg.Execs["touch"].Started
assert.Equal(t, "true", status, "Expected touch status to be clean")
exit := cfg.Execs["touch"].ExitStatus
assert.Equal(t, 0, exit, "Expected touch exit status to be success")
}
func TestExecHalt(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
dir, err := ioutil.TempDir("", "tetherTestExec")
if !assert.Nil(t, err, "Unable to create temp file") {
t.FailNow()
}
flagFile := path.Join(dir, "flagfile")
defer os.RemoveAll(flagFile)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "primary",
Name: "tether_test_executor",
},
Sessions: map[string]*executor.SessionConfig{
"primary": {
Common: executor.Common{
ID: "primary",
Name: "tether_test_session",
},
Tty: true,
Active: true,
OpenStdin: true,
Cmd: executor.Cmd{
// test abs path
Path: "/bin/bash",
Args: []string{"/bin/bash", "-c", fmt.Sprintf("until /usr/bin/test -e %s;do /bin/sleep 0.1;done", flagFile)},
Env: []string{},
Dir: "/",
},
},
},
}
tthr, src, sink := StartTether(t, &cfg, mocker)
<-mocker.Started
// at this point the primary process should be up and running, so grab the pid
result := ExecutorConfig{}
extraconfig.Decode(src, &result)
for result.Sessions["primary"].Started != "true" {
time.Sleep(200 * time.Millisecond)
extraconfig.Decode(src, &result)
}
// reconfigure the primary process to make it inactive
cfg.Sessions["primary"].Active = false
// update the active config
extraconfig.Encode(sink, cfg)
// trigger tether reload
tthr.Reload()
// block until tether exits - exit within test timeout is a pass given we're deactivating the primary
<-mocker.Cleaned
// process should have been killed via signal due to deactivation
extraconfig.Decode(src, &result)
assert.Equal(t, -1, result.Sessions["primary"].ExitStatus, "Expected exit code for primary to indicate signal death")
}

78
vendor/github.com/vmware/vic/lib/tether/interfaces.go generated vendored Normal file
View File

@@ -0,0 +1,78 @@
// 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 tether
import (
"context"
"io"
"net/url"
"os"
"github.com/vmware/vic/pkg/dio"
)
// UtilityFn is the sigature of a function that can be used to launch a utility process
type UtilityFn func() (*os.Process, error)
// Operations defines the set of operations that Tether depends upon. These are split out for:
// * portability
// * dependency injection (primarily for testing)
// * behavioural control (e.g. what behaviour is required when a session exits)
type Operations interface {
Setup(Config) error
Cleanup() error
// Log returns the tether debug log writer
Log() (io.Writer, error)
SetHostname(hostname string, aliases ...string) error
SetupFirewall(ctx context.Context, config *ExecutorConfig) error
Apply(endpoint *NetworkEndpoint) error
MountLabel(ctx context.Context, label, target string) error
MountTarget(ctx context.Context, source url.URL, target string, mountOptions string) error
CopyExistingContent(source string) error
Fork() error
// Returns two DynamicMultiWriters for stdout and stderr
SessionLog(session *SessionConfig) (dio.DynamicMultiWriter, dio.DynamicMultiWriter, error)
// Returns a function to invoke after the session state has been persisted
HandleSessionExit(config *ExecutorConfig, session *SessionConfig) func()
ProcessEnv(env []string) []string
// LaunchUtility starts a process and provides a way to block on completion and retrieve
// it's exit code. This is needed to co-exist with a childreaper.
LaunchUtility(UtilityFn) (<-chan int, error)
// HandleUtilityExit will process the utility exit. If the pid cannot be matched to a launched
// utility process then this returns false and does nothing.
HandleUtilityExit(pid, exitCode int) bool
}
// Tether presents the consumption interface for code needing to run a tether
type Tether interface {
Start() error
Stop() error
Reload()
Register(name string, ext Extension)
}
// Extension is a very simple extension interface for supporting code that need to be
// notified when the configuration is reloaded.
type Extension interface {
Start() error
Reload(config *ExecutorConfig) error
Stop() error
}
type Config interface {
UpdateNetworkEndpoint(e *NetworkEndpoint) error
Flush() error
}

113
vendor/github.com/vmware/vic/lib/tether/mocks.go generated vendored Normal file
View File

@@ -0,0 +1,113 @@
// 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 tether
import (
"net"
"time"
"github.com/vmware/vic/lib/etcconf"
)
type MockSyscall struct{}
func (m MockSyscall) Mount(source string, target string, fstype string, flags uintptr, data string) error {
return nil
}
func (m MockSyscall) Sethostname(_ []byte) error {
return nil
}
func (m MockSyscall) Symlink(_, _ string) error {
return nil
}
func (m MockSyscall) Unmount(_ string, _ int) error {
return nil
}
type MockHosts struct{}
func (h MockHosts) Load() error {
return nil
}
func (h MockHosts) Save() error {
return nil
}
func (h MockHosts) Copy(conf etcconf.Conf) error {
return nil
}
func (h MockHosts) SetHost(_ string, _ net.IP) {
}
func (h MockHosts) RemoveHost(_ string) {
}
func (h MockHosts) RemoveAll() {
}
func (h MockHosts) HostIP(_ string) []net.IP {
return nil
}
func (h MockHosts) Path() string {
return ""
}
type MockResolvConf struct{}
func (h MockResolvConf) Load() error {
return nil
}
func (h MockResolvConf) Save() error {
return nil
}
func (h MockResolvConf) Copy(conf etcconf.Conf) error {
return nil
}
func (h MockResolvConf) AddNameservers(...net.IP) {
}
func (h MockResolvConf) RemoveNameservers(...net.IP) {
}
func (h MockResolvConf) Nameservers() []net.IP {
return nil
}
func (h MockResolvConf) Attempts() uint {
return etcconf.DefaultAttempts
}
func (h MockResolvConf) Timeout() time.Duration {
return etcconf.DefaultTimeout
}
func (h MockResolvConf) SetAttempts(uint) {
}
func (h MockResolvConf) SetTimeout(time.Duration) {
}
func (h MockResolvConf) Path() string {
return ""
}

View File

@@ -0,0 +1,170 @@
// 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 msgs
import (
"fmt"
"strconv"
"strings"
"golang.org/x/crypto/ssh"
)
// All of the messages passed over the ssh channel/global mux are (or will be)
// defined here.
type Message interface {
// Returns the message name
RequestType() string
// Marshalled version of the message
Marshal() []byte
// Unmarshal unpacks the message
Unmarshal([]byte) error
}
// WindowChangeMsg the RFC4254 struct
const WindowChangeReq = "window-change"
type WindowChangeMsg struct {
Columns uint32
Rows uint32
WidthPx uint32
HeightPx uint32
}
func (wc *WindowChangeMsg) RequestType() string {
return WindowChangeReq
}
func (wc *WindowChangeMsg) Marshal() []byte {
return ssh.Marshal(*wc)
}
func (wc *WindowChangeMsg) Unmarshal(payload []byte) error {
return ssh.Unmarshal(payload, wc)
}
var (
Signals = map[ssh.Signal]int{
ssh.SIGABRT: 6,
ssh.SIGALRM: 14,
ssh.SIGFPE: 8,
ssh.SIGHUP: 1,
ssh.SIGILL: 4,
ssh.SIGINT: 2,
ssh.SIGKILL: 9,
ssh.SIGPIPE: 13,
ssh.SIGQUIT: 3,
ssh.SIGSEGV: 11,
ssh.SIGTERM: 15,
ssh.SIGUSR1: 10,
ssh.SIGUSR2: 12,
}
)
// PingMsg
const PingReq = "ping"
const PingMsg = "Ping"
const UnblockReq = "unblock"
const UnblockMsg = "Unblock"
// VersionMsg
const VersionReq = "version"
type VersionMsg struct {
Version uint32
}
func (s *VersionMsg) RequestType() string {
return VersionReq
}
func (s *VersionMsg) Marshal() []byte {
return ssh.Marshal(*s)
}
func (s *VersionMsg) Unmarshal(payload []byte) error {
return ssh.Unmarshal(payload, s)
}
// SignalMsg
const SignalReq = "signal"
type SignalMsg struct {
Signal ssh.Signal
}
func (s *SignalMsg) RequestType() string {
return SignalReq
}
func (s *SignalMsg) Marshal() []byte {
return ssh.Marshal(*s)
}
func (s *SignalMsg) Unmarshal(payload []byte) error {
return ssh.Unmarshal(payload, s)
}
func (s *SignalMsg) Signum() int {
return Signals[s.Signal]
}
func (s *SignalMsg) FromString(name string) error {
num, err := strconv.Atoi(name)
if err == nil {
for sig, val := range Signals {
if num == val {
s.Signal = sig
return nil
}
}
}
name = strings.TrimPrefix(strings.ToUpper(name), "SIG")
s.Signal = ssh.Signal(name)
_, ok := Signals[s.Signal]
if !ok {
return fmt.Errorf("unsupported signal name: %q", name)
}
return nil
}
// CloseStdinMsg
const CloseStdinReq = "close-stdin"
// ContainersMsg
const ContainersReq = "container-ids"
type ContainersMsg struct {
IDs []string
}
func (s *ContainersMsg) RequestType() string {
return ContainersReq
}
func (s *ContainersMsg) Marshal() []byte {
return ssh.Marshal(*s)
}
func (s *ContainersMsg) Unmarshal(payload []byte) error {
return ssh.Unmarshal(payload, s)
}

View File

@@ -0,0 +1,85 @@
// 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 msgs
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/migration/feature"
)
func TestWindowChange(t *testing.T) {
s := &WindowChangeMsg{1, 2, 3, 4}
assert.Equal(t, s.RequestType(), WindowChangeReq)
tmp := s.Marshal()
out := &WindowChangeMsg{}
out.Unmarshal(tmp)
assert.Equal(t, s, out)
}
func TestSignal(t *testing.T) {
s := &SignalMsg{"HUP"}
assert.Equal(t, s.RequestType(), SignalReq)
tmp := s.Marshal()
out := &SignalMsg{}
out.Unmarshal(tmp)
assert.Equal(t, s, out)
for _, name := range []string{"SIGQUIT", "QUIT", "quit", "3"} {
err := out.FromString(name)
if err != nil {
t.Fatal(err)
}
}
for _, name := range []string{"SIGNOPE", "nope", "0", "-1"} {
err := out.FromString(name)
if err == nil {
t.Errorf("expected error parsing %q", name)
}
}
}
func TestContainers(t *testing.T) {
s := &ContainersMsg{IDs: []string{"foo", "bar", "baz"}}
assert.Equal(t, s.RequestType(), ContainersReq)
tmp := s.Marshal()
out := &ContainersMsg{}
out.Unmarshal(tmp)
assert.Equal(t, s, out)
}
func TestVersion(t *testing.T) {
s := &VersionMsg{Version: feature.MaxPluginVersion - 1}
assert.Equal(t, s.RequestType(), VersionReq)
tmp := s.Marshal()
out := &VersionMsg{}
out.Unmarshal(tmp)
assert.Equal(t, s, out)
}

View File

@@ -0,0 +1,245 @@
// 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.
// +build linux
package tether
import (
"fmt"
"io/ioutil"
"net"
"strconv"
"testing"
"github.com/vishvananda/netlink"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/etcconf"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
)
// Utility method to add an interface to Mocked
// This assigns the interface name and returns the "slot" as a string
func AddInterface(name string, mocker *Mocker) string {
mocker.maxSlot++
mocker.Interfaces[name] = &Interface{
LinkAttrs: netlink.LinkAttrs{
Name: name,
Index: mocker.maxSlot,
},
Up: true,
}
return strconv.Itoa(mocker.maxSlot)
}
func TestSetIpAddress(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
hFile, err := ioutil.TempFile("", "vic_set_ip_test_hosts")
if err != nil {
t.Errorf("Failed to create tmp hosts file: %s", err)
}
rFile, err := ioutil.TempFile("", "vic_set_ip_test_resolv")
if err != nil {
t.Errorf("Failed to create tmp resolv file: %s", err)
}
// give us a hosts file we can modify
defer func(hosts etcconf.Hosts, resolv etcconf.ResolvConf) {
Sys.Hosts = hosts
Sys.ResolvConf = resolv
}(Sys.Hosts, Sys.ResolvConf)
Sys.Hosts = etcconf.NewHosts(hFile.Name())
Sys.ResolvConf = etcconf.NewResolvConf(rFile.Name())
bridge := AddInterface("eth1", mocker)
public := AddInterface("eth2", mocker)
secondIP, _ := netlink.ParseIPNet("172.16.0.10/24")
gwIP, _ := netlink.ParseIPNet("172.16.0.1/24")
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "ipconfig",
Name: "tether_test_executor",
},
Networks: map[string]*executor.NetworkEndpoint{
"bridge": {
Common: executor.Common{
ID: bridge,
// interface rename
Name: "bridge",
},
Network: executor.ContainerNetwork{
Common: executor.Common{
Name: "bridge",
},
Default: true,
Gateway: *gwIP,
},
Static: true,
IP: &net.IPNet{
IP: localhost,
Mask: lmask.Mask,
},
},
"cnet": {
Common: executor.Common{
ID: bridge,
// no interface rename
},
Network: executor.ContainerNetwork{
Common: executor.Common{
Name: "cnet",
},
},
Static: true,
IP: secondIP,
},
"public": {
Common: executor.Common{
ID: public,
// interface rename
Name: "public",
},
Network: executor.ContainerNetwork{
Common: executor.Common{
Name: "public",
},
},
Static: true,
IP: &net.IPNet{
IP: gateway,
Mask: gmask.Mask,
},
},
},
}
tthr, _, _ := StartTether(t, &cfg, mocker)
defer func() {
// prevent indefinite wait in tether - normally session exit would trigger this
tthr.Stop()
// wait for tether to exit
<-mocker.Cleaned
}()
<-mocker.Started
assert.NotNil(t, mocker.Interfaces["bridge"], "Expected bridge network if endpoints applied correctly")
// check addresses
bIface, _ := mocker.Interfaces["bridge"].(*Interface)
assert.NotNil(t, bIface)
assert.Equal(t, 2, len(bIface.Addrs), "Expected two addresses on bridge interface")
eIface, _ := mocker.Interfaces["public"].(*Interface)
assert.NotNil(t, eIface)
assert.Equal(t, 1, len(eIface.Addrs), "Expected one address on public interface")
}
func TestOutboundRuleAndCmd(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
// give us a hosts file we can modify
defer func(hosts etcconf.Hosts, resolv etcconf.ResolvConf) {
Sys.Hosts = hosts
Sys.ResolvConf = resolv
}(Sys.Hosts, Sys.ResolvConf)
fmt.Printf("Test root dir: %s, hosts: %s\n", Sys.Root, Sys.Hosts.Path())
bridge := AddInterface("eth1", mocker)
gwIP, _ := netlink.ParseIPNet("172.16.0.1/24")
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "outboundrule",
Name: "tether_test_executor",
},
Diagnostics: executor.Diagnostics{
DebugLevel: 3,
},
Networks: map[string]*executor.NetworkEndpoint{
"bridge": {
Common: executor.Common{
ID: bridge,
// interface rename
Name: "bridge",
},
Network: executor.ContainerNetwork{
Common: executor.Common{
Name: "bridge",
},
Default: true,
Gateway: *gwIP,
},
Static: true,
IP: &net.IPNet{
IP: localhost,
Mask: lmask.Mask,
},
},
},
Sessions: map[string]*executor.SessionConfig{
"outboundrule": {
Common: executor.Common{
ID: "outboundrule",
Name: "tether_test_session",
},
Tty: false,
Active: true,
Cmd: executor.Cmd{
// test relative path
Path: "./date",
Args: []string{"./date", "--reference=/"},
Env: []string{"PATH="},
Dir: "/bin",
},
},
},
}
_, src, _ := StartTether(t, &cfg, mocker)
fmt.Println("Waiting for tether start")
<-mocker.Started
// wait for tether to exit
fmt.Println("Waiting for tether exit")
<-mocker.Cleaned
result := ExecutorConfig{}
extraconfig.Decode(src, &result)
// confirm command ran - necessary to detect early exit due to net config error
assert.Equal(t, "true", result.Sessions["outboundrule"].Started, "Expected command to have been started successfully")
assert.Equal(t, 0, result.Sessions["outboundrule"].ExitStatus, "Expected command to have exited cleanly")
// confirm outbound rules configured
}

72
vendor/github.com/vmware/vic/lib/tether/net_test.go generated vendored Normal file
View File

@@ -0,0 +1,72 @@
// 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 tether
import (
"testing"
"github.com/docker/docker/pkg/stringid"
"github.com/vmware/vic/lib/config/executor"
)
func TestSetHostname(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "sethostname",
Name: "tether_test_executor",
},
}
tthr, _, _ := StartTether(t, &cfg, mocker)
<-mocker.Started
// prevent indefinite wait in tether - normally session exit would trigger this
tthr.Stop()
// wait for tether to exit
<-mocker.Cleaned
expected := stringid.TruncateID(cfg.ID)
if mocker.Hostname != expected {
t.Errorf("expected: %s, actual: %s", expected, mocker.Hostname)
}
}
func TestNoNetwork(t *testing.T) {
_, mocker := testSetup(t)
defer testTeardown(t, mocker)
cfg := executor.ExecutorConfig{
ExecutorConfigCommon: executor.ExecutorConfigCommon{
ID: "ipconfig",
Name: "tether_test_executor",
},
}
tthr, _, _ := StartTether(t, &cfg, mocker)
<-mocker.Started
// prevent indefinite wait in tether - normally session exit would trigger this
tthr.Stop()
// wait for tether to exit
<-mocker.Cleaned
}

View File

@@ -0,0 +1,213 @@
// 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 netfilter
import (
"context"
"os"
"os/exec"
"strings"
"syscall"
"github.com/Sirupsen/logrus"
"github.com/vmware/vic/lib/tether"
)
//
// # default
// # iptables -A INPUT -m state --state ESTABLISHED -i eth0 -j ACCEPT
// # Expose()
// # iptables -A INPUT -p tcp --dport 7 -i eth0 -j ACCEPT
//
// # default on non-bridge
// # iptables -A INPUT -i eth0 -j REJECT
//
// # iptables --list
//
// Chain INPUT (policy ACCEPT)
// target prot opt source destination
// ACCEPT all -- anywhere anywhere state ESTABLISHED
// ACCEPT tcp -- anywhere anywhere tcp dpt:echo
// REJECT all -- anywhere anywhere reject-with icmp-port-unreachable
//
// Chain FORWARD (policy ACCEPT)
// target prot opt source destination
//
// Chain OUTPUT (policy ACCEPT)
// target prot opt source destination
//
type Chain string
type State string
type Protocol string
type Target string
type Table string
type ICMPType string
const (
Filter = Table("filter")
Nat = Table("nat")
Mangle = Table("mangle")
Raw = Table("raw")
Security = Table("security")
Prerouting = Chain("PREROUTING")
Input = Chain("INPUT")
Forward = Chain("FORWARD")
Output = Chain("OUTPUT")
Invalid = State("INVALID")
Established = State("ESTABLISHED")
New = State("NEW")
Related = State("RELATED")
Untracked = State("UNTRACKED")
TCP = Protocol("tcp")
UDP = Protocol("udp")
ICMP = Protocol("icmp")
Drop = Target("DROP")
Accept = Target("ACCEPT")
Queue = Target("QUEUE")
NoQueue = Target("NFQUEUE")
Reject = Target("REJECT")
Redirect = Target("REDIRECT")
EchoRequest = ICMPType("echo-request")
EchoReply = ICMPType("echo-reply")
)
type Rule struct {
Table
Chain
States []State
ICMPType
Protocol
Target
Interface string
SourceAddresses []string
FromPort string
SrcPort string
ToPort string
}
func (r *Rule) Commit(ctx context.Context) tether.UtilityFn {
return iptables(ctx, r.args())
}
func (r *Rule) args() []string {
var args []string
if r.Table != "" {
args = append(args, "-t", string(r.Table))
}
if r.Chain == Input || r.Chain == Output {
args = append(args, "-A", "VIC")
} else {
args = append(args, "-A", string(r.Chain))
}
if r.Protocol != "" {
args = append(args, "-p", string(r.Protocol))
}
if r.ICMPType != "" {
args = append(args, "--icmp-type", string(r.ICMPType))
}
if len(r.SourceAddresses) > 0 {
args = append(args, "-s", strings.Join(r.SourceAddresses, ","))
}
if r.FromPort != "" {
args = append(args, "--dport", r.FromPort)
}
if r.SrcPort != "" {
args = append(args, "--sport", r.SrcPort)
}
if len(r.States) > 0 {
args = append(args, "-m", "state", "--state", joinStates(r.States))
}
if r.Interface != "" {
if r.Chain == Output {
args = append(args, "-o", r.Interface)
} else {
args = append(args, "-i", r.Interface)
}
}
args = append(args, "-j", string(r.Target))
if r.ToPort != "" {
args = append(args, "--to-port", r.ToPort)
}
return args
}
func iptables(ctx context.Context, args []string) tether.UtilityFn {
logrus.Infof("Execing iptables %q", args)
// #nosec: Subprocess launching with variable
cmd := &exec.Cmd{
Path: "/lib64/ld-linux-x86-64.so.2",
Dir: "/",
Args: append([]string{"/lib64/ld-linux-x86-64.so.2", "/iptables"}, args...),
SysProcAttr: &syscall.SysProcAttr{
Chroot: "/.tether",
},
}
return func() (*os.Process, error) {
return os.StartProcess(cmd.Path, cmd.Args, &os.ProcAttr{
Dir: cmd.Dir,
Sys: cmd.SysProcAttr,
})
}
}
func Flush(ctx context.Context, chain string) tether.UtilityFn {
args := []string{"-F"}
if chain != "" {
args = append(args, chain)
}
return iptables(ctx, args)
}
func Return(ctx context.Context, chain string) tether.UtilityFn {
return iptables(ctx, []string{"-A", chain, "-j", "RETURN"})
}
func Policy(ctx context.Context, chain Chain, target Target) tether.UtilityFn {
return iptables(ctx, []string{"-P", string(chain), string(target)})
}
func joinStates(states []State) string {
tmp := make([]string, len(states))
for i, v := range states {
tmp[i] = string(v)
}
return strings.Join(tmp, ",")
}

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 netfilter
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
type input struct {
r *Rule
expected string
}
func TestArgs(t *testing.T) {
input := []input{
{
&Rule{
Chain: Input,
States: []State{Established},
Interface: "external",
Target: Accept,
},
"-A VIC -m state --state ESTABLISHED -i external -j ACCEPT",
},
{
&Rule{
Chain: Input,
Protocol: TCP,
FromPort: "7",
Interface: "external",
Target: Accept,
},
"-A VIC -p tcp --dport 7 -i external -j ACCEPT",
},
{
&Rule{
Chain: Input,
Interface: "external",
Target: Reject,
},
"-A VIC -i external -j REJECT",
},
{
&Rule{
Table: Nat,
Chain: Prerouting,
Interface: "external",
Protocol: TCP,
FromPort: "80",
Target: Redirect,
ToPort: "8080",
},
"-t nat -A PREROUTING -p tcp --dport 80 -i external -j REDIRECT --to-port 8080",
},
}
for _, rule := range input {
args := rule.r.args()
if !assert.Equal(t, strings.Join(args, " "), rule.expected) {
return
}
}
}

1271
vendor/github.com/vmware/vic/lib/tether/ops_linux.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,308 @@
// 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.
// +build linux
package tether
import (
"errors"
"fmt"
"os/user"
"path"
"strconv"
"strings"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vishvananda/netlink"
"os"
"io/ioutil"
"github.com/vmware/vic/pkg/trace"
)
type Interface struct {
netlink.LinkAttrs
Up bool
Addrs []netlink.Addr
}
func (t *Interface) Attrs() *netlink.LinkAttrs {
return &t.LinkAttrs
}
func (t *Interface) Type() string {
return "mocked"
}
func (t *Mocker) LinkByName(name string) (netlink.Link, error) {
defer trace.End(trace.Begin(fmt.Sprintf("Getting link by name %s", name)))
return t.Interfaces[name], nil
}
func (t *Mocker) LinkSetName(link netlink.Link, name string) error {
defer trace.End(trace.Begin(fmt.Sprintf("Renaming %s to %s", link.Attrs().Name, name)))
iface := link.(*Interface)
_, ok := t.Interfaces[name]
if ok {
return fmt.Errorf("Interface with name %s already exists", name)
}
oldName := iface.Name
iface.Name = name
// make sure there's no period where the interface isn't "present"
t.Interfaces[name] = iface
delete(t.Interfaces, oldName)
return nil
}
func (t *Mocker) LinkSetDown(link netlink.Link) error {
defer trace.End(trace.Begin(fmt.Sprintf("Bringing %s down", link.Attrs().Name)))
iface := link.(*Interface)
iface.Up = false
// TODO: should this drop addresses?
return nil
}
func (t *Mocker) LinkSetUp(link netlink.Link) error {
defer trace.End(trace.Begin(fmt.Sprintf("Bringing %s up", link.Attrs().Name)))
iface := link.(*Interface)
iface.Up = true
return nil
}
func (t *Mocker) LinkSetAlias(link netlink.Link, alias string) error {
defer trace.End(trace.Begin(fmt.Sprintf("Adding alias %s to %s", alias, link.Attrs().Name)))
iface := link.(*Interface)
iface.Alias = alias
return nil
}
func (t *Mocker) AddrList(link netlink.Link, family int) ([]netlink.Addr, error) {
defer trace.End(trace.Begin(""))
iface := link.(*Interface)
return iface.Addrs, nil
}
func (t *Mocker) AddrAdd(link netlink.Link, addr *netlink.Addr) error {
defer trace.End(trace.Begin(fmt.Sprintf("Adding %s to %s", addr.String(), link.Attrs().Name)))
iface := link.(*Interface)
for _, adr := range iface.Addrs {
if addr.IP.String() == adr.IP.String() {
return syscall.EEXIST
}
}
iface.Addrs = append(iface.Addrs, *addr)
return nil
}
func (t *Mocker) AddrDel(link netlink.Link, addr *netlink.Addr) error {
iface := link.(*Interface)
for i, adr := range iface.Addrs {
if addr.IP.String() == adr.IP.String() {
iface.Addrs = append(iface.Addrs[:i], iface.Addrs[i+1:]...)
return nil
}
}
return syscall.EADDRNOTAVAIL
}
func (t *Mocker) RouteAdd(route *netlink.Route) error {
defer trace.End(trace.Begin("no implemented"))
// currently ignored
return nil
}
func (t *Mocker) RouteDel(route *netlink.Route) error {
defer trace.End(trace.Begin("no implemented"))
// currently ignored
return nil
}
func (t *Mocker) RuleList(int) ([]netlink.Rule, error) {
defer trace.End(trace.Begin("not implemented"))
return nil, nil
}
func (t *Mocker) LinkBySlot(slot int32) (netlink.Link, error) {
defer trace.End(trace.Begin(""))
id := int(slot)
for _, intf := range t.Interfaces {
if intf.Attrs().Index == id {
return intf, nil
}
}
return nil, errors.New("no such interface")
}
func TestSlotToPciPath(t *testing.T) {
var tests = []struct {
slot int32
p string
err error
}{
{0, path.Join(pciDevPath, "0000:00:00.0"), nil},
{32, path.Join(pciDevPath, "0000:00:11.0", "0000:*:00.0"), nil},
{33, path.Join(pciDevPath, "0000:00:11.0", "0000:*:01.0"), nil},
{192, path.Join(pciDevPath, "0000:00:16.0", "0000:*:00.0"), nil},
{1184, path.Join(pciDevPath, "0000:00:15.1", "0000:*:00.0"), nil},
{1216, path.Join(pciDevPath, "0000:00:16.1", "0000:*:00.0"), nil},
{1248, path.Join(pciDevPath, "0000:00:17.1", "0000:*:00.0"), nil},
{1280, path.Join(pciDevPath, "0000:00:18.1", "0000:*:00.0"), nil},
}
for _, te := range tests {
p, err := slotToPCIPath(te.slot, 0)
if te.err != nil {
if err == nil {
t.Fatalf("slotToPCIPath(%d) => (%#v, %#v), want (%s, nil)", te.slot, p, err, te.p)
}
continue
}
if p != te.p {
t.Fatalf("slotToPCIPath(%d) => (%#v, %#v), want (%s, nil)", te.slot, p, err, te.p)
}
}
}
func TestGetUserSysProcAttr(t *testing.T) {
curr, err := user.Current()
if err != nil {
t.Logf("Failed to get current user, skip test")
return
}
currUID, _ := strconv.Atoi(curr.Uid)
currGID, _ := strconv.Atoi(curr.Gid)
moreThanMax := strconv.Itoa(1 << 33)
lessThanMin := "-100"
var tests = []struct {
uid string
gid string
ruid int
rgid int
err error
}{
{"", "", 0, 0, nil},
{"notexist", "notexist", 0, 0, errors.New("Unable to find user notexist")},
{"notexist", curr.Gid, 0, 0, errors.New("Unable to find user notexist")},
{"", "notexist", 0, 0, errors.New("Unable to find group notexist")},
{"0", "notexist", 0, 0, errors.New("Unable to find group notexist")},
{curr.Uid, "notexist", 0, 0, errors.New("Unable to find group notexist")},
{"2000000", "notexist", 0, 0, errors.New("Unable to find group notexist")},
{curr.Username, "notexist", 0, 0, errors.New("Unable to find group notexist")},
{curr.Username, "0", currUID, 0, nil},
{curr.Uid, "1000", currUID, 1000, nil},
{curr.Uid, curr.Gid, currUID, currGID, nil},
{"root", curr.Gid, 0, currGID, nil},
{"root", "root", 0, 0, nil},
{moreThanMax, "root", 0, 0, errors.New("Uids and gids must be in range 0-2147483647")},
{curr.Username, moreThanMax, 0, 0, errors.New("Uids and gids must be in range 0-2147483647")},
{lessThanMin, "root", 0, 0, errors.New("Uids and gids must be in range 0-2147483647")},
{curr.Username, lessThanMin, 0, 0, errors.New("Uids and gids must be in range 0-2147483647")},
}
for _, test := range tests {
t.Logf("uid: %s, gid: %s", test.uid, test.gid)
user, err := getUserSysProcAttr(test.uid, test.gid)
if err != nil {
assert.True(t, test.err != nil, fmt.Sprintf("Should not have error %s", err))
if !strings.Contains(err.Error(), test.err.Error()) {
assert.True(t, false, fmt.Sprintf("error message mismatch, expected %s, actual %s", test.err, err.Error()))
}
continue
}
assert.True(t, test.err == nil, fmt.Sprintf("didn't get expected error: %s", test.err))
if user == nil {
assert.True(t, (test.ruid == 0 && test.rgid == 0), "returned user is nil, but expect not nil result: %d:%d", test.ruid, test.rgid)
continue
}
assert.Equal(t, test.ruid, int(user.Credential.Uid), "returned user id mismatch")
assert.Equal(t, test.rgid, int(user.Credential.Gid), "returned group id mismatch")
}
}
func TestIsEmpty(t *testing.T) {
dir, err := ioutil.TempDir("", "testisEmpty")
assert.NoError(t, err)
defer os.RemoveAll(dir)
err = os.MkdirAll(dir+"/lost+found", 0644)
assert.NoError(t, err)
ok, err := isEmpty(dir)
assert.True(t, ok)
assert.NoError(t, err)
err = os.MkdirAll(dir+"/test1", 0644)
assert.NoError(t, err)
err = os.MkdirAll(dir+"/test2", 0644)
assert.NoError(t, err)
err = ioutil.WriteFile(dir+"/test3.txt", []byte{}, 0644)
assert.NoError(t, err)
ok, err = isEmpty(dir)
assert.False(t, ok)
assert.NoError(t, err)
}
func TestCreateBindSrcTarget(t *testing.T) {
dir, err := ioutil.TempDir("", "testCreateBindSrcTarget")
assert.NoError(t, err)
defer os.RemoveAll(dir)
// Create a file under an existing directory
file := dir + "/test1"
err = createBindSrcTarget(map[string]os.FileMode{file: 0644})
assert.NoError(t, err, "createBindSrcTarget failed: %s", err)
_, err = os.Stat(dir)
assert.NoError(t, err)
// Create a file under non-existent directory
file = dir + "/testDir/test1"
err = createBindSrcTarget(map[string]os.FileMode{file: 0644})
assert.NoError(t, err, "createBindSrcTarget failed: %s", err)
_, err = os.Stat(dir)
assert.NoError(t, err)
// Create an existing file
err = createBindSrcTarget(map[string]os.FileMode{file: 0644})
assert.NoError(t, err, "createBindSrcTarget failed: %s", err)
}

View File

@@ -0,0 +1,30 @@
// 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 shared is intended for isolating constants and minor functions required
// to be consistent across packages so as to avoid transitive package includes
package shared
/* Constants used by tether for exchange outside of tether */
const (
DiskLabelQueryName = "disk-label"
FilterSpecQueryName = "filter-spec"
SkipRecurseQueryName = "skip-recurse"
SkipDataQueryName = "skip-data"
// This string is referenced in isos/bootstrap/bootstrap and should be kept in sync.
// Using a 0011 prefix caused the disk not to present, so have taken the 6000 prefix
// that was being generated consistency when uuid left undefined.
ScratchUUID = "60002233445566778899aabbccddeeff"
)

View File

@@ -0,0 +1,19 @@
// 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 shared
const (
pidFilePath = ".tether/run"
)

View File

@@ -0,0 +1,27 @@
// 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 shared
import (
"path"
"github.com/vmware/vic/lib/system"
)
var Sys = system.New()
func PIDFileDir() string {
return path.Join(Sys.Root, pidFilePath)
}

58
vendor/github.com/vmware/vic/lib/tether/signer.go generated vendored Normal file
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 tether
import (
"io"
"golang.org/x/crypto/ssh"
)
type ContainerSigner struct {
id string
}
func (c *ContainerSigner) PublicKey() ssh.PublicKey {
return *c
}
// we're going to ignore everything for the moment as we're repurposing the host key for the id.
// later we may use a genuine host key and an SSH out-of-band request to get the container id.
func (c *ContainerSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
return &ssh.Signature{
Format: "container-id",
Blob: []byte{},
}, nil
}
func (c ContainerSigner) Type() string {
return "container-id"
}
func (c ContainerSigner) Marshal() []byte {
return []byte(c.id)
}
func (c ContainerSigner) Verify(data []byte, sig *ssh.Signature) error {
return nil
}
func NewSigner(id string) *ContainerSigner {
signer := &ContainerSigner{
id: id,
}
return signer
}

1104
vendor/github.com/vmware/vic/lib/tether/tether.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

403
vendor/github.com/vmware/vic/lib/tether/tether_linux.go generated vendored Normal file
View File

@@ -0,0 +1,403 @@
// 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 tether
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/signal"
"path/filepath"
"runtime/debug"
"strings"
"syscall"
log "github.com/Sirupsen/logrus"
"github.com/kr/pty"
"github.com/vmware/vic/pkg/trace"
)
const (
//https://github.com/golang/go/blob/master/src/syscall/zerrors_linux_arm64.go#L919
SetChildSubreaper = 0x24
// in sync with lib/apiservers/portlayer/handlers/interaction_handler.go
// 115200 bps is 14.4 KB/s so use that
ioCopyBufferSize = 14 * 1024
)
// Mkdev will hopefully get rolled into go.sys at some point
func Mkdev(majorNumber int, minorNumber int) int {
return (majorNumber << 8) | (minorNumber & 0xff) | ((minorNumber & 0xfff00) << 12)
}
// ReloadConfig signals the current process, which triggers the signal handler
// to reload the config.
func ReloadConfig() error {
defer trace.End(trace.Begin(""))
p, err := os.FindProcess(os.Getpid())
if err != nil {
return err
}
if err = p.Signal(syscall.SIGHUP); err != nil {
return err
}
return nil
}
// childReaper is used to handle events from child processes, including child exit.
// If running as pid=1 then this means it handles zombie process reaping for orphaned children
// as well as direct child processes.
func (t *tether) childReaper() error {
signal.Notify(t.incoming, syscall.SIGCHLD)
/*
PR_SET_CHILD_SUBREAPER (since Linux 3.4)
If arg2 is nonzero, set the "child subreaper" attribute of the
calling process; if arg2 is zero, unset the attribute. When a
process is marked as a child subreaper, all of the children
that it creates, and their descendants, will be marked as
having a subreaper. In effect, a subreaper fulfills the role
of init(1) for its descendant processes. Upon termination of
a process that is orphaned (i.e., its immediate parent has
already terminated) and marked as having a subreaper, the
nearest still living ancestor subreaper will receive a SIGCHLD
signal and be able to wait(2) on the process to discover its
termination status.
*/
if _, _, err := syscall.RawSyscall(syscall.SYS_PRCTL, SetChildSubreaper, uintptr(1), 0); err != 0 {
return err
}
log.Info("Started reaping child processes")
go func() {
var status syscall.WaitStatus
flag := syscall.WNOHANG | syscall.WUNTRACED | syscall.WCONTINUED
for range t.incoming {
func() {
// general resiliency
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "Recovered in childReaper: %s\n%s", r, debug.Stack())
}
}()
// reap until no more children to process
for {
log.Debugf("Inspecting children with status change")
select {
case <-t.ctx.Done():
log.Warnf("Someone called shutdown, returning from child reaper")
return
default:
}
pid, err := syscall.Wait4(-1, &status, flag, nil)
// pid 0 means no processes wish to report status
if pid == 0 || err == syscall.ECHILD {
log.Debug("No more child processes to reap")
break
}
if err != nil {
log.Warnf("Wait4 got error: %v\n", err)
break
}
if !status.Exited() && !status.Signaled() {
log.Debugf("Received notifcation about non-exit status change for %d: %d", pid, status)
// no reaping or exit handling required
continue
}
exitCode := status.ExitStatus()
log.Debugf("Reaped process %d, return code: %d", pid, exitCode)
session, ok := t.removeChildPid(pid)
if ok {
log.Debugf("Removed child pid: %d", pid)
session.Lock()
session.ExitStatus = exitCode
t.handleSessionExit(session)
session.Unlock()
continue
}
ok = t.ops.HandleUtilityExit(pid, exitCode)
if ok {
log.Debugf("Remove utility pid: %d", pid)
continue
}
log.Infof("Reaped zombie process PID %d", pid)
}
}()
}
log.Info("Stopped reaping child processes")
}()
return nil
}
func (t *tether) stopReaper() {
defer trace.End(trace.Begin("Shutting down child reaping"))
// Ordering is important otherwise we may one goroutine closing, and the other goroutine is trying to write afterwards
log.Debugf("Removing the signal notifier")
signal.Reset(syscall.SIGCHLD)
// just closing the incoming channel is not going to stop the iteration
// so we use the context cancellation to signal it
t.cancel()
log.Debugf("Closing the reapers signal channel")
close(t.incoming)
}
func (t *tether) triggerReaper() {
defer trace.End(trace.Begin("Triggering child reaping"))
t.incoming <- syscall.SIGCHLD
}
func findExecutable(file string, chroot string) error {
//log.Infof("***** Dumping directory %s", chroot)
//listDirectory(chroot)
//log.Infof("***** Dumping directory %s", path.Dir(file))
//listDirectory(path.Dir(file))
log.Infof("*** Stating file [%s], chroot [%s]", file, chroot)
d, err := os.Stat(file)
if err != nil {
log.Infof("*** Stating file [%s] failed with error - %s", file, err.Error())
if chroot != "" {
file = fmt.Sprintf("%s/%s", chroot, file)
//log.Infof("***** Dumping directory %s", path.Dir(file))
//listDirectory(path.Dir(file))
}
log.Infof("*** Stating file [%s], chroot [%s]", file, chroot)
d, err = os.Stat(file)
if err != nil {
log.Infof("*** Stating file [%s] failed with error - %s", file, err.Error())
return err
}
}
log.Infof("*** Stating file [%s], chroot [%s] succeeded", file, chroot)
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return os.ErrPermission
}
// listDirectory logs the directory structure
func listDirectory(path string) error {
log.Infof("*** Reading directory %s", path)
files, err := ioutil.ReadDir(path)
if err != nil {
log.Info("*** Reading directory FAILED")
return err
}
for _, file := range files {
if file.IsDir() {
log.Infof("*** %s [dir]", file.Name())
} else {
log.Infof("*** %s [file]", file.Name())
}
}
return nil
}
// lookPath searches for an executable binary named file in the directories
// specified by the path argument.
// This is a direct modification of the unix os/exec core library impl
func lookPath(file string, env []string, dir string, chroot string) (string, error) {
// if it starts with a ./ or ../ it's a relative path
// need to check explicitly to allow execution of .hidden files
if strings.HasPrefix(file, "./") || strings.HasPrefix(file, "../") {
file = fmt.Sprintf("%s%c%s", dir, os.PathSeparator, file)
err := findExecutable(file, chroot)
if err == nil {
return filepath.Clean(file), nil
}
return "", err
}
// check if it's already a path spec
if strings.Contains(file, "/") {
err := findExecutable(file, chroot)
if err == nil {
return filepath.Clean(file), nil
}
return "", err
}
// extract path from the env
var pathenv string
for _, value := range env {
if strings.HasPrefix(value, "PATH=") {
pathenv = value
break
}
}
pathval := strings.TrimPrefix(pathenv, "PATH=")
dirs := filepath.SplitList(pathval)
for _, dir := range dirs {
if dir == "" {
// Unix shell semantics: path element "" means "."
dir = "."
}
path := dir + "/" + file
if err := findExecutable(path, chroot); err == nil {
return filepath.Clean(path), nil
}
}
return "", fmt.Errorf("%s: no such executable in PATH", file)
}
func establishPty(session *SessionConfig) error {
defer trace.End(trace.Begin("initializing pty handling for session " + session.ID))
// pty.Start creates a process group anyway so no change needed to kill all descendants
var err error
session.Pty, err = pty.Start(&session.Cmd)
if err != nil {
return err
}
session.wait.Add(1)
go func() {
_, gerr := io.CopyBuffer(session.Outwriter, session.Pty, make([]byte, ioCopyBufferSize))
log.Debugf("PTY stdout copy: %s", gerr)
session.wait.Done()
}()
go func() {
_, gerr := io.CopyBuffer(session.Pty, session.Reader, make([]byte, ioCopyBufferSize))
log.Debugf("PTY stdin copy: %s", gerr)
// ensure that an EOT is delivered to the process - this makes the behaviour on EOF at this layer
// consistent between tty and non-tty cases
n, gerr := session.Pty.Write([]byte("\x04"))
if n != 1 || gerr != nil {
log.Errorf("Failed to write EOT to pty, closing directly: %s", gerr)
session.Pty.Close()
}
log.Debug("Written EOT to pty")
}()
return nil
}
func establishNonPty(session *SessionConfig) error {
defer trace.End(trace.Begin("initializing nonpty handling for session " + session.ID))
var err error
// configure a process group so we can kill any descendants
if session.Cmd.SysProcAttr == nil {
session.Cmd.SysProcAttr = &syscall.SysProcAttr{}
}
session.Cmd.SysProcAttr.Setsid = true
if session.OpenStdin {
log.Debugf("Setting StdinPipe")
if session.StdinPipe, err = session.Cmd.StdinPipe(); err != nil {
log.Errorf("StdinPipe failed with %s", err)
return err
}
}
log.Debugf("Setting StdoutPipe")
if session.StdoutPipe, err = session.Cmd.StdoutPipe(); err != nil {
log.Errorf("Setting StdoutPipe failed with %s", err)
return err
}
log.Debugf("Setting StderrPipe")
if session.StderrPipe, err = session.Cmd.StderrPipe(); err != nil {
log.Errorf("Setting StderrPipe failed with %s", err)
return err
}
if session.OpenStdin {
go func() {
_, gerr := io.CopyBuffer(session.StdinPipe, session.Reader, make([]byte, ioCopyBufferSize))
log.Debugf("Reader stdin returned: %s", gerr)
if gerr == nil {
if cerr := session.StdinPipe.Close(); cerr != nil {
log.Errorf("(stdin): Close StdinPipe failed with %s", cerr)
}
}
}()
}
// Add 2 for Std{out|err}
session.wait.Add(2)
go func() {
_, gerr := io.CopyBuffer(session.Outwriter, session.StdoutPipe, make([]byte, ioCopyBufferSize))
log.Debugf("Writer goroutine for stdout returned: %s", gerr)
if session.StdinPipe != nil {
log.Debugf("(stdout): Writing zero byte to stdin pipe")
n, werr := session.StdinPipe.Write([]byte{})
if n == 0 && werr != nil && werr.Error() == "write |1: bad file descriptor" {
log.Debugf("(stdout): Closing stdin pipe")
if cerr := session.StdinPipe.Close(); cerr != nil {
log.Errorf("Close failed with %s", cerr)
}
}
}
log.Debugf("Writer goroutine for stdout exiting")
session.wait.Done()
}()
go func() {
_, gerr := io.CopyBuffer(session.Errwriter, session.StderrPipe, make([]byte, ioCopyBufferSize))
log.Debugf("Writer goroutine for stderr returned: %s", gerr)
if session.StdinPipe != nil {
log.Debugf("(stderr): Writing zero byte to stdin pipe")
n, werr := session.StdinPipe.Write([]byte{})
if n == 0 && werr != nil && werr.Error() == "write |1: bad file descriptor" {
log.Debugf("(stderr): Closing stdin pipe")
if cerr := session.StdinPipe.Close(); cerr != nil {
log.Errorf("Close failed with %s", cerr)
}
}
}
log.Debugf("Writer goroutine for stderr exiting")
session.wait.Done()
}()
return session.Cmd.Start()
}

322
vendor/github.com/vmware/vic/lib/tether/tether_test.go generated vendored Normal file
View File

@@ -0,0 +1,322 @@
// 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 tether
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path"
"runtime"
"testing"
log "github.com/Sirupsen/logrus"
"github.com/vishvananda/netlink"
"golang.org/x/crypto/ssh"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/config/executor"
"github.com/vmware/vic/lib/etcconf"
"github.com/vmware/vic/lib/system"
"github.com/vmware/vic/pkg/dio"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
)
var Tthr Tether
type Mocker struct {
Base BaseOperations
// allow tests to tell when the tether has finished setup
Started chan bool
// allow tests to tell when the tether has finished
Cleaned chan bool
// allow tests to wait for a reload to complete
Reloaded chan bool
// debug output gets logged here
LogWriter io.Writer
// session output gets logged here
SessionLogBuffer bytes.Buffer
// the hostname of the system
Hostname string
Aliases []string
// the maximum slot number returned from this mocker
maxSlot int
// the interfaces in the system indexed by name
Interfaces map[string]netlink.Link
// filesystem mounts, indexed by disk label
Mounts map[string]string
WindowCol uint32
WindowRow uint32
Signal ssh.Signal
FirewallRules []string
}
// Start implements the extension method
func (t *Mocker) Start() error {
return nil
}
// Stop implements the extension method
func (t *Mocker) Stop() error {
close(t.Cleaned)
defer func() {
// tolerate closing started again
recover()
}()
close(t.Started)
close(t.Reloaded)
return nil
}
// Reload implements the extension method
func (t *Mocker) Reload(config *ExecutorConfig) error {
// the tether has definitely finished it's startup by the time we hit this
defer func() {
// if we trigger another Reload we don't want to panic
recover()
}()
t.Reloaded <- true
close(t.Started)
return nil
}
func (t *Mocker) Cleanup() error {
return nil
}
func (t *Mocker) Setup(c Config) error {
return t.Base.Setup(c)
}
func (t *Mocker) Log() (io.Writer, error) {
if t.LogWriter != nil {
return t.LogWriter, nil
}
return os.Stdout, nil
}
func (t *Mocker) SetupFirewall(ctx context.Context, conf *ExecutorConfig) error {
return nil
}
func (t *Mocker) SessionLog(session *SessionConfig) (dio.DynamicMultiWriter, dio.DynamicMultiWriter, error) {
return dio.MultiWriter(&t.SessionLogBuffer), dio.MultiWriter(&t.SessionLogBuffer), nil
}
func (t *Mocker) HandleSessionExit(config *ExecutorConfig, session *SessionConfig) func() {
// check for executor behaviour
return func() {
if session.ID == config.ID {
Tthr.Stop()
}
}
}
func (t *Mocker) ProcessEnv(env []string) []string {
return t.Base.ProcessEnv(env)
}
// SetHostname sets both the kernel hostname and /etc/hostname to the specified string
func (t *Mocker) SetHostname(hostname string, aliases ...string) error {
defer trace.End(trace.Begin("mocking hostname to " + hostname))
// TODO: we could mock at a much finer granularity, only extracting the syscall
// that would exercise the file modification paths, however it's much less generalizable
t.Hostname = hostname
t.Aliases = aliases
return nil
}
// Apply takes the network endpoint configuration and applies it to the system
func (t *Mocker) Apply(endpoint *NetworkEndpoint) error {
return ApplyEndpoint(t, &t.Base, endpoint)
}
// MountLabel performs a mount with the source treated as a disk label
// This assumes that /dev/disk/by-label is being populated, probably by udev
func (t *Mocker) MountLabel(ctx context.Context, label, target string) error {
defer trace.End(trace.Begin(fmt.Sprintf("mocking mounting %s on %s", label, target)))
if t.Mounts == nil {
t.Mounts = make(map[string]string)
}
t.Mounts[label] = target
return nil
}
// MountTarget performs a mount with the source treated as an nfs target
func (t *Mocker) MountTarget(ctx context.Context, source url.URL, target string, mountOptions string) error {
defer trace.End(trace.Begin(fmt.Sprintf("mocking mounting %s on %s", source.String(), target)))
if t.Mounts == nil {
t.Mounts = make(map[string]string)
}
t.Mounts[source.String()] = target
return nil
}
// CopyExistingContent copies the underlying files shadowed by a mount on a directory
// to the volume mounted on the directory
func (t *Mocker) CopyExistingContent(source string) error {
defer trace.End(trace.Begin(fmt.Sprintf("mocking copyExistingContent from %s", source)))
return nil
}
// Fork triggers vmfork and handles the necessary pre/post OS level operations
func (t *Mocker) Fork() error {
defer trace.End(trace.Begin("mocking fork"))
return errors.New("Fork test not implemented")
}
// LaunchUtility uses the underlying implementation for launching and tracking utility processes
func (t *Mocker) LaunchUtility(fn UtilityFn) (<-chan int, error) {
return launchUtility(&t.Base, fn)
}
func (t *Mocker) HandleUtilityExit(pid, exitCode int) bool {
return handleUtilityExit(&t.Base, pid, exitCode)
}
// TestMain simply so we have control of debugging level and somewhere to call package wide test setup
func TestMain(m *testing.M) {
log.SetLevel(log.DebugLevel)
trace.Logger = log.StandardLogger()
// restore what we inherited.
defer func(hosts etcconf.Hosts, resolv etcconf.ResolvConf) {
Sys.Hosts = hosts
Sys.ResolvConf = resolv
}(Sys.Hosts, Sys.ResolvConf)
// replace the Sys variable with a mock
r, err := ioutil.TempDir("", "tether-root")
if err != nil {
os.Exit(1)
log.Fatalf("Failed to create tmpdir for test root filesytem: %s", err)
}
Sys = system.NewWithRoot(r)
Sys.Syscall = &MockSyscall{}
BindSys = system.NewWithRoot(path.Join(Sys.Root, "/.tether"))
BindSys.Syscall = &MockSyscall{}
// ensure that the bindsys root exists
// #nosec
os.MkdirAll(BindSys.Root, 0744)
log.Debugf("Configured sys and bindsys to root in %s and %s", Sys.Root, BindSys.Root)
retCode := m.Run()
// call with result of m.Run()
os.Exit(retCode)
}
func StartTether(t *testing.T, cfg *executor.ExecutorConfig, mocker *Mocker) (Tether, extraconfig.DataSource, extraconfig.DataSink) {
store := extraconfig.New()
sink := store.Put
src := store.Get
extraconfig.Encode(sink, cfg)
log.Debugf("Test configuration: %#v", sink)
Tthr = New(src, sink, mocker)
Tthr.Register("mocker", mocker)
// run the tether to service the attach
go func() {
erR := Tthr.Start()
if erR != nil {
t.Error(erR)
}
}()
return Tthr, src, sink
}
func RunTether(t *testing.T, cfg *executor.ExecutorConfig, mocker *Mocker) (Tether, extraconfig.DataSource, error) {
store := extraconfig.New()
sink := store.Put
src := store.Get
extraconfig.Encode(sink, cfg)
log.Debugf("Test configuration: %#v", sink)
Tthr = New(src, sink, mocker)
Tthr.Register("Mocker", mocker)
// run the tether to service the attach
erR := Tthr.Start()
return Tthr, src, erR
}
func OptionValueArrayToString(options []types.BaseOptionValue) string {
// create the key/value store from the extraconfig slice for lookups
kv := make(map[string]string)
for i := range options {
k := options[i].GetOptionValue().Key
v := options[i].GetOptionValue().Value.(string)
kv[k] = v
}
return fmt.Sprintf("%#v", kv)
}
func testSetup(t *testing.T) (string, *Mocker) {
pc, _, _, _ := runtime.Caller(1)
name := runtime.FuncForPC(pc).Name()
log.Infof("Started test setup for %s", name)
// use the mock ops - fresh one each time as tests might apply different mocked calls
mocker := &Mocker{
Started: make(chan bool, 0),
Cleaned: make(chan bool, 0),
Reloaded: make(chan bool, 100),
Interfaces: make(map[string]netlink.Link, 0),
}
return name, mocker
}
func testTeardown(t *testing.T, mocker *Mocker) {
// cleanup
// os.RemoveAll(pathPrefix)
log.SetOutput(os.Stdout)
<-mocker.Cleaned
pc, _, _, _ := runtime.Caller(1)
name := runtime.FuncForPC(pc).Name()
log.Infof("Finished test teardown for %s", name)
}

422
vendor/github.com/vmware/vic/lib/tether/toolbox.go generated vendored Normal file
View File

@@ -0,0 +1,422 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !windows,!darwin
package tether
import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"strconv"
"sync"
"syscall"
"time"
log "github.com/Sirupsen/logrus"
dar "github.com/docker/docker/pkg/archive"
"golang.org/x/crypto/ssh"
"github.com/vmware/govmomi/toolbox"
"github.com/vmware/govmomi/toolbox/hgfs"
"github.com/vmware/govmomi/toolbox/vix"
"github.com/vmware/vic/lib/archive"
"github.com/vmware/vic/lib/tether/msgs"
"github.com/vmware/vic/lib/tether/shared"
"github.com/vmware/vic/pkg/trace"
)
// Toolbox is a tether extension that wraps toolbox.Service
type Toolbox struct {
*toolbox.Service
sess struct {
sync.Mutex
session *SessionConfig
}
// IDs that can be used to authenticate
authIDs map[string]struct{}
stop chan struct{}
}
var (
defaultArchiveHandler = hgfs.NewArchiveHandler().(*hgfs.ArchiveHandler)
baseOp = &BaseOperations{}
)
// NewToolbox returns a tether.Extension that wraps the vsphere/toolbox service
func NewToolbox() *Toolbox {
in := toolbox.NewBackdoorChannelIn()
out := toolbox.NewBackdoorChannelOut()
service := toolbox.NewService(in, out)
service.PrimaryIP = toolbox.DefaultIP
return &Toolbox{
Service: service,
authIDs: make(map[string]struct{}),
}
}
// Start implementation of the tether.Extension interface
func (t *Toolbox) Start() error {
t.stop = make(chan struct{})
on := make(chan struct{})
t.Service.Power.PowerOn.Handler = func() error {
log.Info("toolbox: service is ready (power on event received)")
close(on)
return nil
}
err := t.Service.Start()
if err != nil {
return err
}
// Wait for the vmx to send the OS_PowerOn message,
// at which point it will be ready to service vix command requests.
log.Info("toolbox: waiting for initialization")
select {
case <-on:
case <-time.After(time.Second):
log.Warn("toolbox: timeout waiting for power on event")
}
return nil
}
// Stop implementation of the tether.Extension interface
func (t *Toolbox) Stop() error {
t.Service.Stop()
t.Service.Wait()
close(t.stop)
return nil
}
// Reload implementation of the tether.Extension interface
func (t *Toolbox) Reload(config *ExecutorConfig) error {
if config != nil && config.Sessions != nil {
t.sess.Lock()
defer t.sess.Unlock()
t.sess.session = config.Sessions[config.ID]
}
// we allow the primary session
t.authIDs[config.ID] = struct{}{}
// we also allow any device IDs that are attached
for _, mspec := range config.Mounts {
// mounstpect.source.path is the disk label for vmdks
// TODO: this is not the case for other volumes, eg nfs vols.
if mspec.Source.Scheme == "label" {
t.authIDs[mspec.Source.Path] = struct{}{}
}
}
return nil
}
// InContainer configures the toolbox to run within a container VM
func (t *Toolbox) InContainer() *Toolbox {
t.Power.Halt.Handler = t.halt
cmd := t.Service.Command
cmd.Authenticate = t.containerAuthenticate
cmd.ProcessStartCommand = t.containerStartCommand
cmd.FileServer.RegisterFileHandler(hgfs.ArchiveScheme, &hgfs.ArchiveHandler{
Read: toolboxOverrideArchiveRead,
Write: toolboxOverrideArchiveWrite,
})
return t
}
func (t *Toolbox) session() *SessionConfig {
t.sess.Lock()
defer t.sess.Unlock()
return t.sess.session
}
func (t *Toolbox) kill(_ context.Context, name string) error {
session := t.session()
if session == nil {
return fmt.Errorf("failed to kill container: process not found")
}
session.Lock()
defer session.Unlock()
return t.killHelper(session, name)
}
func (t *Toolbox) killHelper(session *SessionConfig, name string) error {
if name == "" {
name = string(ssh.SIGTERM)
}
if session.Cmd.Process == nil {
return fmt.Errorf("the session %s hasn't launched yet", session.ID)
}
sig := new(msgs.SignalMsg)
err := sig.FromString(name)
if err != nil {
return err
}
num := syscall.Signal(sig.Signum())
log.Infof("sending signal %s (%d) to process group for %s", sig.Signal, num, session.ID)
if err := syscall.Kill(-session.Cmd.Process.Pid, num); err != nil {
return fmt.Errorf("failed to signal %s group: %s", session.ID, err)
}
return nil
}
func (t *Toolbox) containerAuthenticate(_ vix.CommandRequestHeader, data []byte) error {
var c vix.UserCredentialNamePassword
if err := c.UnmarshalBinary(data); err != nil {
return err
}
session := t.session()
if session == nil {
return errors.New("not yet initialized")
}
session.Lock()
defer session.Unlock()
// no authentication yet, just using container ID and device IDs as a sanity check for now
if _, ok := t.authIDs[c.Name]; !ok {
return errors.New("failed to verify authentication ID")
}
return nil
}
func (t *Toolbox) containerStartCommand(m *toolbox.ProcessManager, r *vix.StartProgramRequest) (int64, error) {
var p *toolbox.Process
switch r.ProgramPath {
case "kill":
p = toolbox.NewProcessFunc(t.kill)
case "reload":
p = toolbox.NewProcessFunc(func(_ context.Context, _ string) error {
return ReloadConfig()
})
default:
return -1, fmt.Errorf("unknown command %q", r.ProgramPath)
}
return m.Start(r, p)
}
func (t *Toolbox) halt() error {
session := t.session()
if session == nil {
return fmt.Errorf("failed to halt container: not initialized yet")
}
session.Lock()
defer session.Unlock()
if session.Cmd.Process == nil {
return fmt.Errorf("the session %s hasn't launched yet", session.ID)
}
log.Infof("stopping %s", session.ID)
if err := t.killHelper(session, session.StopSignal); err != nil {
return err
}
// Killing the executor session in the container VM will stop the tether and its extensions.
// If that doesn't happen within the timeout, send a SIGKILL.
select {
case <-t.stop:
log.Infof("%s has stopped", session.ID)
return nil
case <-time.After(time.Second * 10):
}
log.Warnf("killing %s", session.ID)
return session.Cmd.Process.Kill()
}
// toolboxOverrideArchiveRead is the online DataSink Override Handler
func toolboxOverrideArchiveRead(u *url.URL, tr *tar.Reader) error {
// special behavior when using disk-labels and filterspec
diskLabel := u.Query().Get(shared.DiskLabelQueryName)
filterSpec := u.Query().Get(shared.FilterSpecQueryName)
if diskLabel != "" && filterSpec != "" {
op := trace.NewOperation(context.Background(), "ToolboxOnlineDataSink: %s", u.String())
op.Debugf("Reading from tar archive to path %s: %s", u.Path, u.String())
spec, err := archive.DecodeFilterSpec(op, &filterSpec)
if err != nil {
op.Errorf(err.Error())
return err
}
diskPath, err := mountDiskLabel(op, diskLabel)
if err != nil {
op.Errorf(err.Error())
return err
}
defer unmount(op, diskPath)
// no need to join on u.Path here. u.Path == spec.Rebase, but
// Unpack will rebase tar headers for us. :thumbsup:
err = archive.InvokeUnpack(op, tr, spec, diskPath)
if err != nil {
op.Errorf(err.Error())
}
op.Debugf("Finished reading from tar archive to path %s: %s", u.Path, u.String())
return err
}
return defaultArchiveHandler.Read(u, tr)
}
// toolboxOverrideArchiveWrite is the Online DataSource Override Handler
func toolboxOverrideArchiveWrite(u *url.URL, tw *tar.Writer) error {
// special behavior when using disk-labels and filterspec
diskLabel := u.Query().Get(shared.DiskLabelQueryName)
filterSpec := u.Query().Get(shared.FilterSpecQueryName)
skiprecurse, _ := strconv.ParseBool(u.Query().Get(shared.SkipRecurseQueryName))
skipdata, _ := strconv.ParseBool(u.Query().Get(shared.SkipDataQueryName))
if diskLabel != "" && filterSpec != "" {
op := trace.NewOperation(context.Background(), "ToolboxOnlineDataSource: %s", u.String())
op.Debugf("Writing to archive from %s: %s", u.Path, u.String())
spec, err := archive.DecodeFilterSpec(op, &filterSpec)
if err != nil {
op.Errorf(err.Error())
return err
}
// get the container fs mount
diskPath, err := mountDiskLabel(op, diskLabel)
if err != nil {
op.Errorf(err.Error())
return err
}
defer unmount(op, diskPath)
var rc io.ReadCloser
if skiprecurse {
// we only want a single file - this is a hack while we're abusing Diff, but
// accomplish this by generating a single entry ChangeSet
changes := []dar.Change{
{
Kind: dar.ChangeModify,
Path: u.Path,
},
}
rc, err = archive.Tar(op, diskPath, changes, spec, !skipdata, false)
} else {
rc, err = archive.Diff(op, diskPath, "", spec, !skipdata, false)
}
if err != nil {
op.Errorf(err.Error())
return err
}
tr := tar.NewReader(rc)
defer rc.Close()
for {
hdr, err := tr.Next()
if err == io.EOF {
op.Debugf("Finished writing to archive from %s: %s with error %#v", u.Path, u.String(), err)
break
}
if err != nil {
op.Errorf("error writing tar: %s", err.Error())
return err
}
op.Debugf("Writing header: %#s", *hdr)
err = tw.WriteHeader(hdr)
if err != nil {
op.Errorf("error writing tar header: %s", err.Error())
return err
}
_, err = io.Copy(tw, tr)
if err != nil {
op.Errorf("error writing tar contents: %s", err.Error())
return err
}
}
return nil
}
return defaultArchiveHandler.Write(u, tw)
}
func mountDiskLabel(op trace.Operation, label string) (string, error) {
// We know the vmdk will always be attached at '/'
if label == "containerfs" {
return "/", nil
}
// otherwise, label represents a volume that needs to be mounted
tmpDir, err := ioutil.TempDir("", fmt.Sprintf("toolbox-%s", label))
if err != nil {
op.Errorf("failed to create mountpoint %s: %s", tmpDir, err)
return "", fmt.Errorf("failed to create mountpoint %s: %s", tmpDir, err)
}
err = baseOp.MountLabel(op, label, tmpDir)
if err != nil {
os.Remove(tmpDir)
op.Errorf("failed to mount label %s at %s: %s", label, tmpDir, err)
return "", fmt.Errorf("failed to mount label %s at %s: %s", label, tmpDir, err)
}
return tmpDir, nil
}
func unmount(op trace.Operation, unmountPath string) {
// don't unmount the root vmdk
if unmountPath == "/" {
return
}
// unmount the disk from the temporary directory
if err := Sys.Syscall.Unmount(unmountPath, syscall.MNT_DETACH); err != nil {
op.Errorf("failed to unmount %s: %s", unmountPath, err.Error())
}
// finally, remove the temporary directory
os.Remove(unmountPath)
}