Skip to content

Commit

Permalink
Merge pull request #11770 from Meulengracht/feature/journal-quota-3
Browse files Browse the repository at this point in the history
wrappers: write journald config files for quota groups with journal quotas (3/n)
  • Loading branch information
mvo5 committed Jun 7, 2022
2 parents 13fc224 + d9be580 commit 9d2958f
Show file tree
Hide file tree
Showing 3 changed files with 301 additions and 3 deletions.
2 changes: 2 additions & 0 deletions dirs/dirs.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ var (
SnapDesktopFilesDir string
SnapDesktopIconsDir string
SnapPolkitPolicyDir string
SnapSystemdDir string

SnapDBusSessionPolicyDir string
SnapDBusSystemPolicyDir string
Expand Down Expand Up @@ -420,6 +421,7 @@ func SetRootDir(rootdir string) {
SnapRuntimeServicesDir = filepath.Join(rootdir, "/run/systemd/system")
SnapUserServicesDir = filepath.Join(rootdir, "/etc/systemd/user")
SnapSystemdConfDir = SnapSystemdConfDirUnder(rootdir)
SnapSystemdDir = filepath.Join(rootdir, "/etc/systemd")

SnapDBusSystemPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/system.d")
SnapDBusSessionPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/session.d")
Expand Down
93 changes: 90 additions & 3 deletions wrappers/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ TasksAccounting=true
return buf.String()
}

func formatLogNamespaceSlice(grp *quota.Group) string {
if grp.JournalLimit == nil {
return ""
}
return fmt.Sprintf("LogNamespace=snap-%s\n", grp.Name)
}

// generateGroupSliceFile generates a systemd slice unit definition for the
// specified quota group.
func generateGroupSliceFile(grp *quota.Group) []byte {
Expand All @@ -146,6 +153,7 @@ func generateGroupSliceFile(grp *quota.Group) []byte {
cpuOptions := formatCpuGroupSlice(grp)
memoryOptions := formatMemoryGroupSlice(grp)
taskOptions := formatTaskGroupSlice(grp)
logOptions := formatLogNamespaceSlice(grp)
template := `[Unit]
Description=Slice for snap quota group %[1]s
Before=slices.target
Expand All @@ -155,7 +163,41 @@ X-Snappy=yes
`

fmt.Fprintf(&buf, template, grp.Name)
fmt.Fprint(&buf, cpuOptions, memoryOptions, taskOptions)
fmt.Fprint(&buf, cpuOptions, memoryOptions, taskOptions, logOptions)
return buf.Bytes()
}

func formatJournalSizeConf(grp *quota.Group) string {
if grp.JournalLimit.Size == 0 {
return ""
}
return fmt.Sprintf(`SystemMaxUse=%[1]d
RuntimeMaxUse=%[1]d
`, grp.JournalLimit.Size)
}

func formatJournalRateConf(grp *quota.Group) string {
if grp.JournalLimit.RateCount == 0 || grp.JournalLimit.RatePeriod == 0 {
return ""
}
return fmt.Sprintf(`RateLimitIntervalSec=%dus
RateLimitBurst=%d
`, grp.JournalLimit.RatePeriod.Microseconds(), grp.JournalLimit.RateCount)
}

func generateJournaldConfFile(grp *quota.Group) []byte {
if grp.JournalLimit == nil {
return nil
}

sizeOptions := formatJournalSizeConf(grp)
rateOptions := formatJournalRateConf(grp)
template := `# Journald configuration for snap quota group %[1]s
[Journal]
`
buf := bytes.Buffer{}
fmt.Fprintf(&buf, template, grp.Name)
fmt.Fprint(&buf, sizeOptions, rateOptions)
return buf.Bytes()
}

Expand Down Expand Up @@ -695,6 +737,48 @@ func (es *ensureSnapServicesContext) ensureSnapSlices(quotaGroups *quota.QuotaGr
return nil
}

func (es *ensureSnapServicesContext) ensureSnapJournaldUnits(quotaGroups *quota.QuotaGroupSet) error {
handleJournalModification := func(grp *quota.Group, path string, content []byte) error {
old, modifiedFile, err := tryFileUpdate(path, content)
if err != nil {
return err
}

if !modifiedFile {
return nil
}

// suppress any event and restart if we actually did not do anything
// as it seems modifiedFile is set even when the file does not exist
// and when the new content is nil.
if (old == nil || len(old.Content) == 0) && len(content) == 0 {
return nil
}

if es.observeChange != nil {
var oldContent []byte
if old != nil {
oldContent = old.Content
}
es.observeChange(nil, grp, "journald", grp.Name, string(oldContent), string(content))
}

es.modifiedUnits[path] = old
return nil
}

for _, grp := range quotaGroups.AllQuotaGroups() {
contents := generateJournaldConfFile(grp)
fileName := grp.JournalFileName()

path := filepath.Join(dirs.SnapSystemdDir, fileName)
if err := handleJournalModification(grp, path, contents); err != nil {
return err
}
}
return nil
}

// EnsureSnapServices will ensure that the specified snap services' file states
// are up to date with the specified options and infos. It will add new services
// if those units don't already exist, but it does not delete existing service
Expand Down Expand Up @@ -738,8 +822,11 @@ func EnsureSnapServices(snaps map[*snap.Info]*SnapServiceOptions, opts *EnsureSn
return err
}

err = context.ensureSnapSlices(quotaGroups)
if err != nil {
if err := context.ensureSnapSlices(quotaGroups); err != nil {
return err
}

if err := context.ensureSnapJournaldUnits(quotaGroups); err != nil {
return err
}

Expand Down
209 changes: 209 additions & 0 deletions wrappers/services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,215 @@ TasksAccounting=true
c.Assert(svcFile, testutil.FileEquals, svcContent)
}

func (s *servicesTestSuite) TestEnsureSnapServicesWithJournalNamespaceOnly(c *C) {
// Ensure that the journald.conf file is correctly written
info := snaptest.MockSnap(c, packageHello, &snap.SideInfo{Revision: snap.R(12)})
svcFile := filepath.Join(s.tempdir, "/etc/systemd/system/snap.hello-snap.svc1.service")

// set up arbitrary quotas for the group to test they get written correctly to the slice
resourceLimits := quota.NewResourcesBuilder().
WithJournalNamespace().
Build()
grp, err := quota.NewGroup("foogroup", resourceLimits)
c.Assert(err, IsNil)

m := map[*snap.Info]*wrappers.SnapServiceOptions{
info: {QuotaGroup: grp},
}

dir := filepath.Join(dirs.SnapMountDir, "hello-snap", "12.mount")
svcContent := fmt.Sprintf(`[Unit]
# Auto-generated, DO NOT EDIT
Description=Service for snap application hello-snap.svc1
Requires=%[1]s
Wants=network.target
After=%[1]s network.target snapd.apparmor.service
X-Snappy=yes
[Service]
EnvironmentFile=-/etc/environment
ExecStart=/usr/bin/snap run hello-snap.svc1
SyslogIdentifier=hello-snap.svc1
Restart=on-failure
WorkingDirectory=%[2]s/var/snap/hello-snap/12
ExecStop=/usr/bin/snap run --command=stop hello-snap.svc1
ExecStopPost=/usr/bin/snap run --command=post-stop hello-snap.svc1
TimeoutStopSec=30
Type=forking
Slice=snap.foogroup.slice
[Install]
WantedBy=multi-user.target
`,
systemd.EscapeUnitNamePath(dir),
dirs.GlobalRootDir,
)
jconfTempl := `# Journald configuration for snap quota group %s
[Journal]
`

sliceTempl := `[Unit]
Description=Slice for snap quota group %s
Before=slices.target
X-Snappy=yes
[Slice]
# Always enable cpu accounting, so the following cpu quota options have an effect
CPUAccounting=true
# Always enable memory accounting otherwise the MemoryMax setting does nothing.
MemoryAccounting=true
# Always enable task accounting in order to be able to count the processes/
# threads, etc for a slice
TasksAccounting=true
LogNamespace=snap-foogroup
`

jconfContent := fmt.Sprintf(jconfTempl, grp.Name)
sliceContent := fmt.Sprintf(sliceTempl, grp.Name)

exp := []changesObservation{
{
grp: grp,
unitType: "journald",
new: jconfContent,
old: "",
name: "foogroup",
},
{
snapName: "hello-snap",
unitType: "service",
name: "svc1",
old: "",
new: svcContent,
},
{
grp: grp,
unitType: "slice",
new: sliceContent,
old: "",
name: "foogroup",
},
}
r, observe := expChangeObserver(c, exp)
defer r()

err = wrappers.EnsureSnapServices(m, nil, observe, progress.Null)
c.Assert(err, IsNil)
c.Check(s.sysdLog, DeepEquals, [][]string{
{"daemon-reload"},
})

c.Assert(svcFile, testutil.FileEquals, svcContent)
}

func (s *servicesTestSuite) TestEnsureSnapServicesWithJournalQuotas(c *C) {
// Ensure that the journald.conf file is correctly written
info := snaptest.MockSnap(c, packageHello, &snap.SideInfo{Revision: snap.R(12)})
svcFile := filepath.Join(s.tempdir, "/etc/systemd/system/snap.hello-snap.svc1.service")

// set up arbitrary quotas for the group to test they get written correctly to the slice
resourceLimits := quota.NewResourcesBuilder().
WithJournalSize(10*quantity.SizeMiB).
WithJournalRate(15, 5*time.Second).
Build()
grp, err := quota.NewGroup("foogroup", resourceLimits)
c.Assert(err, IsNil)

m := map[*snap.Info]*wrappers.SnapServiceOptions{
info: {QuotaGroup: grp},
}

dir := filepath.Join(dirs.SnapMountDir, "hello-snap", "12.mount")
svcContent := fmt.Sprintf(`[Unit]
# Auto-generated, DO NOT EDIT
Description=Service for snap application hello-snap.svc1
Requires=%[1]s
Wants=network.target
After=%[1]s network.target snapd.apparmor.service
X-Snappy=yes
[Service]
EnvironmentFile=-/etc/environment
ExecStart=/usr/bin/snap run hello-snap.svc1
SyslogIdentifier=hello-snap.svc1
Restart=on-failure
WorkingDirectory=%[2]s/var/snap/hello-snap/12
ExecStop=/usr/bin/snap run --command=stop hello-snap.svc1
ExecStopPost=/usr/bin/snap run --command=post-stop hello-snap.svc1
TimeoutStopSec=30
Type=forking
Slice=snap.foogroup.slice
[Install]
WantedBy=multi-user.target
`,
systemd.EscapeUnitNamePath(dir),
dirs.GlobalRootDir,
)
jconfTempl := `# Journald configuration for snap quota group %s
[Journal]
SystemMaxUse=10485760
RuntimeMaxUse=10485760
RateLimitIntervalSec=5000000us
RateLimitBurst=15
`

sliceTempl := `[Unit]
Description=Slice for snap quota group %s
Before=slices.target
X-Snappy=yes
[Slice]
# Always enable cpu accounting, so the following cpu quota options have an effect
CPUAccounting=true
# Always enable memory accounting otherwise the MemoryMax setting does nothing.
MemoryAccounting=true
# Always enable task accounting in order to be able to count the processes/
# threads, etc for a slice
TasksAccounting=true
LogNamespace=snap-foogroup
`

jconfContent := fmt.Sprintf(jconfTempl, grp.Name)
sliceContent := fmt.Sprintf(sliceTempl, grp.Name)

exp := []changesObservation{
{
grp: grp,
unitType: "journald",
new: jconfContent,
old: "",
name: "foogroup",
},
{
snapName: "hello-snap",
unitType: "service",
name: "svc1",
old: "",
new: svcContent,
},
{
grp: grp,
unitType: "slice",
new: sliceContent,
old: "",
name: "foogroup",
},
}
r, observe := expChangeObserver(c, exp)
defer r()

err = wrappers.EnsureSnapServices(m, nil, observe, progress.Null)
c.Assert(err, IsNil)
c.Check(s.sysdLog, DeepEquals, [][]string{
{"daemon-reload"},
})

c.Assert(svcFile, testutil.FileEquals, svcContent)
}

type changesObservation struct {
snapName string
grp *quota.Group
Expand Down

0 comments on commit 9d2958f

Please sign in to comment.