mirror of
https://github.com/weaveworks/scope.git
synced 2026-02-14 18:09:59 +00:00
Add host-level terminals
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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' )
|
||||
|
||||
19
integration/420_host_control_test.sh
Executable file
19
integration/420_host_control_test.sh
Executable 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
66
probe/host/controls.go
Normal 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,
|
||||
}
|
||||
}
|
||||
3
probe/host/controls_darwin.go
Normal file
3
probe/host/controls_darwin.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package host
|
||||
|
||||
var hostShellCmd = []string{"/bin/bash"}
|
||||
78
probe/host/controls_linux.go
Normal file
78
probe/host/controls_linux.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user