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

o/hookstate/ctlcmd: add optional --pid and --apparmor-label arguments to "snapctl is-connected" #9132

Merged
merged 16 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions overlord/hookstate/ctlcmd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,11 @@ func (c *MockCommand) Execute(args []string) error {

return nil
}

func MockCgroupSnapNameFromPid(f func(int) (string, error)) (restore func()) {
old := cgroupSnapNameFromPid
cgroupSnapNameFromPid = f
return func() {
cgroupSnapNameFromPid = old
}
}
38 changes: 36 additions & 2 deletions overlord/hookstate/ctlcmd/is_connected.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,19 @@ import (
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/overlord/ifacestate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/sandbox/cgroup"
"github.com/snapcore/snapd/snap"
)

var cgroupSnapNameFromPid = cgroup.SnapNameFromPid

type isConnectedCommand struct {
baseCommand

Positional struct {
PlugOrSlotSpec string `positional-args:"true" positional-arg-name:"<plug|slot>"`
} `positional-args:"true" required:"true"`
Pid int `long:"pid" description:"Process ID for a plausibly connected process"`
}

var shortIsConnectedHelp = i18n.G(`Return success if the given plug or slot is connected, and failure otherwise`)
Expand All @@ -47,6 +52,12 @@ $ echo $?

Snaps can only query their own plugs and slots - snap name is implicit and
implied by the snapctl execution context.

The --pid option can be used to determine whether a plug or slot is
connected to the snap identified by the given process ID. In this
mode, additional failure exit codes may be returned: 10 if the other
process is not snap confined, or 11 if the other snap is not connected
but uses classic confinement.
`)

func init() {
Expand Down Expand Up @@ -87,6 +98,19 @@ func (c *isConnectedCommand) Execute(args []string) error {
return fmt.Errorf("internal error: cannot get connections: %s", err)
}

var otherSnap *snap.Info
if c.Pid != 0 {
name, err := cgroupSnapNameFromPid(c.Pid)
if err != nil {
// Indicate that this pid is not a snap
return &UnsuccessfulError{ExitCode: 10}
}
otherSnap, err = snapstate.CurrentInfo(st, name)
if err != nil {
return fmt.Errorf("internal error: cannot get snap info for pid %d: %s", c.Pid, err)
}
}

// snapName is the name of the snap executing snapctl command, it's
// obtained from the context (ephemeral if run by apps, or full if run by
// hooks). plug and slot names are unique within a snap, so there is no
Expand All @@ -102,10 +126,20 @@ func (c *isConnectedCommand) Execute(args []string) error {

matchingPlug := connRef.PlugRef.Snap == snapName && connRef.PlugRef.Name == plugOrSlot
matchingSlot := connRef.SlotRef.Snap == snapName && connRef.SlotRef.Name == plugOrSlot
if matchingPlug || matchingSlot {
return nil
if otherSnap != nil {
if matchingPlug && connRef.SlotRef.Snap == otherSnap.InstanceName() || matchingSlot && connRef.PlugRef.Snap == otherSnap.InstanceName() {
return nil
}
} else {
if matchingPlug || matchingSlot {
return nil
}
}
}

if otherSnap != nil && otherSnap.Confinement == snap.ClassicConfinement {
return &UnsuccessfulError{ExitCode: 11}
}

return &UnsuccessfulError{ExitCode: 1}
}
57 changes: 57 additions & 0 deletions overlord/hookstate/ctlcmd/is_connected_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
package ctlcmd_test

import (
"fmt"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
Expand Down Expand Up @@ -74,6 +76,36 @@ var isConnectedTests = []struct {
}, {
args: []string{"is-connected", "foo"},
err: `snap "snap1" has no plug or slot named "foo"`,
}, {
// snap1:plug1 is connected to snap2
args: []string{"is-connected", "--pid", "1002", "plug1"},
}, {
// snap1:plug1 is not connected to snap3
args: []string{"is-connected", "--pid", "1003", "plug1"},
exitCode: 1,
}, {
// snap1:plug1 is not connected to a non-snap pid
args: []string{"is-connected", "--pid", "42", "plug1"},
exitCode: 10,
}, {
// snap1:plug1 is not connected to snap4, but snap4 is classic
args: []string{"is-connected", "--pid", "1004", "plug1"},
exitCode: 11,
}, {
// snap1:slot1 is connected to snap3
args: []string{"is-connected", "--pid", "1003", "slot1"},
}, {
// snap1:slot1 is not connected to snap2
args: []string{"is-connected", "--pid", "1002", "slot1"},
exitCode: 1,
}, {
// snap1:slot1 is not connected to a non-snap pid
args: []string{"is-connected", "--pid", "42", "slot1"},
exitCode: 10,
}, {
// snap1:slot1 is not connected to snap4, but snap4 is classic
args: []string{"is-connected", "--pid", "1004", "slot1"},
exitCode: 11,
}}

func mockInstalledSnap(c *C, st *state.State, snapYaml string) {
Expand Down Expand Up @@ -103,6 +135,31 @@ plugs:
slots:
slot1:
interface: x11`)
mockInstalledSnap(c, s.st, `name: snap2
slots:
slot2:
interface: x11`)
mockInstalledSnap(c, s.st, `name: snap3
plugs:
plug4:
interface: x11
slots:
slot3:
interface: x11`)
mockInstalledSnap(c, s.st, `name: snap4
confinement: classic
slots:
slot4:
interface: x11`)
restore := ctlcmd.MockCgroupSnapNameFromPid(func(pid int) (string, error) {
switch {
case 1000 < pid && pid < 1100:
return fmt.Sprintf("snap%d", pid-1000), nil
default:
return "", fmt.Errorf("Not a snap")
}
})
defer restore()

s.st.Set("conns", map[string]interface{}{
"snap1:plug1 snap2:slot2": map[string]interface{}{},
Expand Down
86 changes: 86 additions & 0 deletions tests/main/snapctl-is-connected-pid/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
summary: Ensure that "snapctl is-connected --pid" works.

prepare: |
#shellcheck source=tests/lib/snaps.sh
. "$TESTSLIB"/snaps.sh
install_local test-snap1
install_local test-snap2

#shellcheck source=tests/lib/dirs.sh
. "$TESTSLIB"/dirs.sh
case "$SPREAD_SYSTEM" in
fedora-*|arch-*|centos-*)
# although classic snaps do not work out of the box on fedora,
# we still want to verify if the basics do work if the user
# symlinks /snap to $SNAP_MOUNT_DIR themselves
ln -sf $SNAP_MOUNT_DIR /snap
jhenstridge marked this conversation as resolved.
Show resolved Hide resolved
;;
esac

restore: |
case "$SPREAD_SYSTEM" in
fedora-*|arch-*|centos-*)
rm -f /snap
;;
esac

execute: |
echo "The test-snap1 service is running"
systemctl is-active snap.test-snap1.svc.service
svc_pid=$(systemctl show --property=MainPID snap.test-snap1.svc.service | cut -d = -f 2)

fail() {
jhenstridge marked this conversation as resolved.
Show resolved Hide resolved
expected="$1"
shift
if "$@"; then
jhenstridge marked this conversation as resolved.
Show resolved Hide resolved
echo "Expected command to fail"
exit 1
else
err="$?"
test "$err" -eq "$expected"
fi
}

echo "Plugs and slots are initially disconnected"
not test-snap2.snapctl is-connected bar-plug
not test-snap2.snapctl is-connected foo-slot

echo "Disconnected interfaces are not connected to a snap process"
fail 1 test-snap2.snapctl is-connected --pid "$svc_pid" bar-plug
fail 1 test-snap2.snapctl is-connected --pid "$svc_pid" foo-slot

echo "Disconnected interfaces are not connected to non-snap process"
fail 10 test-snap2.snapctl is-connected --pid 1 bar-plug
fail 10 test-snap2.snapctl is-connected --pid 1 foo-slot

echo "Connect interfaces"
snap connect test-snap1:foo-plug test-snap2:foo-slot
snap connect test-snap2:bar-plug test-snap1:bar-slot

echo "Connected interfaces report as connected to snap process"
test-snap2.snapctl is-connected --pid "$svc_pid" bar-plug
test-snap2.snapctl is-connected --pid "$svc_pid" foo-slot

echo "Interfaces still not connected to non-snap process"
fail 10 test-snap2.snapctl is-connected --pid 1 bar-plug
fail 10 test-snap2.snapctl is-connected --pid 1 foo-slot
jhenstridge marked this conversation as resolved.
Show resolved Hide resolved

# The remaining tests rely on classic confinement, so skip Ubuntu Core
if [[ "$SPREAD_SYSTEM" = ubuntu-core-* ]]; then
exit 0
fi

#shellcheck source=tests/lib/snaps.sh
. "$TESTSLIB"/snaps.sh
install_local_classic test-snap-classic

echo "The test-snap-classic service is running"
systemctl is-active snap.test-snap-classic.svc.service
classic_pid=$(systemctl show --property=MainPID snap.test-snap-classic.svc.service | cut -d = -f 2)

echo "Unconnected classic snaps report a special exit code"
fail 11 test-snap2.snapctl is-connected --pid "$classic_pid" foo-slot

echo "But still reports success when connected"
snap connect test-snap-classic:foo-plug test-snap2:foo-slot
test-snap2.snapctl is-connected --pid "$classic_pid" foo-slot
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

echo "service running"
exec sleep infinity
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: test-snap-classic
confinement: classic
version: 1
summary: Classic test snap

plugs:
foo-plug:
interface: content
content: foo
target: $SNAP_COMMON/foo

apps:
svc:
command: bin/service.sh
daemon: simple
4 changes: 4 additions & 0 deletions tests/main/snapctl-is-connected-pid/test-snap1/bin/service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

echo "service running"
exec sleep infinity
21 changes: 21 additions & 0 deletions tests/main/snapctl-is-connected-pid/test-snap1/meta/snap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: test-snap1
version: 1
summary: First test snap

plugs:
foo-plug:
interface: content
content: foo
target: $SNAP_COMMON/foo

slots:
bar-slot:
interface: content
content: bar
read:
- $SNAP

apps:
svc:
command: bin/service.sh
daemon: simple
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
exec snapctl "$@"
20 changes: 20 additions & 0 deletions tests/main/snapctl-is-connected-pid/test-snap2/meta/snap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: test-snap2
version: 1
summary: Second test snap

plugs:
bar-plug:
interface: content
content: bar
target: $SNAP_COMMON/bar

slots:
foo-slot:
interface: content
content: foo
read:
- $SNAP

apps:
snapctl:
command: bin/run-snapctl.sh