Skip to content

Commit

Permalink
Add host-level terminals
Browse files Browse the repository at this point in the history
  • Loading branch information
Alfonso Acosta committed Mar 29, 2016
1 parent 7b03f01 commit c1c40ad
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 12 deletions.
11 changes: 10 additions & 1 deletion common/xfer/pipes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 0 additions & 2 deletions integration/410_container_control_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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' )
Expand Down
19 changes: 19 additions & 0 deletions integration/420_host_control_test.sh
Original file line number Diff line number Diff line change
@@ -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
16 changes: 13 additions & 3 deletions probe/controls/pipes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package controls

import (
"fmt"
"io"
"math/rand"

"github.com/weaveworks/scope/common/xfer"
Expand All @@ -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,
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion probe/docker/controls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion probe/docker/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})

Expand Down
66 changes: 66 additions & 0 deletions probe/host/controls.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
3 changes: 3 additions & 0 deletions probe/host/controls_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package host

var hostShellCmd = []string{"/bin/bash"}
78 changes: 78 additions & 0 deletions probe/host/controls_linux.go
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 11 additions & 2 deletions probe/host/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -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
}
3 changes: 2 additions & 1 deletion probe/host/reporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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)
}
Expand Down
5 changes: 4 additions & 1 deletion prog/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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))
Expand Down

0 comments on commit c1c40ad

Please sign in to comment.