Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

host-level ttys #1208

Merged
merged 5 commits into from
Apr 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

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
58 changes: 58 additions & 0 deletions probe/host/controls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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"
)

func (r *Reporter) registerControls() {
controls.Register(ExecHost, r.execHost)
}

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.


func (*Reporter) deregisterControls() {
controls.Rm(ExecHost)
}

func (r *Reporter) execHost(req xfer.Request) xfer.Response {
cmd := exec.Command(r.hostShellCmd[0], r.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, r.pipes, req.AppID)
if err != nil {
return xfer.ResponseError(err)
}
pipe.OnClose(func() {
if err := cmd.Process.Kill(); err != nil {
log.Errorf("Error stopping host shell: %v", err)
}
if err := ptyPipe.Close(); err != nil {
log.Errorf("Error closing host shell's pty: %v", err)
}
log.Info("Host shell closed.")
})
go func() {
if err := cmd.Wait(); err != nil {
log.Errorf("Error waiting on host shell: %v", err)
}
pipe.Close()

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

}()

return xfer.Response{
Pipe: id,
RawTTY: true,
}
}
5 changes: 5 additions & 0 deletions probe/host/controls_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package host

func getHostShellCmd() []string {
return []string{"/bin/bash"}
}
88 changes: 88 additions & 0 deletions probe/host/controls_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package host

import (
"bytes"
"os/exec"
"strings"
"syscall"

log "github.com/Sirupsen/logrus"
"github.com/willdonnelly/passwd"
)

func getHostShellCmd() []string {
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)
return []string{
"/usr/bin/nsenter", "-t1", "-m", "-i", "-n", "-p", "--no-fork",
"--setuid", uid,
"--setgid", gid,
shell,
}
}

_, _, shell := getRootUserDetails([]string{"cat", "/etc/passwd"})
return []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 {
log.Warnf(
"getRootUserDetails(): error running read passwd command %q: %s",
strings.Join(readPasswdCmd, " "),
err,
)
return
}

entries, err := passwd.ParseReader(cmdBuffer)
if err != nil {
log.Warnf("getRootUserDetails(): error parsing passwd: %s", err)
return
}

entry, ok := entries["root"]
if !ok {
log.Warnf("getRootUserDetails(): no root entry in passwd")
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

path := "/proc/self/ns/mnt"
if err := syscall.Stat(path, &statT); err != nil {
log.Warnf("isProbeContainerized(): stat() error on %q: %s", path, err)
return false
}
selfMountNamespaceID := statT.Ino

path = "/proc/1/ns/mnt"
if err := syscall.Stat(path, &statT); err != nil {
log.Warnf("isProbeContainerized(): stat() error on %q: %s", path, err)
return false
}

return selfMountNamespaceID != statT.Ino
}
35 changes: 28 additions & 7 deletions probe/host/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/weaveworks/scope/common/mtime"
"github.com/weaveworks/scope/probe/controls"
"github.com/weaveworks/scope/report"
)

Expand Down Expand Up @@ -34,17 +35,25 @@ const (

// Reporter generates Reports containing the host topology.
type Reporter struct {
hostID string
hostName string
hostID string
hostName string
probeID string
pipes controls.PipeClient
hostShellCmd []string
}

// NewReporter returns a Reporter which produces a report containing host
// topology for this host.
func NewReporter(hostID, hostName string) *Reporter {
return &Reporter{
hostID: hostID,
hostName: hostName,
func NewReporter(hostID, hostName, probeID string, pipes controls.PipeClient) *Reporter {
r := &Reporter{
hostID: hostID,
hostName: hostName,
probeID: probeID,
pipes: pipes,
hostShellCmd: getHostShellCmd(),
}
r.registerControls()
return r
}

// Name of this reporter, for metrics gathering
Expand Down Expand Up @@ -98,6 +107,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 +116,18 @@ 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
}

// Stop stops the reporter.
func (r *Reporter) Stop() {
r.deregisterControls()
}
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, nil).Report()
if err != nil {
t.Fatal(err)
}
Expand Down
4 changes: 3 additions & 1 deletion prog/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,11 @@ func probeMain() {

p := probe.New(*spyInterval, *publishInterval, clients)
p.AddTicker(processCache)
hostReporter := host.NewReporter(hostID, hostName, probeID, clients)
defer hostReporter.Stop()
p.AddReporter(
endpointReporter,
host.NewReporter(hostID, hostName),
hostReporter,
process.NewReporter(processCache, hostID, process.GetDeltaTotalJiffies),
)
p.AddTagger(probe.NewTopologyTagger(), host.NewTagger(hostID))
Expand Down
23 changes: 23 additions & 0 deletions vendor/github.com/kr/pty/License

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading