diff --git a/common/xfer/pipes.go b/common/xfer/pipes.go index 9510a9d7f..895b2bb33 100644 --- a/common/xfer/pipes.go +++ b/common/xfer/pipes.go @@ -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() diff --git a/integration/410_container_control_test.sh b/integration/410_container_control_test.sh index 483c18622..b85a57a8b 100755 --- a/integration/410_container_control_test.sh +++ b/integration/410_container_control_test.sh @@ -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;/docker_exec_container" | jq -r '.pipe' ) diff --git a/integration/420_host_control_test.sh b/integration/420_host_control_test.sh new file mode 100755 index 000000000..54d23314f --- /dev/null +++ b/integration/420_host_control_test.sh @@ -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_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 diff --git a/probe/controls/pipes.go b/probe/controls/pipes.go index cf540419f..ec4198eed 100644 --- a/probe/controls/pipes.go +++ b/probe/controls/pipes.go @@ -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) diff --git a/probe/docker/controls.go b/probe/docker/controls.go index 8977523e9..23b8d23ba 100644 --- a/probe/docker/controls.go +++ b/probe/docker/controls.go @@ -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" diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index d56a2677e..87b69a606 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -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", }) diff --git a/probe/host/controls.go b/probe/host/controls.go new file mode 100644 index 000000000..c7d8d878c --- /dev/null +++ b/probe/host/controls.go @@ -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, + } +} diff --git a/probe/host/controls_darwin.go b/probe/host/controls_darwin.go new file mode 100644 index 000000000..418293025 --- /dev/null +++ b/probe/host/controls_darwin.go @@ -0,0 +1,3 @@ +package host + +var hostShellCmd = []string{"/bin/bash"} diff --git a/probe/host/controls_linux.go b/probe/host/controls_linux.go new file mode 100644 index 000000000..2a4ef4459 --- /dev/null +++ b/probe/host/controls_linux.go @@ -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 +} diff --git a/probe/host/reporter.go b/probe/host/reporter.go index 5d20e3fcf..eced090eb 100644 --- a/probe/host/reporter.go +++ b/probe/host/reporter.go @@ -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 } diff --git a/probe/host/reporter_test.go b/probe/host/reporter_test.go index 4bf7ba2e1..86abd89fe 100644 --- a/probe/host/reporter_test.go +++ b/probe/host/reporter_test.go @@ -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) } diff --git a/prog/probe.go b/prog/probe.go index ccb7f10e6..79408f9b4 100644 --- a/prog/probe.go +++ b/prog/probe.go @@ -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))