Add host-level terminals

This commit is contained in:
Alfonso Acosta
2016-03-23 22:31:46 +00:00
parent 7b03f01630
commit c1c40ad20d
12 changed files with 208 additions and 12 deletions

View File

@@ -27,7 +27,16 @@ type pipe struct {
onClose func()
}
// NewPipe makes a new... pipe.
// NewPipeFromEnds makes a new pipe specifying its ends
func NewPipeFromEnds(local io.ReadWriter, remote io.ReadWriter) Pipe {
return &pipe{
port: local,
starboard: remote,
quit: make(chan struct{}),
}
}
// NewPipe makes a new pipe
func NewPipe() Pipe {
r1, w1 := io.Pipe()
r2, w2 := io.Pipe()

View File

@@ -13,8 +13,6 @@ wait_for_containers $HOST1 60 alpine
assert "docker_on $HOST1 inspect --format='{{.State.Running}}' alpine" "true"
PROBEID=$(docker_on $HOST1 logs weavescope 2>&1 | grep "probe starting" | sed -n 's/^.*ID \([0-9a-f]*\)$/\1/p')
HOSTID=$(echo $HOST1 | cut -d"." -f1)
# Execute 'echo foo' in a container tty and check its output
PIPEID=$(curl -s -f -X POST "http://$HOST1:4040/api/control/$PROBEID/$CID;<container>/docker_exec_container" | jq -r '.pipe' )

View File

@@ -0,0 +1,19 @@
#! /bin/bash
. ./config.sh
start_suite "Test host controls"
weave_on $HOST1 launch
scope_on $HOST1 launch
sleep 10
PROBEID=$(docker_on $HOST1 logs weavescope 2>&1 | grep "probe starting" | sed -n 's/^.*ID \([0-9a-f]*\)$/\1/p')
HOSTID=$($SSH $HOST1 hostname)
# Execute 'echo foo' in the host tty and check its output
PIPEID=$(curl -s -f -X POST "http://$HOST1:4040/api/control/$PROBEID/$HOSTID;<host>/host_exec" | jq -r '.pipe' )
assert "(sleep 1 && echo \"PS1=''; echo foo\" && sleep 1) | wscat -b 'ws://$HOST1:4040/api/pipe/$PIPEID' | col -pb | tail -n 1" "foo\n"
scope_end_suite

View File

@@ -2,6 +2,7 @@ package controls
import (
"fmt"
"io"
"math/rand"
"github.com/weaveworks/scope/common/xfer"
@@ -21,11 +22,10 @@ type pipe struct {
client PipeClient
}
// NewPipe creats a new pipe and connects it to the app.
var NewPipe = func(c PipeClient, appID string) (string, xfer.Pipe, error) {
func newPipe(p xfer.Pipe, c PipeClient, appID string) (string, xfer.Pipe, error) {
pipeID := fmt.Sprintf("pipe-%d", rand.Int63())
pipe := &pipe{
Pipe: xfer.NewPipe(),
Pipe: p,
appID: appID,
id: pipeID,
client: c,
@@ -36,6 +36,16 @@ var NewPipe = func(c PipeClient, appID string) (string, xfer.Pipe, error) {
return pipeID, pipe, nil
}
// NewPipe creates a new pipe and connects it to the app.
var NewPipe = func(c PipeClient, appID string) (string, xfer.Pipe, error) {
return newPipe(xfer.NewPipe(), c, appID)
}
// NewPipeFromEnds creates a new pipe from its ends and connects it to the app.
func NewPipeFromEnds(local, remote io.ReadWriter, c PipeClient, appID string) (string, xfer.Pipe, error) {
return newPipe(xfer.NewPipeFromEnds(local, remote), c, appID)
}
func (p *pipe) Close() error {
err1 := p.Pipe.Close()
err2 := p.client.PipeClose(p.appID, p.id)

View File

@@ -10,7 +10,7 @@ import (
"github.com/weaveworks/scope/report"
)
// Control IDs used by the docker intergation.
// Control IDs used by the docker integration.
const (
StopContainer = "docker_stop_container"
StartContainer = "docker_start_container"

View File

@@ -102,7 +102,7 @@ func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology {
})
result.Controls.AddControl(report.Control{
ID: ExecContainer,
Human: "Exec /bin/sh",
Human: "Exec shell",
Icon: "fa-terminal",
})

66
probe/host/controls.go Normal file
View File

@@ -0,0 +1,66 @@
package host
import (
"os/exec"
log "github.com/Sirupsen/logrus"
"github.com/kr/pty"
"github.com/weaveworks/scope/common/xfer"
"github.com/weaveworks/scope/probe/controls"
)
// Control IDs used by the host integration.
const (
ExecHost = "host_exec"
)
// Controls handles controls for a hosts.
type Controls struct {
pipes controls.PipeClient
}
// NewControls creates new host controls.
func NewControls(pipes controls.PipeClient) *Controls {
c := &Controls{pipes: pipes}
controls.Register(ExecHost, c.execHost)
return c
}
// Stop stops the host controls.
func (*Controls) Stop() {
controls.Rm(ExecHost)
}
func (c *Controls) execHost(req xfer.Request) xfer.Response {
cmd := exec.Command(hostShellCmd[0], hostShellCmd[1:]...)
cmd.Env = []string{"TERM=xterm"}
ptyPipe, err := pty.Start(cmd)
if err != nil {
return xfer.ResponseError(err)
}
id, pipe, err := controls.NewPipeFromEnds(nil, ptyPipe, c.pipes, req.AppID)
if err != nil {
return xfer.ResponseError(err)
}
pipe.OnClose(func() {
if err := cmd.Process.Kill(); err != nil {
log.Errorf("Error closing host shell: %v", err)
return
}
log.Info("Host shell closed.")
})
go func() {
if err := cmd.Wait(); err != nil {
log.Errorf("Error waiting on host shell: %v", err)
}
ptyPipe.Close()
pipe.Close()
}()
return xfer.Response{
Pipe: id,
RawTTY: true,
}
}

View File

@@ -0,0 +1,3 @@
package host
var hostShellCmd = []string{"/bin/bash"}

View File

@@ -0,0 +1,78 @@
package host
import (
"bytes"
"os/exec"
"syscall"
"github.com/willdonnelly/passwd"
)
var hostShellCmd []string
func init() {
if isProbeContainerized() {
// Escape the container namespaces and jump into the ones from
// the host's init process.
// Note: There should be no need to enter into the host network
// and PID namespace because we should already already be there
// but it doesn't hurt.
readPasswdCmd := []string{"/usr/bin/nsenter", "-t1", "-m", "--no-fork", "cat", "/etc/passwd"}
uid, gid, shell := getRootUserDetails(readPasswdCmd)
hostShellCmd = []string{
"/usr/bin/nsenter", "-t1", "-m", "-i", "-n", "-p", "--no-fork",
"--setuid", uid,
"--setgid", gid,
shell,
}
return
}
_, _, shell := getRootUserDetails([]string{"cat", "/etc/passwd"})
hostShellCmd = []string{shell}
}
func getRootUserDetails(readPasswdCmd []string) (uid, gid, shell string) {
uid = "0"
gid = "0"
shell = "/bin/sh"
cmd := exec.Command(readPasswdCmd[0], readPasswdCmd[1:]...)
cmdBuffer := &bytes.Buffer{}
cmd.Stdout = cmdBuffer
if err := cmd.Run(); err != nil {
return
}
entries, err := passwd.ParseReader(cmdBuffer)
if err != nil {
return
}
entry, ok := entries["root"]
if !ok {
return
}
return entry.Uid, entry.Gid, entry.Shell
}
func isProbeContainerized() bool {
// Figure out whether we are running in a container by checking if our
// mount namespace matches the one from init process. This works
// because, when containerized, the Scope probes run in the host's PID
// namespace (and if they weren't due to a configuration problem, we
// wouldn't have a way to escape the container anyhow).
var statT syscall.Stat_t
if err := syscall.Stat("/proc/self/ns/mnt", &statT); err != nil {
return false
}
selfMountNamespaceID := statT.Ino
if err := syscall.Stat("/proc/1/ns/mnt", &statT); err != nil {
return false
}
return selfMountNamespaceID != statT.Ino
}

View File

@@ -36,14 +36,16 @@ const (
type Reporter struct {
hostID string
hostName string
probeID string
}
// NewReporter returns a Reporter which produces a report containing host
// topology for this host.
func NewReporter(hostID, hostName string) *Reporter {
func NewReporter(hostID, hostName, probeID string) *Reporter {
return &Reporter{
hostID: hostID,
hostName: hostName,
probeID: probeID,
}
}
@@ -98,6 +100,7 @@ func (r *Reporter) Report() (report.Report, error) {
memoryUsage, max := GetMemoryUsageBytes()
metrics[MemoryUsage] = report.MakeMetric().Add(now, memoryUsage).WithMax(max)
metadata := map[string]string{report.ControlProbeID: r.probeID}
rep.Host.AddNode(report.MakeHostNodeID(r.hostID), report.MakeNodeWith(map[string]string{
Timestamp: mtime.Now().UTC().Format(time.RFC3339Nano),
HostName: r.hostName,
@@ -106,7 +109,13 @@ func (r *Reporter) Report() (report.Report, error) {
Uptime: uptime.String(),
}).WithSets(report.EmptySets.
Add(LocalNetworks, report.MakeStringSet(localCIDRs...)),
).WithMetrics(metrics))
).WithMetrics(metrics).WithControls(ExecHost).WithLatests(metadata))
rep.Host.Controls.AddControl(report.Control{
ID: ExecHost,
Human: "Exec shell",
Icon: "fa-terminal",
})
return rep, nil
}

View File

@@ -18,6 +18,7 @@ func TestReporter(t *testing.T) {
network = "192.168.0.0/16"
hostID = "hostid"
hostname = "hostname"
probeID = "abcdeadbeef"
timestamp = time.Now()
metrics = report.Metrics{
host.Load1: report.MakeMetric().Add(timestamp, 1.0),
@@ -57,7 +58,7 @@ func TestReporter(t *testing.T) {
host.GetMemoryUsageBytes = func() (float64, float64) { return 60.0, 100.0 }
host.GetLocalNetworks = func() ([]*net.IPNet, error) { return []*net.IPNet{ipnet}, nil }
rpt, err := host.NewReporter(hostID, hostname).Report()
rpt, err := host.NewReporter(hostID, hostname, probeID).Report()
if err != nil {
t.Fatal(err)
}

View File

@@ -129,6 +129,9 @@ func probeMain() {
})
defer clients.Stop()
hostControls := host.NewControls(clients)
defer hostControls.Stop()
resolver := appclient.NewResolver(targets, net.LookupIP, clients.Set)
defer resolver.Stop()
@@ -142,7 +145,7 @@ func probeMain() {
p.AddTicker(processCache)
p.AddReporter(
endpointReporter,
host.NewReporter(hostID, hostName),
host.NewReporter(hostID, hostName, probeID),
process.NewReporter(processCache, hostID, process.GetDeltaTotalJiffies),
)
p.AddTagger(probe.NewTopologyTagger(), host.NewTagger(hostID))