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:
723
vendor/github.com/vmware/vic/cmd/tether/attach.go
generated
vendored
Normal file
723
vendor/github.com/vmware/vic/cmd/tether/attach.go
generated
vendored
Normal file
@@ -0,0 +1,723 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/vmware/vic/lib/migration/feature"
|
||||
"github.com/vmware/vic/lib/tether"
|
||||
"github.com/vmware/vic/lib/tether/msgs"
|
||||
"github.com/vmware/vic/pkg/serial"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
attachChannelType = "attach"
|
||||
)
|
||||
|
||||
// server is the singleton attachServer for the tether - there can be only one
|
||||
// as the backchannel line protocol may not provide multiplexing of connections
|
||||
var server AttachServer
|
||||
var once sync.Once
|
||||
|
||||
type AttachServer interface {
|
||||
tether.Extension
|
||||
|
||||
start() error
|
||||
stop() error
|
||||
}
|
||||
|
||||
// config is a struct that holds Sessions and Execs
|
||||
type config struct {
|
||||
Key []byte
|
||||
|
||||
Sessions map[string]*tether.SessionConfig
|
||||
Execs map[string]*tether.SessionConfig
|
||||
}
|
||||
|
||||
type attachServerSSH struct {
|
||||
// serializes data access for exported functions
|
||||
m sync.Mutex
|
||||
|
||||
// conn is the underlying net.Conn which carries SSH
|
||||
// held directly as it is how we stop the attach server
|
||||
conn struct {
|
||||
sync.Mutex
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// we pass serverConn to the channelMux goroutine so we need to lock it
|
||||
serverConn struct {
|
||||
sync.Mutex
|
||||
*ssh.ServerConn
|
||||
}
|
||||
|
||||
// extension local copy of the bits of config important to attach
|
||||
config config
|
||||
sshConfig *ssh.ServerConfig
|
||||
|
||||
enabled int32
|
||||
|
||||
// Cancelable context and its cancel func. Used for resolving the deadlock
|
||||
// between run() and stop()
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// INTERNAL: must set by testAttachServer only
|
||||
testing bool
|
||||
}
|
||||
|
||||
// NewAttachServerSSH either creates a new instance or returns the initialized one
|
||||
func NewAttachServerSSH() AttachServer {
|
||||
once.Do(func() {
|
||||
// create a cancelable context and assign it to the CancelFunc
|
||||
// it isused for resolving the deadlock between run() and stop()
|
||||
// it has a Background parent as we don't want timeouts here,
|
||||
// otherwise we may start leaking goroutines in the handshake code
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
server = &attachServerSSH{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
})
|
||||
return server
|
||||
}
|
||||
|
||||
// Reload - tether.Extension implementation
|
||||
func (t *attachServerSSH) Reload(tconfig *tether.ExecutorConfig) error {
|
||||
defer trace.End(trace.Begin("attach reload"))
|
||||
|
||||
t.m.Lock()
|
||||
defer t.m.Unlock()
|
||||
|
||||
// We copy this stuff so that we're not referencing the direct config
|
||||
// structure if/while it's being updated.
|
||||
// The subelements generally have locks or updated in single assignment
|
||||
t.config.Key = tconfig.Key
|
||||
t.config.Sessions = make(map[string]*tether.SessionConfig)
|
||||
for k, v := range tconfig.Sessions {
|
||||
t.config.Sessions[k] = v
|
||||
}
|
||||
|
||||
t.config.Execs = make(map[string]*tether.SessionConfig)
|
||||
for k, v := range tconfig.Execs {
|
||||
t.config.Execs[k] = v
|
||||
}
|
||||
|
||||
err := server.start()
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("unable to start attach server: %s", err)
|
||||
log.Error(detail)
|
||||
return errors.New(detail)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enable sets the enabled to true
|
||||
func (t *attachServerSSH) Enable() {
|
||||
atomic.StoreInt32(&t.enabled, 1)
|
||||
}
|
||||
|
||||
// Disable sets the enabled to false
|
||||
func (t *attachServerSSH) Disable() {
|
||||
atomic.StoreInt32(&t.enabled, 0)
|
||||
}
|
||||
|
||||
// Enabled returns whether the enabled is true
|
||||
func (t *attachServerSSH) Enabled() bool {
|
||||
return atomic.LoadInt32(&t.enabled) == 1
|
||||
}
|
||||
|
||||
func (t *attachServerSSH) Start() error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop needed for tether.Extensions interface
|
||||
func (t *attachServerSSH) Stop() error {
|
||||
defer trace.End(trace.Begin("stop attach server"))
|
||||
|
||||
t.m.Lock()
|
||||
defer t.m.Unlock()
|
||||
|
||||
// calling server.start not t.start so that test impl gets invoked
|
||||
return server.stop()
|
||||
}
|
||||
|
||||
func (t *attachServerSSH) reload() error {
|
||||
t.serverConn.Lock()
|
||||
defer t.serverConn.Unlock()
|
||||
|
||||
// push the exec'ed session ids to the portlayer
|
||||
if t.serverConn.ServerConn != nil {
|
||||
msg := msgs.ContainersMsg{
|
||||
IDs: t.sessions(false),
|
||||
}
|
||||
payload := msg.Marshal()
|
||||
|
||||
ok, _, err := t.serverConn.SendRequest(msgs.ContainersReq, true, payload)
|
||||
if !ok || err != nil {
|
||||
return fmt.Errorf("failed to send container ids: %s, %t", err, ok)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *attachServerSSH) start() error {
|
||||
defer trace.End(trace.Begin("start attach server"))
|
||||
|
||||
// if we come here while enabled, reload
|
||||
if t.Enabled() {
|
||||
log.Debugf("Start called while enabled, reloading")
|
||||
if err := t.reload(); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// don't assume that the key hasn't changed
|
||||
pkey, err := ssh.ParsePrivateKey([]byte(t.config.Key))
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("failed to load key for attach: %s", err)
|
||||
log.Error(detail)
|
||||
return errors.New(detail)
|
||||
}
|
||||
|
||||
// An SSH server is represented by a ServerConfig, which holds
|
||||
// certificate details and handles authentication of ServerConns.
|
||||
// TODO: update this with generated credentials for the appliance
|
||||
t.sshConfig = &ssh.ServerConfig{
|
||||
PublicKeyCallback: func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
if c.User() == "daemon" {
|
||||
return &ssh.Permissions{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected daemon user")
|
||||
},
|
||||
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||
if c.User() == "daemon" {
|
||||
return &ssh.Permissions{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected daemon user")
|
||||
},
|
||||
NoClientAuth: true,
|
||||
}
|
||||
t.sshConfig.AddHostKey(pkey)
|
||||
|
||||
// enable the server and start it
|
||||
t.Enable()
|
||||
go t.run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// stop is not thread safe with start
|
||||
func (t *attachServerSSH) stop() error {
|
||||
defer trace.End(trace.Begin("stop attach server"))
|
||||
|
||||
if t == nil {
|
||||
err := fmt.Errorf("attach server is not configured")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !t.Enabled() {
|
||||
err := fmt.Errorf("attach server is not enabled")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// disable the server
|
||||
t.Disable()
|
||||
|
||||
// This context is used by backchannel only. We need to cancel it before
|
||||
// trying to obtain the following lock so that backchannel interrupts the
|
||||
// underlying Read call by calling Close on it.
|
||||
// The lock is held by backchannel's caller and not released until it returns
|
||||
log.Debugf("Canceling AttachServer's context")
|
||||
t.cancel()
|
||||
|
||||
t.conn.Lock()
|
||||
if t.conn.conn != nil {
|
||||
log.Debugf("Close called again on rawconn - squashing")
|
||||
// #nosec: Errors unhandled.
|
||||
t.conn.conn.Close()
|
||||
t.conn.conn = nil
|
||||
}
|
||||
t.conn.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func backchannel(ctx context.Context, conn net.Conn) error {
|
||||
defer trace.End(trace.Begin("establish tether backchannel"))
|
||||
|
||||
// used for shutting down the goroutine cleanly otherwise we leak a goroutine for every successful return from this function
|
||||
done := make(chan struct{})
|
||||
|
||||
// HACK: currently RawConn dosn't implement timeout so throttle the spinning
|
||||
// it does implement the Timeout methods so the intermediary code can be written
|
||||
// to support it, but they are stub implementation in rawconn impl.
|
||||
|
||||
// This needs to tick *faster* than the ticker in connection.go on the
|
||||
// portlayer side. The PL sends the first syn and if this isn't waiting,
|
||||
// alignment will take a few rounds (or it may never happen).
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
// We run this in a separate goroutine because HandshakeServer
|
||||
// calls a Read on rawconn which is a blocking call which causes
|
||||
// the caller to block as well so this is the only way to cancel.
|
||||
// Calling Close() will unblock us and on the next tick we will
|
||||
// return ctx.Err()
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
conn.Close()
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
deadline, ok := ctx.Deadline()
|
||||
if ok {
|
||||
conn.SetReadDeadline(deadline)
|
||||
}
|
||||
|
||||
err := serial.HandshakeServer(conn)
|
||||
if err == nil {
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
close(done)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch et := err.(type) {
|
||||
case *serial.HandshakeError:
|
||||
log.Debugf("HandshakeServer: %v", et)
|
||||
default:
|
||||
log.Errorf("HandshakeServer: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *attachServerSSH) establish() error {
|
||||
var err error
|
||||
|
||||
// we hold the t.conn.Lock during the scope of this function
|
||||
t.conn.Lock()
|
||||
defer t.conn.Unlock()
|
||||
|
||||
// tests are passing their own connections so do not create connections when testing is set
|
||||
if !t.testing {
|
||||
// close the connection if required
|
||||
if t.conn.conn != nil {
|
||||
// #nosec: Errors unhandled.
|
||||
t.conn.conn.Close()
|
||||
t.conn.conn = nil
|
||||
}
|
||||
t.conn.conn, err = rawConnectionFromSerial()
|
||||
if err != nil {
|
||||
detail := fmt.Errorf("failed to create raw connection: %s", err)
|
||||
log.Error(detail)
|
||||
return detail
|
||||
}
|
||||
} else {
|
||||
// A series of unfortunate events can lead calling backchannel with nil when we run unit tests.
|
||||
// https://github.com/vmware/vic/pull/5327#issuecomment-305619860
|
||||
// This check is here to handle that
|
||||
if t.conn.conn == nil {
|
||||
return fmt.Errorf("nil connection")
|
||||
}
|
||||
}
|
||||
|
||||
// wait for backchannel to establish
|
||||
err = backchannel(t.ctx, t.conn.conn)
|
||||
if err != nil {
|
||||
detail := fmt.Errorf("failed to establish backchannel: %s", err)
|
||||
log.Error(detail)
|
||||
return detail
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *attachServerSSH) cleanup() {
|
||||
t.serverConn.Lock()
|
||||
defer t.serverConn.Unlock()
|
||||
|
||||
log.Debugf("cleanup on connection")
|
||||
|
||||
if t.serverConn.ServerConn != nil {
|
||||
log.Debugf("closing underlying connection")
|
||||
t.serverConn.Close()
|
||||
t.serverConn.ServerConn = nil
|
||||
}
|
||||
}
|
||||
|
||||
// run should not be called directly, but via start
|
||||
// run will establish an ssh server listening on the backchannel
|
||||
func (t *attachServerSSH) run() error {
|
||||
defer trace.End(trace.Begin("main attach server loop"))
|
||||
|
||||
var established bool
|
||||
|
||||
var chans <-chan ssh.NewChannel
|
||||
var reqs <-chan *ssh.Request
|
||||
var err error
|
||||
|
||||
// main run loop
|
||||
for t.Enabled() {
|
||||
t.serverConn.Lock()
|
||||
established = t.serverConn.ServerConn != nil
|
||||
t.serverConn.Unlock()
|
||||
|
||||
// keep waiting for the connection to establish
|
||||
for !established && t.Enabled() {
|
||||
log.Infof("Trying to establish a connection")
|
||||
|
||||
if err := t.establish(); err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
// create the SSH server using underlying t.conn
|
||||
t.serverConn.Lock()
|
||||
|
||||
t.serverConn.ServerConn, chans, reqs, err = ssh.NewServerConn(t.conn.conn, t.sshConfig)
|
||||
if err != nil {
|
||||
detail := fmt.Errorf("failed to establish ssh handshake: %s", err)
|
||||
log.Error(detail)
|
||||
}
|
||||
established = t.serverConn.ServerConn != nil
|
||||
|
||||
t.serverConn.Unlock()
|
||||
}
|
||||
|
||||
// Global requests
|
||||
go t.globalMux(reqs, t.cleanup)
|
||||
|
||||
log.Infof("Ready to service attach requests")
|
||||
// Service the incoming channels
|
||||
for attachchan := range chans {
|
||||
// The only channel type we'll support is attach
|
||||
if attachchan.ChannelType() != attachChannelType {
|
||||
detail := fmt.Sprintf("unknown channel type %s", attachchan.ChannelType())
|
||||
attachchan.Reject(ssh.UnknownChannelType, detail)
|
||||
log.Error(detail)
|
||||
continue
|
||||
}
|
||||
|
||||
// check we have a Session matching the requested ID
|
||||
bytes := attachchan.ExtraData()
|
||||
if bytes == nil {
|
||||
detail := "attach channel requires ID in ExtraData"
|
||||
attachchan.Reject(ssh.Prohibited, detail)
|
||||
log.Error(detail)
|
||||
continue
|
||||
}
|
||||
|
||||
sessionid := string(bytes)
|
||||
|
||||
s, oks := t.config.Sessions[sessionid]
|
||||
e, oke := t.config.Execs[sessionid]
|
||||
if !oks && !oke {
|
||||
detail := fmt.Sprintf("session %s is invalid", sessionid)
|
||||
attachchan.Reject(ssh.Prohibited, detail)
|
||||
log.Error(detail)
|
||||
continue
|
||||
}
|
||||
|
||||
// we have sessionid
|
||||
session := s
|
||||
if oke {
|
||||
session = e
|
||||
}
|
||||
|
||||
// session is potentially blocked in launch until we've got the unblock message, so we cannot lock it.
|
||||
// check that session is valid
|
||||
// The detail remains concise as it'll eventually make its way to the user
|
||||
if session.Started != "" && session.Started != "true" {
|
||||
detail := fmt.Sprintf("launch failed with: %s", session.Started)
|
||||
attachchan.Reject(ssh.Prohibited, detail)
|
||||
log.Error(detail)
|
||||
continue
|
||||
}
|
||||
|
||||
if session.StopTime != 0 {
|
||||
detail := fmt.Sprintf("process finished with exit code: %d", session.ExitStatus)
|
||||
attachchan.Reject(ssh.Prohibited, detail)
|
||||
log.Error(detail)
|
||||
continue
|
||||
}
|
||||
|
||||
channel, requests, err := attachchan.Accept()
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("could not accept channel: %s", err)
|
||||
log.Errorf(detail)
|
||||
continue
|
||||
}
|
||||
|
||||
// bind the channel to the Session
|
||||
log.Debugf("binding reader/writers for channel for %s", sessionid)
|
||||
|
||||
log.Debugf("Adding [%p] to Outwriter", channel)
|
||||
session.Outwriter.Add(channel)
|
||||
log.Debugf("Adding [%p] to Reader", channel)
|
||||
session.Reader.Add(channel)
|
||||
|
||||
// cleanup on detach from the session
|
||||
cleanup := func() {
|
||||
log.Debugf("Cleanup on detach from the session")
|
||||
|
||||
log.Debugf("Removing [%p] from Outwriter", channel)
|
||||
session.Outwriter.Remove(channel)
|
||||
|
||||
log.Debugf("Removing [%p] from Reader", channel)
|
||||
session.Reader.Remove(channel)
|
||||
|
||||
channel.Close()
|
||||
}
|
||||
|
||||
detach := cleanup
|
||||
// tty's merge stdout and stderr so we don't bind an additional reader in that case but we need to do so for non-tty
|
||||
if !session.Tty {
|
||||
// persist the value as we end up with different values each time we access it
|
||||
stderr := channel.Stderr()
|
||||
|
||||
log.Debugf("Adding [%p] to Errwriter", stderr)
|
||||
session.Errwriter.Add(stderr)
|
||||
|
||||
detach = func() {
|
||||
log.Debugf("Cleanup on detach from the session (non-tty)")
|
||||
|
||||
log.Debugf("Removing [%p] from Errwriter", stderr)
|
||||
session.Errwriter.Remove(stderr)
|
||||
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
log.Debugf("reader/writers bound for channel for %s", sessionid)
|
||||
|
||||
go t.channelMux(requests, session, detach)
|
||||
}
|
||||
log.Info("Incoming attach channel closed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *attachServerSSH) sessions(all bool) []string {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
var keys []string
|
||||
|
||||
// this iterates the local copies of the sessions maps
|
||||
// so we don't need to care whether they're initialized or not
|
||||
// as extension reload comes after that point
|
||||
|
||||
// whether include sessions or not
|
||||
if all {
|
||||
for k, v := range t.config.Sessions {
|
||||
if v.Active && v.StopTime == 0 {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range t.config.Execs {
|
||||
// skip those that have had launch errors
|
||||
if v.Active && v.StopTime == 0 && (v.Started == "" || v.Started == "true") {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Returning %d keys", len(keys))
|
||||
return keys
|
||||
}
|
||||
|
||||
func (t *attachServerSSH) globalMux(in <-chan *ssh.Request, cleanup func()) {
|
||||
defer trace.End(trace.Begin("attach server global request handler"))
|
||||
|
||||
// cleanup function passed by the caller
|
||||
defer cleanup()
|
||||
|
||||
// for the actions after we process the request
|
||||
var pendingFn func()
|
||||
for req := range in {
|
||||
var payload []byte
|
||||
ok := true
|
||||
|
||||
log.Infof("received global request type %v", req.Type)
|
||||
|
||||
switch req.Type {
|
||||
case msgs.ContainersReq:
|
||||
msg := msgs.ContainersMsg{
|
||||
IDs: t.sessions(true),
|
||||
}
|
||||
payload = msg.Marshal()
|
||||
case msgs.VersionReq:
|
||||
msg := msgs.VersionMsg{
|
||||
Version: feature.MaxPluginVersion - 1,
|
||||
}
|
||||
payload = msg.Marshal()
|
||||
default:
|
||||
ok = false
|
||||
payload = []byte("unknown global request type: " + req.Type)
|
||||
}
|
||||
|
||||
log.Debugf("Returning payload: %s", string(payload))
|
||||
|
||||
// make sure that errors get send back if we failed
|
||||
if req.WantReply {
|
||||
log.Debugf("Sending global request reply %t back with %#v", ok, payload)
|
||||
if err := req.Reply(ok, payload); err != nil {
|
||||
log.Warnf("Failed to reply a global request back")
|
||||
}
|
||||
}
|
||||
|
||||
// run any pending work now that a reply has been sent
|
||||
if pendingFn != nil {
|
||||
log.Debug("Invoking pending work for global mux")
|
||||
go pendingFn()
|
||||
pendingFn = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *attachServerSSH) channelMux(in <-chan *ssh.Request, session *tether.SessionConfig, cleanup func()) {
|
||||
defer trace.End(trace.Begin("attach server channel request handler"))
|
||||
|
||||
// cleanup function passed by the caller
|
||||
defer cleanup()
|
||||
|
||||
// to make sure we close the channel once
|
||||
var once sync.Once
|
||||
|
||||
// for the actions after we process the request
|
||||
var pendingFn func()
|
||||
for req := range in {
|
||||
ok := true
|
||||
abort := false
|
||||
|
||||
log.Infof("received channel mux type %v", req.Type)
|
||||
|
||||
switch req.Type {
|
||||
case msgs.PingReq:
|
||||
log.Infof("Received PingReq for %s", session.ID)
|
||||
|
||||
if string(req.Payload) != msgs.PingMsg {
|
||||
log.Infof("Received corrupted PingReq for %s", session.ID)
|
||||
ok = false
|
||||
}
|
||||
case msgs.UnblockReq:
|
||||
log.Infof("Received UnblockReq for %s", session.ID)
|
||||
|
||||
if string(req.Payload) != msgs.UnblockMsg {
|
||||
log.Infof("Received corrupted UnblockReq for %s", session.ID)
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
|
||||
// if the process has exited, or couldn't launch
|
||||
if session.Started != "" && session.Started != "true" {
|
||||
// we need to force the session closed so that error handling occurs on the callers
|
||||
// side
|
||||
ok = false
|
||||
abort = true
|
||||
} else {
|
||||
// unblock ^ (above)
|
||||
pendingFn = func() {
|
||||
once.Do(func() {
|
||||
launchChan := session.ClearToLaunch
|
||||
if session.RunBlock && launchChan != nil && session.Started == "" {
|
||||
log.Infof("Unblocking the launch of %s", session.Common.ID)
|
||||
// make sure that portlayer received the container id back
|
||||
launchChan <- struct{}{}
|
||||
log.Infof("Unblocked the launch of %s", session.Common.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
case msgs.WindowChangeReq:
|
||||
session.Lock()
|
||||
pty := session.Pty
|
||||
session.Unlock()
|
||||
|
||||
msg := msgs.WindowChangeMsg{}
|
||||
if pty == nil {
|
||||
ok = false
|
||||
log.Errorf("illegal window-change request for non-tty")
|
||||
} else if err := msg.Unmarshal(req.Payload); err != nil {
|
||||
ok = false
|
||||
log.Errorf(err.Error())
|
||||
} else if err := resizePty(pty.Fd(), &msg); err != nil {
|
||||
ok = false
|
||||
log.Errorf(err.Error())
|
||||
}
|
||||
case msgs.CloseStdinReq:
|
||||
log.Infof("Received CloseStdinReq for %s", session.ID)
|
||||
|
||||
log.Debugf("Configuring reader to propagate EOF for %s", session.ID)
|
||||
session.Reader.PropagateEOF(true)
|
||||
default:
|
||||
ok = false
|
||||
log.Error(fmt.Sprintf("ssh request type %s is not supported", req.Type))
|
||||
}
|
||||
|
||||
// payload is ignored on channel specific replies. The ok is passed, however.
|
||||
if req.WantReply {
|
||||
log.Debugf("Sending channel request reply %t back", ok)
|
||||
if err := req.Reply(ok, nil); err != nil {
|
||||
log.Warnf("Failed replying to a channel request: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// run any pending work now that a reply has been sent
|
||||
if pendingFn != nil {
|
||||
log.Debug("Invoking pending work for channel mux")
|
||||
go pendingFn()
|
||||
pendingFn = nil
|
||||
}
|
||||
|
||||
if abort {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The syscall struct
|
||||
type winsize struct {
|
||||
wsRow uint16
|
||||
wsCol uint16
|
||||
wsXpixel uint16
|
||||
wsYpixel uint16
|
||||
}
|
||||
121
vendor/github.com/vmware/vic/cmd/tether/attach_linux.go
generated
vendored
Normal file
121
vendor/github.com/vmware/vic/cmd/tether/attach_linux.go
generated
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/vmware/vic/lib/tether/msgs"
|
||||
"github.com/vmware/vic/pkg/serial"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
var backchannelMode = os.ModePerm
|
||||
|
||||
func setTerminalSpeed(fd uintptr) error {
|
||||
var current struct {
|
||||
termios syscall.Termios
|
||||
}
|
||||
|
||||
// get the current state
|
||||
// #nosec: Use of unsafe calls should be audited
|
||||
if _, _, errno := syscall.Syscall6(syscall.SYS_IOCTL,
|
||||
uintptr(fd),
|
||||
syscall.TCGETS,
|
||||
uintptr(unsafe.Pointer(¤t.termios)),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
|
||||
// copy it as the future
|
||||
future := current.termios
|
||||
|
||||
// unset 9600 bps
|
||||
future.Cflag &^= syscall.B9600
|
||||
// set them to 115200 bps
|
||||
future.Cflag |= syscall.B115200
|
||||
future.Ispeed = syscall.B115200
|
||||
future.Ospeed = syscall.B115200
|
||||
|
||||
// set the future values
|
||||
// #nosec: Use of unsafe calls should be audited
|
||||
if _, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_IOCTL,
|
||||
uintptr(fd),
|
||||
syscall.TCSETS,
|
||||
uintptr(unsafe.Pointer(&future)),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rawConnectionFromSerial() (net.Conn, error) {
|
||||
log.Info("opening ttyS0 for backchannel")
|
||||
f, err := os.OpenFile(path.Join(pathPrefix, "ttyS0"), os.O_RDWR|os.O_SYNC|syscall.O_NOCTTY, backchannelMode)
|
||||
if err != nil {
|
||||
detail := fmt.Errorf("failed to open serial port for backchannel: %s", err)
|
||||
log.Error(detail)
|
||||
return nil, detail
|
||||
}
|
||||
|
||||
// set the provided FDs to raw if it's a termial
|
||||
// 0 is the uninitialized value for Fd
|
||||
if f.Fd() != 0 && terminal.IsTerminal(int(f.Fd())) {
|
||||
log.Info("setting terminal to raw mode")
|
||||
_, err := terminal.MakeRaw(int(f.Fd()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := setTerminalSpeed(f.Fd()); err != nil {
|
||||
log.Errorf("Setting terminal speed failed with %s", err)
|
||||
}
|
||||
|
||||
log.Infof("creating raw connection from ttyS0 (fd=%d)", f.Fd())
|
||||
return serial.NewFileConn(f)
|
||||
}
|
||||
|
||||
func resizePty(pty uintptr, winSize *msgs.WindowChangeMsg) error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
ws := &winsize{uint16(winSize.Rows), uint16(winSize.Columns), uint16(winSize.WidthPx), uint16(winSize.HeightPx)}
|
||||
// #nosec: Use of unsafe calls should be audited
|
||||
_, _, errno := syscall.Syscall(
|
||||
syscall.SYS_IOCTL,
|
||||
pty,
|
||||
syscall.TIOCSWINSZ,
|
||||
uintptr(unsafe.Pointer(ws)),
|
||||
)
|
||||
if errno != 0 {
|
||||
return syscall.Errno(errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
1096
vendor/github.com/vmware/vic/cmd/tether/attach_test.go
generated
vendored
Normal file
1096
vendor/github.com/vmware/vic/cmd/tether/attach_test.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
74
vendor/github.com/vmware/vic/cmd/tether/haveged_linux.go
generated
vendored
Normal file
74
vendor/github.com/vmware/vic/cmd/tether/haveged_linux.go
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/vmware/vic/lib/tether"
|
||||
)
|
||||
|
||||
// Haveged is a tether extension that wraps command
|
||||
type Haveged struct {
|
||||
p *os.Process
|
||||
exec func() (*os.Process, error)
|
||||
}
|
||||
|
||||
// NewHaveged returns a tether.Extension that wraps haveged
|
||||
func NewHaveged() *Haveged {
|
||||
return &Haveged{
|
||||
exec: func() (*os.Process, error) {
|
||||
args := []string{"/.tether/lib/ld-linux-x86-64.so.2", "--library-path", "/.tether/lib", "/.tether/haveged", "-w", "1024", "-v", "1", "-F"}
|
||||
// #nosec: Subprocess launching with variable
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
|
||||
log.Infof("Starting haveged with args: %q", args)
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Errorf("Starting haveged failed with %q", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return cmd.Process, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Start implementation of the tether.Extension interface
|
||||
func (h *Haveged) Start() error {
|
||||
log.Infof("Starting haveged")
|
||||
|
||||
var err error
|
||||
h.p, err = h.exec()
|
||||
return err
|
||||
}
|
||||
|
||||
// Stop implementation of the tether.Extension interface
|
||||
func (h *Haveged) Stop() error {
|
||||
log.Infof("Stopping haveged")
|
||||
|
||||
if h.p != nil {
|
||||
return h.p.Kill()
|
||||
}
|
||||
return fmt.Errorf("haveged process is missing")
|
||||
}
|
||||
|
||||
// Reload implementation of the tether.Extension interface
|
||||
func (h *Haveged) Reload(config *tether.ExecutorConfig) error {
|
||||
// haveged doesn't support reloading
|
||||
return nil
|
||||
}
|
||||
139
vendor/github.com/vmware/vic/cmd/tether/main_linux.go
generated
vendored
Normal file
139
vendor/github.com/vmware/vic/cmd/tether/main_linux.go
generated
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/vmware/vic/lib/tether"
|
||||
viclog "github.com/vmware/vic/pkg/log"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/extraconfig"
|
||||
)
|
||||
|
||||
var tthr tether.Tether
|
||||
|
||||
func init() {
|
||||
// Initiliaze logger with default TextFormatter
|
||||
log.SetFormatter(viclog.NewTextFormatter())
|
||||
|
||||
// use the same logger for trace and other logging
|
||||
trace.Logger.Level = log.DebugLevel
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
// init and start the HUP handler
|
||||
startSignalHandler()
|
||||
|
||||
pathPrefix = "/dev"
|
||||
}
|
||||
|
||||
func main() {
|
||||
if strings.HasSuffix(os.Args[0], "-debug") {
|
||||
// very, very verbose
|
||||
extraconfig.SetLogLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("run time panic: %s : %s", r, debug.Stack())
|
||||
}
|
||||
halt()
|
||||
}()
|
||||
|
||||
logFile, err := os.OpenFile(path.Join(pathPrefix, "ttyS1"), os.O_WRONLY|os.O_SYNC, 0)
|
||||
if err != nil {
|
||||
log.Errorf("Could not open serial port for debugging info. Some debug info may be lost! Error reported was %s", err)
|
||||
}
|
||||
|
||||
if err = syscall.Dup3(int(logFile.Fd()), int(os.Stderr.Fd()), 0); err != nil {
|
||||
log.Errorf("Could not pipe logfile to standard error due to error %s", err)
|
||||
}
|
||||
|
||||
if _, err = os.Stderr.WriteString("all stderr redirected to debug log"); err != nil {
|
||||
log.Errorf("Could not write to Stderr due to error %s", err)
|
||||
}
|
||||
|
||||
// where to look for the various devices and files related to tether
|
||||
|
||||
// TODO: hard code executor initialization status reporting via guestinfo here
|
||||
sshserver := NewAttachServerSSH()
|
||||
src, err := extraconfig.GuestInfoSource()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
sink, err := extraconfig.GuestInfoSink()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// create the tether
|
||||
tthr = tether.New(src, sink, &operations{})
|
||||
|
||||
// register the attach extension
|
||||
tthr.Register("Attach", sshserver)
|
||||
|
||||
// register the toolbox extension
|
||||
tthr.Register("Toolbox", tether.NewToolbox().InContainer())
|
||||
|
||||
// register the executor extension
|
||||
tthr.Register("Haveged", NewHaveged())
|
||||
|
||||
err = tthr.Start()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Clean exit from tether")
|
||||
}
|
||||
|
||||
// exit cleanly shuts down the system
|
||||
func halt() {
|
||||
log.Infof("Powering off the system")
|
||||
if strings.HasSuffix(os.Args[0], "-debug") {
|
||||
log.Info("Squashing power off for debug tether")
|
||||
return
|
||||
}
|
||||
|
||||
syscall.Sync()
|
||||
syscall.Reboot(syscall.LINUX_REBOOT_CMD_POWER_OFF)
|
||||
}
|
||||
|
||||
func startSignalHandler() {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGHUP)
|
||||
|
||||
go func() {
|
||||
for s := range sigs {
|
||||
switch s {
|
||||
case syscall.SIGHUP:
|
||||
log.Infof("Reloading tether configuration")
|
||||
tthr.Reload()
|
||||
default:
|
||||
log.Infof("%s signal not defined", s.String())
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
160
vendor/github.com/vmware/vic/cmd/tether/main_test.go
generated
vendored
Normal file
160
vendor/github.com/vmware/vic/cmd/tether/main_test.go
generated
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/vic/lib/tether"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
func init() {
|
||||
trace.Logger.Level = log.DebugLevel
|
||||
}
|
||||
|
||||
// createFakeDevices creates regular files or pipes in place of the char devices used
|
||||
// in a full VM
|
||||
func createFakeDevices() error {
|
||||
var err error
|
||||
|
||||
// create control channel
|
||||
path := fmt.Sprintf("%s/ttyS0", pathPrefix)
|
||||
err = syscall.Mkfifo(path+"s", uint32(backchannelMode))
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("failed to create fifo pipe %ss for com0: %s", path, err)
|
||||
return errors.New(detail)
|
||||
}
|
||||
err = syscall.Mkfifo(path+"c", uint32(backchannelMode))
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("failed to create fifo pipe %sc for com0: %s", path, err)
|
||||
return errors.New(detail)
|
||||
}
|
||||
log.Debugf("created %s/ttyS0{c,s} as raw conn pipes", pathPrefix)
|
||||
|
||||
// others are non-interactive
|
||||
for i := 1; i < 3; i++ {
|
||||
path = fmt.Sprintf("%s/ttyS%d", pathPrefix, i)
|
||||
_, err = os.Create(path)
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("failed to create %s for com%d: %s", path, i+1, err)
|
||||
return errors.New(detail)
|
||||
}
|
||||
log.Debugf("created %s as persistent log destinations", path)
|
||||
}
|
||||
|
||||
// make an access to urandom
|
||||
path = fmt.Sprintf("%s/urandom", pathPrefix)
|
||||
err = os.Symlink("/dev/urandom", path)
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("failed to create urandom access: %s", err)
|
||||
return errors.New(detail)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func testSetup(t *testing.T) *Mocker {
|
||||
var err error
|
||||
|
||||
pathPrefix, err = ioutil.TempDir("", path.Base(t.Name()))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
mocker := tetherTestSetup(t)
|
||||
|
||||
err = os.MkdirAll(pathPrefix, 0777)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
t.Error(err)
|
||||
}
|
||||
log.Infof("Using %s as test prefix", pathPrefix)
|
||||
|
||||
backchannelMode = os.ModeNamedPipe | os.ModePerm
|
||||
err = createFakeDevices()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// supply custom attach server so we can inspect its state
|
||||
testServer := &testAttachServer{
|
||||
updated: make(chan bool, 10),
|
||||
attachServerSSH: attachServerSSH{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
},
|
||||
}
|
||||
server = testServer
|
||||
|
||||
return mocker
|
||||
}
|
||||
|
||||
func testTeardown(t *testing.T, mocker *Mocker) {
|
||||
tetherTestTeardown(t, mocker)
|
||||
}
|
||||
|
||||
type mockery struct {
|
||||
reloaded chan bool
|
||||
}
|
||||
|
||||
func (m *mockery) Reload() {
|
||||
m.reloaded <- true
|
||||
}
|
||||
|
||||
func (m *mockery) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockery) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockery) Register(name string, config tether.Extension) {
|
||||
}
|
||||
|
||||
func (m *mockery) LaunchUtility(fn tether.UtilityFn) (<-chan int, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Test reloading via signal helper
|
||||
func TestReload(t *testing.T) {
|
||||
m := &mockery{make(chan bool)}
|
||||
tthr = m
|
||||
|
||||
startSignalHandler()
|
||||
|
||||
if !assert.NoError(t, tether.ReloadConfig()) {
|
||||
return
|
||||
}
|
||||
|
||||
// check the started channel is closed (which gets closed on reconfig)
|
||||
if !assert.True(t, <-m.reloaded) {
|
||||
return
|
||||
}
|
||||
}
|
||||
127
vendor/github.com/vmware/vic/cmd/tether/net_linux_test.go
generated
vendored
Normal file
127
vendor/github.com/vmware/vic/cmd/tether/net_linux_test.go
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
// 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.
|
||||
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/vmware/vic/lib/config/executor"
|
||||
"github.com/vmware/vic/lib/tether"
|
||||
"github.com/vmware/vic/pkg/vsphere/extraconfig"
|
||||
)
|
||||
|
||||
// addInterface 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 TestOutboundRuleAndCmd(t *testing.T) {
|
||||
t.Skip("https://github.com/vmware/vic/issues/5965")
|
||||
|
||||
mocker := testSetup(t)
|
||||
defer testTeardown(t, mocker)
|
||||
|
||||
bridge := addInterface("eth1", mocker)
|
||||
|
||||
ip, _ := netlink.ParseIPNet("172.16.0.2/24")
|
||||
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: ip,
|
||||
},
|
||||
},
|
||||
|
||||
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 := tether.ExecutorConfig{}
|
||||
extraconfig.Decode(src, &result)
|
||||
|
||||
// confirm outbound rules configured
|
||||
// this should modify state depending on prior rule state
|
||||
|
||||
// confirm command ran - necessary to detect early exit due to net config error
|
||||
// TODO: this should be modifed to fail if the last rule to be configured hasn't completed with expected output
|
||||
// when this is run. Pending mocked iptables interface
|
||||
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")
|
||||
|
||||
}
|
||||
52
vendor/github.com/vmware/vic/cmd/tether/ops.go
generated
vendored
Normal file
52
vendor/github.com/vmware/vic/cmd/tether/ops.go
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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 main
|
||||
|
||||
import "github.com/vmware/vic/lib/tether"
|
||||
|
||||
// pathPrefix is present to allow the various files referenced by tether to be placed
|
||||
// in specific directories, primarily for testing.
|
||||
var pathPrefix string
|
||||
|
||||
func (t *operations) Cleanup() error {
|
||||
return t.BaseOperations.Cleanup()
|
||||
}
|
||||
|
||||
func (t *operations) Apply(endpoint *tether.NetworkEndpoint) error {
|
||||
return t.BaseOperations.Apply(endpoint)
|
||||
}
|
||||
|
||||
// HandleSessionExit controls the behaviour on session exit - for the tether if the session exiting
|
||||
// is the primary session (i.e. SessionID matches ExecutorID) then we exit everything.
|
||||
func (t *operations) HandleSessionExit(config *tether.ExecutorConfig, session *tether.SessionConfig) func() {
|
||||
// if the session that's exiting is the primary session, stop the tether
|
||||
return func() {
|
||||
pod := false
|
||||
for _, s := range config.Sessions {
|
||||
if s.ExecutionEnvironment != "" {
|
||||
pod = true
|
||||
}
|
||||
if s.StopTime <= s.StartTime {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, s := range config.Execs {
|
||||
if pod && s.StopTime <= s.StartTime {
|
||||
return
|
||||
}
|
||||
}
|
||||
tthr.Stop()
|
||||
}
|
||||
}
|
||||
490
vendor/github.com/vmware/vic/cmd/tether/ops_linux.go
generated
vendored
Normal file
490
vendor/github.com/vmware/vic/cmd/tether/ops_linux.go
generated
vendored
Normal file
@@ -0,0 +1,490 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/vmware/vic/lib/config/executor"
|
||||
"github.com/vmware/vic/lib/constants"
|
||||
"github.com/vmware/vic/lib/iolog"
|
||||
"github.com/vmware/vic/lib/tether"
|
||||
"github.com/vmware/vic/lib/tether/netfilter"
|
||||
"github.com/vmware/vic/pkg/dio"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
runMountPoint = "/run"
|
||||
|
||||
// default values to set for ulimit fields
|
||||
defaultNOFILE = 1024 * 1024
|
||||
defaultULimit = math.MaxUint64
|
||||
)
|
||||
|
||||
type operations struct {
|
||||
tether.BaseOperations
|
||||
|
||||
logging bool
|
||||
}
|
||||
|
||||
func (t *operations) Log() (io.Writer, error) {
|
||||
defer trace.End(trace.Begin("operations.Log"))
|
||||
|
||||
// redirect logging to the serial log
|
||||
log.Infof("opening %s/ttyS1 for debug log", pathPrefix)
|
||||
f, err := os.OpenFile(path.Join(pathPrefix, "ttyS1"), os.O_RDWR|os.O_SYNC|syscall.O_NOCTTY, 0)
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("failed to open serial port for debug log: %s", err)
|
||||
log.Error(detail)
|
||||
return nil, errors.New(detail)
|
||||
}
|
||||
|
||||
if err := setTerminalSpeed(f.Fd()); err != nil {
|
||||
log.Errorf("Setting terminal speed failed with %s", err)
|
||||
}
|
||||
|
||||
// enable raw mode
|
||||
_, err = terminal.MakeRaw(int(f.Fd()))
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("Making ttyS1 raw failed with %s", err)
|
||||
log.Error(detail)
|
||||
return nil, errors.New(detail)
|
||||
}
|
||||
|
||||
return io.MultiWriter(f, os.Stdout), nil
|
||||
}
|
||||
|
||||
// sessionLogWriter returns a writer that will persist the session output
|
||||
func (t *operations) SessionLog(session *tether.SessionConfig) (dio.DynamicMultiWriter, dio.DynamicMultiWriter, error) {
|
||||
defer trace.End(trace.Begin("configure session log writer"))
|
||||
|
||||
if t.logging {
|
||||
detail := "unable to log more than one session concurrently to persistent logging"
|
||||
log.Warn(detail)
|
||||
// use multi-writer so it's still viable for attach
|
||||
return dio.MultiWriter(), dio.MultiWriter(), nil
|
||||
}
|
||||
|
||||
t.logging = true
|
||||
|
||||
// open SttyS2 for session logging
|
||||
log.Info("opening ttyS2 for session logging")
|
||||
f, err := os.OpenFile(path.Join(pathPrefix, "ttyS2"), os.O_RDWR|os.O_SYNC|syscall.O_NOCTTY, 0)
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("failed to open serial port for session log: %s", err)
|
||||
log.Error(detail)
|
||||
return nil, nil, errors.New(detail)
|
||||
}
|
||||
|
||||
if err := setTerminalSpeed(f.Fd()); err != nil {
|
||||
log.Errorf("Setting terminal speed failed with %s", err)
|
||||
}
|
||||
|
||||
// enable raw mode
|
||||
_, err = terminal.MakeRaw(int(f.Fd()))
|
||||
if err != nil {
|
||||
detail := fmt.Sprintf("Making ttyS2 raw failed with %s", err)
|
||||
log.Error(detail)
|
||||
return nil, nil, errors.New(detail)
|
||||
}
|
||||
|
||||
// wrap output in a LogWriter to serialize it into our persisted
|
||||
// containerVM output format, using iolog.LogClock for timestamps
|
||||
lw := iolog.NewLogWriter(f, iolog.LogClock{})
|
||||
|
||||
// use multi-writer so it goes to both screen and session log
|
||||
return dio.MultiWriter(lw, os.Stdout), dio.MultiWriter(lw, os.Stderr), nil
|
||||
}
|
||||
|
||||
func (t *operations) Setup(sink tether.Config) error {
|
||||
if err := t.BaseOperations.Setup(sink); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// unmount /run - https://github.com/vmware/vic/issues/1643
|
||||
if err := tether.Sys.Syscall.Unmount(runMountPoint, syscall.MNT_DETACH); err != nil {
|
||||
if errno, ok := err.(syscall.Errno); !ok || errno != syscall.EINVAL {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: ulimit default values should change when we support ulimit configuration
|
||||
ApplyDefaultULimit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyDefaultULimit sets ulimit fields to their defined default value
|
||||
func ApplyDefaultULimit() {
|
||||
var rLimit syscall.Rlimit
|
||||
|
||||
// NOFILE does not support defaultULimit as a value due to kernel restriction on number of open files
|
||||
rLimit.Max = defaultNOFILE
|
||||
rLimit.Cur = rLimit.Max
|
||||
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||
log.Errorf("Cannot set ulimit for nofile: %s", err.Error())
|
||||
}
|
||||
|
||||
rLimit.Max = defaultULimit
|
||||
rLimit.Cur = rLimit.Max
|
||||
if err := syscall.Setrlimit(syscall.RLIMIT_STACK, &rLimit); err != nil {
|
||||
log.Errorf("Cannot set ulimit for stack: %s ", err.Error())
|
||||
}
|
||||
|
||||
if err := syscall.Setrlimit(syscall.RLIMIT_CORE, &rLimit); err != nil {
|
||||
log.Errorf("Cannot set ulimit for core blocks: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := syscall.Setrlimit(unix.RLIMIT_MEMLOCK, &rLimit); err != nil {
|
||||
log.Errorf("Cannot set ulimit for memlock: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := syscall.Setrlimit(unix.RLIMIT_NPROC, &rLimit); err != nil {
|
||||
log.Errorf("Cannot set ulimit for nproc: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// invoke will invoke the closure returned from the tether netfilter prep and
|
||||
// block until complete. It handles both potential preparation errors and invocation
|
||||
// errors.
|
||||
// the 'task' specified is used to construct error messages with the specific operation
|
||||
// embedded
|
||||
func invoke(t *tether.BaseOperations, fn tether.UtilityFn, task string) error {
|
||||
exitChan, err := t.LaunchUtility(fn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s failed: %s", task, err)
|
||||
}
|
||||
|
||||
exitCode := <-exitChan
|
||||
if exitCode != 0 {
|
||||
return fmt.Errorf("%s returned non-zero: %d", task, exitCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupFirewall sets up firewall rules on the external scope only. Any
|
||||
// portmaps are honored as are port exposes.
|
||||
func (t *operations) SetupFirewall(ctx context.Context, config *tether.ExecutorConfig) error {
|
||||
return setupFirewall(ctx, &t.BaseOperations, config)
|
||||
}
|
||||
|
||||
// setupFirewall is broken out from SetupFirewall so that it can be referenced from the test code
|
||||
func setupFirewall(ctx context.Context, t *tether.BaseOperations, config *tether.ExecutorConfig) error {
|
||||
fn := netfilter.Flush(ctx, "VIC")
|
||||
if err := invoke(t, fn, "flush"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range config.Networks {
|
||||
switch endpoint.Network.Type {
|
||||
case constants.ExternalScopeType:
|
||||
id, err := strconv.Atoi(endpoint.ID)
|
||||
if err != nil {
|
||||
log.Errorf("can't apply port rules: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
iface, err := t.LinkBySlot(int32(id))
|
||||
if err != nil {
|
||||
log.Errorf("can't apply rules: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
ifaceName := iface.Attrs().Name
|
||||
log.Debugf("slot %d -> %s", endpoint.ID, ifaceName)
|
||||
|
||||
// ensure that we can pass DHCP traffic if it's necessary
|
||||
if endpoint.Network.TrustLevel != executor.Open && endpoint.IsDynamic() {
|
||||
allowDHCPTraffic(ctx, t, ifaceName)
|
||||
}
|
||||
|
||||
switch endpoint.Network.TrustLevel {
|
||||
case executor.Open:
|
||||
// Configure port redirect in Open deployment
|
||||
if err := setupPorts(ctx, t, endpoint, ifaceName, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Accept all incoming and outgoing traffic
|
||||
for _, chain := range []netfilter.Chain{netfilter.Input, netfilter.Output, netfilter.Forward} {
|
||||
fn := (&netfilter.Rule{
|
||||
Chain: chain,
|
||||
Target: netfilter.Accept,
|
||||
Interface: ifaceName,
|
||||
}).Commit(ctx)
|
||||
if err := invoke(t, fn, "accept all"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case executor.Closed:
|
||||
// Reject all incoming and outgoing traffic
|
||||
// Since our default policy is to drop traffic, nothing is needed here.
|
||||
|
||||
case executor.Outbound:
|
||||
// Reject all incoming traffic, but allow outgoing
|
||||
if err := setupOutboundFirewall(ctx, t, ifaceName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case executor.Peers:
|
||||
// Outbound + all ports open to source addresses in --container-network-ip-range
|
||||
if err := setupOutboundFirewall(ctx, t, ifaceName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Configure port redirect in Peer deployment
|
||||
if err := setupPorts(ctx, t, endpoint, ifaceName, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sourceAddresses := make([]string, len(endpoint.Network.Pools))
|
||||
for i, v := range endpoint.Network.Pools {
|
||||
sourceAddresses[i] = v.String()
|
||||
}
|
||||
fn := (&netfilter.Rule{
|
||||
Chain: netfilter.Input,
|
||||
Target: netfilter.Accept,
|
||||
SourceAddresses: sourceAddresses,
|
||||
Interface: ifaceName,
|
||||
}).Commit(ctx)
|
||||
if err := invoke(t, fn, "allow outbound and peers"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := allowPingTraffic(ctx, t, ifaceName, sourceAddresses); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
// covers executor.Published and executor.Unspecified as well as invalid values
|
||||
log.Infof("Applying published rules for configuration %v", endpoint.Network.TrustLevel)
|
||||
|
||||
if err := setupOutboundFirewall(ctx, t, ifaceName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := allowPingTraffic(ctx, t, ifaceName, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := setupPorts(ctx, t, endpoint, ifaceName, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case constants.BridgeScopeType:
|
||||
id, err := strconv.Atoi(endpoint.ID)
|
||||
if err != nil {
|
||||
log.Errorf("can't apply port rules: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
iface, err := t.LinkBySlot(int32(id))
|
||||
if err != nil {
|
||||
log.Errorf("can't apply rules: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
ifaceName := iface.Attrs().Name
|
||||
log.Debugf("slot %d -> %s", endpoint.ID, ifaceName)
|
||||
|
||||
// Traffic over container bridge network should be peers+outbound.
|
||||
if err := setupOutboundFirewall(ctx, t, ifaceName); err != nil {
|
||||
return err
|
||||
}
|
||||
sourceAddresses := make([]string, len(endpoint.Network.Pools))
|
||||
|
||||
for i, v := range endpoint.Network.Pools {
|
||||
sourceAddresses[i] = v.String()
|
||||
}
|
||||
fn := (&netfilter.Rule{
|
||||
Chain: netfilter.Input,
|
||||
Target: netfilter.Accept,
|
||||
SourceAddresses: sourceAddresses,
|
||||
Interface: ifaceName,
|
||||
}).Commit(ctx)
|
||||
if err := invoke(t, fn, "configure for bridge scope"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return invoke(t, netfilter.Return(ctx, "VIC"), "return from VIC chain")
|
||||
}
|
||||
|
||||
func setupOutboundFirewall(ctx context.Context, t *tether.BaseOperations, ifaceName string) error {
|
||||
// All already established inputs are accepted
|
||||
fn := (&netfilter.Rule{
|
||||
Chain: netfilter.Input,
|
||||
States: []netfilter.State{netfilter.Established, netfilter.Related},
|
||||
Target: netfilter.Accept,
|
||||
Interface: ifaceName,
|
||||
}).Commit(ctx)
|
||||
if err := invoke(t, fn, "permit established inbound"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// All output is accepted
|
||||
fn = (&netfilter.Rule{
|
||||
Chain: netfilter.Output,
|
||||
Target: netfilter.Accept,
|
||||
Interface: ifaceName,
|
||||
}).Commit(ctx)
|
||||
return invoke(t, fn, "permit all outbound")
|
||||
}
|
||||
|
||||
func setupPorts(ctx context.Context, t *tether.BaseOperations, endpoint *tether.NetworkEndpoint, ifaceName string, redirectOnly bool) error {
|
||||
// handle the ports
|
||||
for _, p := range endpoint.Ports {
|
||||
// parse the port maps
|
||||
rules, err := portToRule(p, redirectOnly)
|
||||
if err != nil {
|
||||
log.Errorf("can't apply port rule (%s): %s", p, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("Applying %d rules for port %s", len(rules), p)
|
||||
for _, r := range rules {
|
||||
r.Interface = ifaceName
|
||||
fn := r.Commit(ctx)
|
||||
if err := invoke(t, fn, "allow incoming on published port"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func allowPingTraffic(ctx context.Context, t *tether.BaseOperations, ifaceName string, sourceAddresses []string) error {
|
||||
fn := (&netfilter.Rule{
|
||||
Chain: netfilter.Input,
|
||||
Target: netfilter.Accept,
|
||||
Interface: ifaceName,
|
||||
Protocol: netfilter.ICMP,
|
||||
ICMPType: netfilter.EchoRequest,
|
||||
SourceAddresses: sourceAddresses,
|
||||
}).Commit(ctx)
|
||||
if err := invoke(t, fn, "allow ping inbound"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fn = (&netfilter.Rule{
|
||||
Chain: netfilter.Output,
|
||||
Target: netfilter.Accept,
|
||||
Interface: ifaceName,
|
||||
Protocol: netfilter.ICMP,
|
||||
ICMPType: netfilter.EchoReply,
|
||||
SourceAddresses: sourceAddresses,
|
||||
}).Commit(ctx)
|
||||
return invoke(t, fn, "allow ping outbound")
|
||||
}
|
||||
|
||||
func allowDHCPTraffic(ctx context.Context, t *tether.BaseOperations, ifaceName string) error {
|
||||
fn := (&netfilter.Rule{
|
||||
Chain: netfilter.Input,
|
||||
Target: netfilter.Accept,
|
||||
Interface: ifaceName,
|
||||
Protocol: netfilter.UDP,
|
||||
FromPort: "67:68",
|
||||
SrcPort: "67:68",
|
||||
}).Commit(ctx)
|
||||
if err := invoke(t, fn, "allow dhcp inbound"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fn = (&netfilter.Rule{
|
||||
Chain: netfilter.Output,
|
||||
Target: netfilter.Accept,
|
||||
Interface: ifaceName,
|
||||
Protocol: netfilter.UDP,
|
||||
FromPort: "67:68",
|
||||
SrcPort: "67:68",
|
||||
}).Commit(ctx)
|
||||
return invoke(t, fn, "allow dhcp outbound")
|
||||
}
|
||||
|
||||
func portToRule(p string, redirectOnly bool) ([]*netfilter.Rule, error) {
|
||||
// 9999/tcp
|
||||
s := strings.Split(p, "/")
|
||||
if len(s) != 2 {
|
||||
return nil, errors.New("can't parse port spec: " + p)
|
||||
}
|
||||
|
||||
proto := netfilter.Protocol(s[1])
|
||||
switch proto {
|
||||
case netfilter.UDP:
|
||||
case netfilter.TCP:
|
||||
default:
|
||||
return nil, errors.New("unknown protocol")
|
||||
}
|
||||
|
||||
mapping := strings.Split(s[0], ":")
|
||||
directPort := mapping[len(mapping)-1]
|
||||
|
||||
// publish the actual port directly
|
||||
var rules []*netfilter.Rule
|
||||
expose := &netfilter.Rule{
|
||||
Chain: netfilter.Input,
|
||||
Interface: "external",
|
||||
Target: netfilter.Accept,
|
||||
Protocol: proto,
|
||||
// ranges are specified using a hyphen in docker
|
||||
FromPort: strings.Replace(directPort, "-", ":", -1),
|
||||
}
|
||||
|
||||
if !redirectOnly {
|
||||
rules = append(rules, expose)
|
||||
}
|
||||
|
||||
// if there's no redirection we're done
|
||||
if len(mapping) == 1 || mapping[0] == mapping[1] {
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// redirect port
|
||||
// https://wiki.debian.org/Firewalls-local-port-redirection contains a useful reference
|
||||
|
||||
if strings.Contains(s[0], "-") {
|
||||
return nil, errors.New("cannot port forward a range")
|
||||
}
|
||||
|
||||
rules = append(rules, &netfilter.Rule{
|
||||
Table: netfilter.Nat,
|
||||
Chain: netfilter.Prerouting,
|
||||
Interface: "external",
|
||||
Target: netfilter.Redirect,
|
||||
Protocol: proto,
|
||||
FromPort: mapping[0],
|
||||
ToPort: mapping[1],
|
||||
})
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
161
vendor/github.com/vmware/vic/cmd/tether/ops_linux_test.go
generated
vendored
Normal file
161
vendor/github.com/vmware/vic/cmd/tether/ops_linux_test.go
generated
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
// 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.
|
||||
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
// This has been copied from lib/tether/ but should be split into a common base package that is
|
||||
// dedicated to mocking these operations. We cannot reference lib/tether/*_test elements from
|
||||
// outside that package.
|
||||
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")
|
||||
}
|
||||
300
vendor/github.com/vmware/vic/cmd/tether/tether_test.go
generated
vendored
Normal file
300
vendor/github.com/vmware/vic/cmd/tether/tether_test.go
generated
vendored
Normal file
@@ -0,0 +1,300 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/vmware/vic/lib/config/executor"
|
||||
"github.com/vmware/vic/lib/system"
|
||||
"github.com/vmware/vic/lib/tether"
|
||||
"github.com/vmware/vic/pkg/dio"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/extraconfig"
|
||||
)
|
||||
|
||||
// Copied from lib/tether
|
||||
// because there's no easy way to use test code from other packages and a separate
|
||||
// package causes cyclic dependencies.
|
||||
// Some modifications to deal with the change of package and attach usage
|
||||
|
||||
var Mocked Mocker
|
||||
|
||||
type Mocker struct {
|
||||
Base tether.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
|
||||
|
||||
// debug output gets logged here
|
||||
LogBuffer bytes.Buffer
|
||||
|
||||
// session output gets logged here
|
||||
SessionLogBuffer bytes.Buffer
|
||||
|
||||
// track the number of interfaces for a run
|
||||
maxSlot int
|
||||
// the interfaces in the system indexed by name
|
||||
Interfaces map[string]netlink.Link
|
||||
|
||||
// the hostname of the system
|
||||
Hostname string
|
||||
// the ip configuration for name index networks
|
||||
IPs map[string]net.IP
|
||||
// filesystem mounts, indexed by disk label
|
||||
Mounts map[string]string
|
||||
|
||||
WindowCol uint32
|
||||
WindowRow uint32
|
||||
Signal ssh.Signal
|
||||
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// Start implements the extension method
|
||||
func (t *Mocker) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop implements the extension method
|
||||
func (t *Mocker) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload implements the extension method
|
||||
func (t *Mocker) Reload(config *tether.ExecutorConfig) error {
|
||||
t.once.Do(func() {
|
||||
// the tether has definitely finished it's startup by the time we hit this
|
||||
close(t.Started)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Mocker) Setup(config tether.Config) error {
|
||||
return t.Base.Setup(config)
|
||||
}
|
||||
|
||||
func (t *Mocker) Cleanup() error {
|
||||
err := t.Base.Cleanup()
|
||||
close(t.Cleaned)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Mocker) Log() (io.Writer, error) {
|
||||
return os.Stdout, nil
|
||||
}
|
||||
|
||||
func (t *Mocker) SessionLog(session *tether.SessionConfig) (dio.DynamicMultiWriter, dio.DynamicMultiWriter, error) {
|
||||
return dio.MultiWriter(&t.SessionLogBuffer), dio.MultiWriter(&t.SessionLogBuffer), nil
|
||||
}
|
||||
|
||||
func (t *Mocker) HandleSessionExit(config *tether.ExecutorConfig, session *tether.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
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Mocker) SetupFirewall(ctx context.Context, config *tether.ExecutorConfig) error {
|
||||
err := setupFirewall(ctx, &t.Base, config)
|
||||
// NOTE: we squash errors from here for now because it's almost certain to fail
|
||||
// in absence of a mock for the iptables update
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply takes the network endpoint configuration and applies it to the system
|
||||
func (t *Mocker) Apply(endpoint *tether.NetworkEndpoint) error {
|
||||
return tether.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 {
|
||||
return errors.New("Fork test not implemented")
|
||||
}
|
||||
|
||||
// LaunchUtility uses the underlying implementation for launching and tracking utility processes
|
||||
func (t *Mocker) LaunchUtility(fn tether.UtilityFn) (<-chan int, error) {
|
||||
return t.Base.LaunchUtility(fn)
|
||||
}
|
||||
|
||||
func (t *Mocker) HandleUtilityExit(pid, exitCode int) bool {
|
||||
return t.Base.HandleUtilityExit(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)
|
||||
|
||||
retCode := m.Run()
|
||||
|
||||
// call with result of m.Run()
|
||||
os.Exit(retCode)
|
||||
}
|
||||
|
||||
func StartTether(t *testing.T, cfg *executor.ExecutorConfig, mocker *Mocker) (tether.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 = tether.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 StartAttachTether(t *testing.T, cfg *executor.ExecutorConfig, mocker *Mocker) (tether.Tether, extraconfig.DataSource, net.Conn) {
|
||||
store := extraconfig.New()
|
||||
sink := store.Put
|
||||
src := store.Get
|
||||
extraconfig.Encode(sink, cfg)
|
||||
log.Debugf("Test configuration: %#v", sink)
|
||||
|
||||
tthr = tether.New(src, sink, mocker)
|
||||
tthr.Register("mocker", mocker)
|
||||
tthr.Register("Attach", server)
|
||||
|
||||
// run the tether to service the attach
|
||||
go func() {
|
||||
erR := tthr.Start()
|
||||
if erR != nil {
|
||||
t.Error(erR)
|
||||
}
|
||||
}()
|
||||
|
||||
// create client on the mock pipe
|
||||
conn, err := mockBackChannel(context.Background())
|
||||
if err != nil && (err != io.EOF || server.(*testAttachServer).enabled) {
|
||||
// we accept the case where the error is end-of-file and the attach server is disabled because that's
|
||||
// expected when the tether is shut down.
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
return tthr, src, conn
|
||||
}
|
||||
|
||||
func tetherTestSetup(t *testing.T) *Mocker {
|
||||
log.Infof("Started test setup for %s", t.Name())
|
||||
|
||||
// use the mock ops - fresh one each time as tests might apply different mocked calls
|
||||
mocker := Mocker{
|
||||
Started: make(chan bool),
|
||||
Cleaned: make(chan bool),
|
||||
Interfaces: make(map[string]netlink.Link, 0),
|
||||
}
|
||||
|
||||
// replace the Sys variable with a mock
|
||||
tether.Sys = system.NewWithRoot(pathPrefix)
|
||||
tether.Sys.Syscall = &tether.MockSyscall{}
|
||||
|
||||
tether.BindSys = system.NewWithRoot(path.Join(pathPrefix, "/.tether"))
|
||||
tether.BindSys.Syscall = &tether.MockSyscall{}
|
||||
|
||||
// ensure that the sys and bindsys root exists
|
||||
// #nosec
|
||||
os.MkdirAll(tether.Sys.Root, 0700)
|
||||
// #nosec
|
||||
os.MkdirAll(tether.BindSys.Root, 0700)
|
||||
|
||||
return &mocker
|
||||
}
|
||||
|
||||
func tetherTestTeardown(t *testing.T, mocker *Mocker) {
|
||||
<-mocker.Cleaned
|
||||
|
||||
// cleanup
|
||||
os.RemoveAll(pathPrefix)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
log.Infof("Finished test teardown for %s", t.Name())
|
||||
}
|
||||
Reference in New Issue
Block a user