Skip to content

Commit

Permalink
wrappers: support for creating journal config files
Browse files Browse the repository at this point in the history
  • Loading branch information
Meulengracht committed May 23, 2022
1 parent e0b211d commit 13021f8
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 20 deletions.
58 changes: 38 additions & 20 deletions wrappers/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func formatLogNamespaceSlice(grp *quota.Group) string {
if grp.JournalLimit == nil {
return ""
}
return fmt.Sprintf("LogNamespace=%s\n", grp.Name)
return fmt.Sprintf("LogNamespace=snap-%s\n", grp.Name)
}

// generateGroupSliceFile generates a systemd slice unit definition for the
Expand Down Expand Up @@ -171,16 +171,18 @@ func formatJournalSizeConf(grp *quota.Group) string {
if grp.JournalLimit.Size == 0 {
return ""
}
return fmt.Sprintf("SystemMaxUse=%d\n", grp.JournalLimit.Size)
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=%d
return fmt.Sprintf(`RateLimitIntervalSec=%ds
RateLimitBurst=%d
`, grp.JournalLimit.RateCount, grp.JournalLimit.RatePeriod)
`, grp.JournalLimit.RatePeriod, grp.JournalLimit.RateCount)
}

func generateJournaldConfFile(grp *quota.Group) []byte {
Expand Down Expand Up @@ -529,6 +531,7 @@ type ensureSnapServicesContext struct {
sysd systemd.Systemd
systemDaemonReloadNeeded bool
userDaemonReloadNeeded bool
journalCtlReloadNeeded bool
// modifiedUnits is the set of units that were modified and the previous
// state of the unit before modification that we can roll back to if there
// are any issues.
Expand Down Expand Up @@ -569,6 +572,11 @@ func (es *ensureSnapServicesContext) restore() {
es.inter.Notify(fmt.Sprintf("while trying to perform user systemd daemon-reload due to previous failure: %v", err))
}
}
if es.journalCtlReloadNeeded {
if e := es.sysd.Restart([]string{"systemd-journald.service"}); e != nil {
es.inter.Notify(fmt.Sprintf("while trying to restart systemd-journald due to previous failure: %v", e))
}
}
}
}

Expand All @@ -589,6 +597,11 @@ func (es *ensureSnapServicesContext) reloadModified() error {
if err := userDaemonReload(); err != nil {
return err
}
if es.journalCtlReloadNeeded {
if err := es.sysd.Restart([]string{"systemd-journald.service"}); err != nil {
return err
}
}
}
return nil
}
Expand Down Expand Up @@ -747,40 +760,42 @@ func (es *ensureSnapServicesContext) ensureSnapSlices(quotaGroups *quota.QuotaGr
return nil
}

func ensureSnapJournaldUnits(context *ensureSnapServicesContext, quotaGroups *quota.QuotaGroupSet) error {
handleSliceModification := func(grp *quota.Group, path string, content []byte) error {
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 {
if context.observeChange != 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
}
context.observeChange(nil, grp, "slice", grp.Name, string(oldContent), string(content))
es.observeChange(nil, grp, "journald", grp.Name, string(oldContent), string(content))
}

context.modifiedFiles[path] = old

// also mark that we need to reload the system instance of systemd
// TODO: also handle reloading the user instance of systemd when
// needed
context.modifiedSystem = true
es.modifiedUnits[path] = old
es.journalCtlReloadNeeded = true
}

return nil
}

// now make sure that all of the slice units exist
for _, grp := range quotaGroups.AllQuotaGroups() {
contents := generateJournaldConfFile(grp)
journalConfFileName := grp.JournalFileName()
fileName := grp.JournalFileName()

journalConfPath := filepath.Join(dirs.SnapSystemdDir, journalConfFileName)
if err := handleSliceModification(grp, journalConfPath, contents); err != nil {
journalConfPath := filepath.Join(dirs.SnapSystemdDir, fileName)
if err := handleJournalModification(grp, journalConfPath, contents); err != nil {
return err
}
}
Expand Down Expand Up @@ -830,8 +845,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
215 changes: 215 additions & 0 deletions wrappers/services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,221 @@ 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"},
{"stop", "systemd-journald.service"},
{"show", "--property=ActiveState", "systemd-journald.service"},
{"start", "systemd-journald.service"},
})

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).
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=5s
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"},
{"stop", "systemd-journald.service"},
{"show", "--property=ActiveState", "systemd-journald.service"},
{"start", "systemd-journald.service"},
})

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

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

0 comments on commit 13021f8

Please sign in to comment.