Skip to content

Commit

Permalink
Merge pull request canonical#11813 from Meulengracht/feature/journal-…
Browse files Browse the repository at this point in the history
…quota-6

overlord/ifacestate: add journal bind-mount snap layout when snap is in a journal quota group (4/n)
  • Loading branch information
mvo5 authored Jun 10, 2022
2 parents 13571aa + 2b28dda commit 5d514ae
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 40 deletions.
1 change: 1 addition & 0 deletions overlord/ifacestate/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ var (

BatchConnectTasks = batchConnectTasks
FirstTaskAfterBootWhenPreseeding = firstTaskAfterBootWhenPreseeding
BuildConfinementOptions = buildConfinementOptions
)

type ConnectOpts = connectOpts
Expand Down
115 changes: 98 additions & 17 deletions overlord/ifacestate/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,76 @@ package ifacestate
import (
"errors"
"fmt"
"path"
"reflect"
"sort"
"strings"
"time"

"gopkg.in/tomb.v2"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/hotplug"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/servicestate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/quota"
"github.com/snapcore/snapd/timings"
)

var snapstateFinishRestart = snapstate.FinishRestart

// confinementOptions returns interfaces.ConfinementOptions from snapstate.Flags.
func confinementOptions(flags snapstate.Flags) interfaces.ConfinementOptions {
return interfaces.ConfinementOptions{
DevMode: flags.DevMode,
JailMode: flags.JailMode,
Classic: flags.Classic,
// journalQuotaLayout returns the necessary journal quota mount layouts
// to mimick what systemd does for services with log namespaces.
func journalQuotaLayout(quotaGroup *quota.Group) []snap.Layout {
if quotaGroup.JournalLimit == nil {
return nil
}

// bind mount the journal namespace folder on top of the journal folder
// /etc/systemd/journal.<ns> -> /etc/systemd/journal
layouts := []snap.Layout{{
Bind: path.Join(dirs.SnapSystemdDir, fmt.Sprintf("journal.snap-%s", quotaGroup.Name)),
Path: path.Join(dirs.SnapSystemdDir, "journal"),
Mode: 0755,
}}
return layouts
}

// getExtraLayouts helper function to dynamically calculate the extra mount layouts for
// a snap instance. These are the layouts which can change during the lifetime of a snap
// like for instance mimicking systemd journal namespace mount layouts.
func getExtraLayouts(st *state.State, snapInstanceName string) ([]snap.Layout, error) {
snapOpts, err := servicestate.SnapServiceOptions(st, snapInstanceName, nil)
if err != nil {
return nil, err
}

var extraLayouts []snap.Layout
if snapOpts.QuotaGroup != nil {
extraLayouts = append(extraLayouts, journalQuotaLayout(snapOpts.QuotaGroup)...)
}

return extraLayouts, nil
}

func buildConfinementOptions(st *state.State, snapInstanceName string, flags snapstate.Flags) (interfaces.ConfinementOptions, error) {
extraLayouts, err := getExtraLayouts(st, snapInstanceName)
if err != nil {
return interfaces.ConfinementOptions{}, fmt.Errorf("cannot get extra mount layouts of snap %q: %s", snapInstanceName, err)
}

return interfaces.ConfinementOptions{
DevMode: flags.DevMode,
JailMode: flags.JailMode,
Classic: flags.Classic,
ExtraLayouts: extraLayouts,
}, nil
}

func (m *InterfaceManager) setupAffectedSnaps(task *state.Task, affectingSnap string, affectedSnaps []string, tm timings.Measurer) error {
Expand All @@ -72,7 +115,10 @@ func (m *InterfaceManager) setupAffectedSnaps(task *state.Task, affectingSnap st
if err := addImplicitSlots(st, affectedSnapInfo); err != nil {
return err
}
opts := confinementOptions(snapst.Flags)
opts, err := buildConfinementOptions(st, affectedSnapInfo.InstanceName(), snapst.Flags)
if err != nil {
return err
}
if err := m.setupSnapSecurity(task, affectedSnapInfo, opts, tm); err != nil {
return err
}
Expand Down Expand Up @@ -115,7 +161,10 @@ func (m *InterfaceManager) doSetupProfiles(task *state.Task, tomb *tomb.Tomb) er
return nil
}

opts := confinementOptions(snapsup.Flags)
opts, err := buildConfinementOptions(task.State(), snapInfo.InstanceName(), snapsup.Flags)
if err != nil {
return err
}
return m.setupProfilesForSnap(task, tomb, snapInfo, opts, perfTimings)
}

Expand Down Expand Up @@ -205,8 +254,13 @@ func (m *InterfaceManager) setupProfilesForSnap(task *state.Task, _ *tomb.Tomb,
if err := addImplicitSlots(st, snapInfo); err != nil {
return err
}
opts, err := buildConfinementOptions(st, snapInfo.InstanceName(), snapst.Flags)
if err != nil {
return err
}

affectedSnaps = append(affectedSnaps, snapInfo)
confinementOpts = append(confinementOpts, confinementOptions(snapst.Flags))
confinementOpts = append(confinementOpts, opts)
}

return m.setupSecurityByBackend(task, affectedSnaps, confinementOpts, tm)
Expand Down Expand Up @@ -297,7 +351,10 @@ func (m *InterfaceManager) undoSetupProfiles(task *state.Task, tomb *tomb.Tomb)
if err != nil {
return err
}
opts := confinementOptions(snapst.Flags)
opts, err := buildConfinementOptions(task.State(), snapInfo.InstanceName(), snapst.Flags)
if err != nil {
return err
}
return m.setupProfilesForSnap(task, tomb, snapInfo, opts, perfTimings)
}
}
Expand Down Expand Up @@ -499,12 +556,18 @@ func (m *InterfaceManager) doConnect(task *state.Task, _ *tomb.Tomb) (err error)
}()

if !delayedSetupProfiles {
slotOpts := confinementOptions(slotSnapst.Flags)
slotOpts, err := buildConfinementOptions(st, slotSnapst.InstanceName(), slotSnapst.Flags)
if err != nil {
return err
}
if err := m.setupSnapSecurity(task, slot.Snap, slotOpts, perfTimings); err != nil {
return err
}

plugOpts := confinementOptions(plugSnapst.Flags)
plugOpts, err := buildConfinementOptions(st, plugSnapst.InstanceName(), plugSnapst.Flags)
if err != nil {
return err
}
if err := m.setupSnapSecurity(task, plug.Snap, plugOpts, perfTimings); err != nil {
return err
}
Expand Down Expand Up @@ -605,7 +668,10 @@ func (m *InterfaceManager) doDisconnect(task *state.Task, _ *tomb.Tomb) error {
if err != nil {
return err
}
opts := confinementOptions(snapst.Flags)
opts, err := buildConfinementOptions(st, snapInfo.InstanceName(), snapst.Flags)
if err != nil {
return err
}
if err := m.setupSnapSecurity(task, snapInfo, opts, perfTimings); err != nil {
return err
}
Expand Down Expand Up @@ -711,11 +777,18 @@ func (m *InterfaceManager) undoDisconnect(task *state.Task, _ *tomb.Tomb) error
return err
}

slotOpts := confinementOptions(slotSnapst.Flags)
slotOpts, err := buildConfinementOptions(st, slotSnapst.InstanceName(), slotSnapst.Flags)
if err != nil {
return err
}
if err := m.setupSnapSecurity(task, slot.Snap, slotOpts, perfTimings); err != nil {
return err
}
plugOpts := confinementOptions(plugSnapst.Flags)

plugOpts, err := buildConfinementOptions(st, plugSnapst.InstanceName(), plugSnapst.Flags)
if err != nil {
return err
}
if err := m.setupSnapSecurity(task, plug.Snap, plugOpts, perfTimings); err != nil {
return err
}
Expand Down Expand Up @@ -794,11 +867,19 @@ func (m *InterfaceManager) undoConnect(task *state.Task, _ *tomb.Tomb) error {
if err != nil {
return err
}
slotOpts := confinementOptions(slotSnapst.Flags)

slotOpts, err := buildConfinementOptions(st, slotSnapst.InstanceName(), slotSnapst.Flags)
if err != nil {
return err
}
if err := m.setupSnapSecurity(task, slot.Snap, slotOpts, perfTimings); err != nil {
return err
}
plugOpts := confinementOptions(plugSnapst.Flags)

plugOpts, err := buildConfinementOptions(st, plugSnapst.InstanceName(), plugSnapst.Flags)
if err != nil {
return err
}
if err := m.setupSnapSecurity(task, plug.Snap, plugOpts, perfTimings); err != nil {
return err
}
Expand Down
123 changes: 101 additions & 22 deletions overlord/ifacestate/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,49 @@
package ifacestate_test

import (
"path"

. "gopkg.in/check.v1"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/overlord/configstate/config"
"github.com/snapcore/snapd/overlord/ifacestate"
"github.com/snapcore/snapd/overlord/servicestate/servicestatetest"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/quota"
"github.com/snapcore/snapd/snap/snaptest"
)

type handlersSuite struct{}
const snapAyaml = `name: snap-a
type: app
base: base-snap-a
`

type handlersSuite struct {
st *state.State
}

var _ = Suite(&handlersSuite{})

func (s *handlersSuite) SetUpTest(c *C) {
s.st = state.New(nil)
dirs.SetRootDir(c.MkDir())
}

func (s *handlersSuite) TearDownTest(c *C) {
dirs.SetRootDir("")
}

func (s *handlersSuite) TestInSameChangeWaitChain(c *C) {
st := state.New(nil)
st.Lock()
defer st.Unlock()
s.st.Lock()
defer s.st.Unlock()

// no wait chain (yet)
startT := st.NewTask("start", "...start")
intermediateT := st.NewTask("intermediateT", "...intermediateT")
searchT := st.NewTask("searchT", "...searchT")
startT := s.st.NewTask("start", "...start")
intermediateT := s.st.NewTask("intermediateT", "...intermediateT")
searchT := s.st.NewTask("searchT", "...searchT")
c.Check(ifacestate.InSameChangeWaitChain(startT, searchT), Equals, false)

// add (indirect) wait chain
Expand All @@ -48,16 +72,15 @@ func (s *handlersSuite) TestInSameChangeWaitChain(c *C) {
}

func (s *handlersSuite) TestInSameChangeWaitChainDifferentChanges(c *C) {
st := state.New(nil)
st.Lock()
defer st.Unlock()
s.st.Lock()
defer s.st.Unlock()

t1 := st.NewTask("t1", "...")
chg1 := st.NewChange("chg1", "...")
t1 := s.st.NewTask("t1", "...")
chg1 := s.st.NewChange("chg1", "...")
chg1.AddTask(t1)

t2 := st.NewTask("t2", "...")
chg2 := st.NewChange("chg2", "...")
t2 := s.st.NewTask("t2", "...")
chg2 := s.st.NewChange("chg2", "...")
chg2.AddTask(t2)

// add a cross change wait chain
Expand All @@ -66,23 +89,79 @@ func (s *handlersSuite) TestInSameChangeWaitChainDifferentChanges(c *C) {
}

func (s *handlersSuite) TestInSameChangeWaitChainWithCycles(c *C) {
st := state.New(nil)
st.Lock()
defer st.Unlock()
s.st.Lock()
defer s.st.Unlock()

// cycles like this are unexpected in practice but are easier to test than
// the exponential paths situation that e.g. seed changes present.
startT := st.NewTask("start", "...start")
task1 := st.NewTask("task1", "...")
startT := s.st.NewTask("start", "...start")
task1 := s.st.NewTask("task1", "...")
task1.WaitFor(startT)
task2 := st.NewTask("task2", "...")
task2 := s.st.NewTask("task2", "...")
task2.WaitFor(task1)
task3 := st.NewTask("task3", "...")
task3 := s.st.NewTask("task3", "...")
task3.WaitFor(task2)

startT.WaitFor(task2)
startT.WaitFor(task3)

unrelated := st.NewTask("unrelated", "...")
unrelated := s.st.NewTask("unrelated", "...")
c.Check(ifacestate.InSameChangeWaitChain(startT, unrelated), Equals, false)
}

func mockInstalledSnap(c *C, st *state.State, snapYaml string) *snap.Info {
snapInfo := snaptest.MockSnap(c, snapYaml, &snap.SideInfo{
Revision: snap.R(1),
})

snapName := snapInfo.SnapName()
si := &snap.SideInfo{RealName: snapName, SnapID: snapName + "-id", Revision: snap.R(1)}
snapstate.Set(st, snapName, &snapstate.SnapState{
Active: true,
Sequence: []*snap.SideInfo{si},
Current: si.Revision,
SnapType: string(snapInfo.Type()),
})
return snapInfo
}

func (s *handlersSuite) TestBuildConfinementOptions(c *C) {
s.st.Lock()
defer s.st.Unlock()

snapInfo := mockInstalledSnap(c, s.st, snapAyaml)
flags := snapstate.Flags{}
opts, err := ifacestate.BuildConfinementOptions(s.st, snapInfo.InstanceName(), snapstate.Flags{})

c.Check(err, IsNil)
c.Check(len(opts.ExtraLayouts), Equals, 0)
c.Check(opts.Classic, Equals, flags.Classic)
c.Check(opts.DevMode, Equals, flags.DevMode)
c.Check(opts.JailMode, Equals, flags.JailMode)
}

func (s *handlersSuite) TestBuildConfinementOptionsWithLogNamespace(c *C) {
s.st.Lock()
defer s.st.Unlock()

tr := config.NewTransaction(s.st)
tr.Set("core", "experimental.quota-groups", true)
tr.Commit()

snapInfo := mockInstalledSnap(c, s.st, snapAyaml)

// Create a new quota group with a journal quota
err := servicestatetest.MockQuotaInState(s.st, "foo", "", []string{snapInfo.InstanceName()}, quota.NewResourcesBuilder().WithJournalNamespace().Build())
c.Assert(err, IsNil)

flags := snapstate.Flags{}
opts, err := ifacestate.BuildConfinementOptions(s.st, snapInfo.InstanceName(), snapstate.Flags{})

c.Check(err, IsNil)
c.Assert(len(opts.ExtraLayouts), Equals, 1)
c.Check(opts.ExtraLayouts[0].Bind, Equals, path.Join(dirs.SnapSystemdDir, "journal.snap-foo"))
c.Check(opts.ExtraLayouts[0].Path, Equals, path.Join(dirs.SnapSystemdDir, "journal"))
c.Check(opts.Classic, Equals, flags.Classic)
c.Check(opts.DevMode, Equals, flags.DevMode)
c.Check(opts.JailMode, Equals, flags.JailMode)
}
6 changes: 5 additions & 1 deletion overlord/ifacestate/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles(tm timings.Measurer) er
if err := snapstate.Get(m.state, snapName, &snapst); err != nil {
logger.Noticef("cannot get state of snap %q: %s", snapName, err)
}
return confinementOptions(snapst.Flags)
opts, err := buildConfinementOptions(m.state, snapst.InstanceName(), snapst.Flags)
if err != nil {
logger.Noticef("cannot get confinement options for snap %q: %s", snapName, err)
}
return opts
}

// For each backend:
Expand Down

0 comments on commit 5d514ae

Please sign in to comment.