snapstate: add support for refresh.schedule=managed #4161

Merged
merged 36 commits into from Nov 29, 2017

Conversation

Projects
None yet
6 participants
Collaborator

mvo5 commented Nov 7, 2017

This PR allows setting refresh.schedule: managed if we have a connected plug on the system for the snapd-control interface with the refresh-schedule: managed property.

This is got get a first review, the PR needs some more work, i.e. it needs to contact the store for refreshes (even if we not act on them) so that local warnings can be issued in the future.

mvo5 added some commits Oct 30, 2017

interfaces: add "refresh-schedule" attribute to snapd-control
Add a "refresh-schedule" attribute to the snapd-control interface
that can be set to "managed".
snapstate: add support for "refresh.schedule=managed" via core config
If there is a connected plug that has the snapd-control interfaces
and the "refresh-schedule: managed" attribute, then snapmanager
now allows the refresh schedule to be managed externally.

@mvo5 mvo5 added this to the 2.30 milestone Nov 7, 2017

some initial comments

interfaces/builtin/snapd_control.go
const snapdControlConnectedPlugAppArmor = `
# Description: Can manage snaps via snapd.
/run/snapd.socket rw,
`
+type snapControlInterface struct {
+ commonInterface
@pedronis

pedronis Nov 9, 2017

Contributor

afaict seems we haven't used this kind of embedding pattern so far, wondering if there's a preference, I suppose it's a question for @zyga

@zyga

zyga Nov 13, 2017

Contributor

I noticed this and I was thinking about it myself. While we haven't done this before I don't think it is a bad practice myself (unless there's something golang-y that I'm not familiar with). Doing it would allow us to simplify a few interfaces that just need to override one method and are happy with the semantics of common interface for others.

@bboozzoo

bboozzoo Nov 13, 2017

Contributor

FWIW it's used in #4185 too

interfaces/builtin/snapd_control.go
+
+ refreshSchedule, ok := plug.Attrs["refresh-schedule"].(string)
+ if ok {
+ if refreshSchedule != "" && refreshSchedule != "managed" {
@pedronis

pedronis Nov 9, 2017

Contributor

I wouldn't accept refresh-schedule to be set to an empty "" string, it seems strange (easier to leave it just out) and it's not a pattern that our plug/slot rule language supports well

overlord/snapstate/snapmgr.go
@@ -489,6 +498,7 @@ func (m *SnapManager) NextRefresh() time.Time {
// RefreshSchedule returns the current refresh schedule.
// The caller should be holding the state lock.
func (m *SnapManager) RefreshSchedule() string {
+ m.checkRefreshSchedule()
@pedronis

pedronis Nov 9, 2017

Contributor

are we sure we can ignore errors here? anyway I think the one place using this could survive this returning an error, it's not used all over the place

overlord/snapstate/snapmgr.go
+ // the snap must come from the store
+ // FIXME: should we use
+ // assertstate.SnapDeclaration(info.SideInfo.SnapID)
+ // here?
@pedronis

pedronis Nov 9, 2017

Contributor

yes, it's a stronger check that makes sense here

+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-control-consumer
+
+ # now pretend test-snapd-control-consumer comes from the store (fugly HACK)
@pedronis

pedronis Nov 9, 2017

Contributor

we could use the fakestore here instead (it could help also with setting up assertions), it's a bit of work though

Collaborator

mvo5 commented Nov 10, 2017

We also need to revert the emergency weekly refresh timer

overlord/snapstate/snapmgr.go
+ if refreshScheduleStr != "managed" {
+ return false
+ }
+ snapStates, err := All(m.state)
@pedronis

pedronis Nov 13, 2017

Contributor

I think everything from here and below belongs to an ifacestate helper that gets hooked into snapstate

@mvo5

mvo5 Nov 14, 2017

Collaborator

Thanks, I pushed a variant of your suggestion. It seems like a device property if the refreshes are managed or not so I moved it into devicestate and hook from there. But happy to go with your original suggestion if you feel strongly about it. I also moved to us snapdeclaration and started the store hints (in a separate branch in https://github.com/snapcore/snapd/compare/master...mvo5:refresh-candidates-managed?expand=1.

@pedronis

pedronis Nov 14, 2017

Contributor

no, that seems reasonable as well (devicestate)

Some comments for your consideration.

interfaces/builtin/common.go
@@ -87,6 +87,12 @@ func (iface *commonInterface) SanitizeSlot(slot *interfaces.Slot) error {
return nil
}
+// SanitizePlug checks and possibly modifies a plug.
@zyga

zyga Nov 13, 2017

Contributor

Why do you add an empty sanitize helper if those are optional?

@mvo5

mvo5 Nov 14, 2017

Collaborator

It is here to make things future proof, but your question is valid and YAGNI etc - so I think its fine to remove it as long as we remember that if we add SanitizePlug to commonInterface we need to look over the existing commonInterface embedders.

interfaces/builtin/snapd_control.go
+}
+
+func (iface *snapControlInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if err := iface.commonInterface.SanitizePlug(plug); err != nil {
@zyga

zyga Nov 13, 2017

Contributor

Aha, for future-proofing, I assume. In such case can you comment in the common code so that things are clear. Alternatively just don't do this :)

@mvo5

mvo5 Nov 14, 2017

Collaborator

I removed it. I'm a strong believer in YAGNI.

interfaces/builtin/snapd_control.go
+ return err
+ }
+
+ refreshSchedule, ok := plug.Attrs["refresh-schedule"].(string)
@zyga

zyga Nov 13, 2017

Contributor

You can merge this with the next line.

overlord/snapstate/snapmgr.go
@@ -389,8 +390,15 @@ func refreshScheduleNoWeekdays(rs []*timeutil.Schedule) error {
}
func (m *SnapManager) checkRefreshSchedule() ([]*timeutil.Schedule, error) {
- refreshScheduleStr := defaultRefreshSchedule
+ if m.refreshScheduleManaged() {
+ if m.currentRefreshSchedule != "managed" {
@zyga

zyga Nov 13, 2017

Contributor

Curious if this should exclude ability to manually manage a system. What do you think?

@mvo5

mvo5 Nov 14, 2017

Collaborator

Interessting idea. But also a wider scope than this PR :)

overlord/snapstate/snapstate_test.go
+ tr.Set("core", "refresh.schedule", "managed")
+ tr.Commit()
+
+ // using MockSnap, we want to read the bits on disk
@zyga

zyga Nov 13, 2017

Contributor

This comment is a bit weird. Are we using MockSnap or MockReadInfo?

@mvo5

mvo5 Nov 14, 2017

Collaborator

I updated the comment now, there are a bunch of places this is copyied^Wcargo-culted^Wused, if we are happy with the update text I can fix it in all places (probably in a separate PR).

@pedronis pedronis added the Blocked label Nov 13, 2017

codecov-io commented Nov 17, 2017

Codecov Report

Merging #4161 into master will increase coverage by 0.03%.
The diff coverage is 54.66%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4161      +/-   ##
==========================================
+ Coverage   76.17%   76.21%   +0.03%     
==========================================
  Files         443      445       +2     
  Lines       38591    38837     +246     
==========================================
+ Hits        29398    29600     +202     
- Misses       7174     7214      +40     
- Partials     2019     2023       +4
Impacted Files Coverage Δ
overlord/ifacestate/ifacemgr.go 93.75% <ø> (-0.13%) ⬇️
corecfg/refresh.go 36.84% <0%> (-33.16%) ⬇️
daemon/api.go 72.02% <0%> (-0.08%) ⬇️
interfaces/builtin/snapd_control.go 100% <100%> (ø) ⬆️
overlord/snapstate/refreshhints.go 73.33% <100%> (+0.6%) ⬆️
cmd/snap/cmd_can_manage_refreshes.go 44.44% <44.44%> (ø)
overlord/snapstate/autorefresh.go 77.27% <64.7%> (-1.72%) ⬇️
overlord/devicestate/devicestate.go 73.96% <72%> (-0.35%) ⬇️
overlord/hookstate/ctlcmd/set.go 81.7% <0%> (-1.35%) ⬇️
store/store.go 80.13% <0%> (-0.08%) ⬇️
... and 17 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 00f25d7...fe8bc54. Read the comment docs.

data/systemd/snapd.refresh.service.in
@@ -7,5 +7,5 @@ Documentation=man:snap(1)
[Service]
Type=oneshot
-ExecStart=@bindir@/snap refresh
+ExecStart=/bin/sh -c 'if ! @bindir@/snap refresh --list|grep "schedule:.*managed"; then @bindir@/snap refresh; fi'
@pedronis

pedronis Nov 17, 2017

Contributor

that should be --time, not --list, no?

@mvo5

mvo5 Nov 17, 2017

Collaborator

Yes and it shows this needs some sort of test :/

mvo5 added some commits Nov 17, 2017

snapstate: ask store for refresh-hints every 24h
We currently do not use the refresh hints but once the warning
framework is in place we can use this information to do smart
stuff like warn about important refreshes etc.

looking good, a few comments more though, mostly about tests

overlord/devicestate/devicestate.go
@@ -223,3 +225,60 @@ func ProxyStore(st *state.State) (*asserts.Store, error) {
return a.(*asserts.Store), nil
}
+
+func plugConnected(st *state.State, snapName, plugName string) bool {
@pedronis

pedronis Nov 17, 2017

Contributor

seems this def should be directly before or after CanSetRefreshScheduleManaged

overlord/devicestate/devicestate_test.go
@@ -1929,3 +1932,78 @@ func (s *deviceMgrSuite) TestCanAutoRefreshOnClassic(c *C) {
s.state.Set("seeded", false)
c.Check(canAutoRefresh(), Equals, false)
}
+
+func (s *deviceMgrSuite) TestRefreshControlManaged(c *C) {
@pedronis

pedronis Nov 17, 2017

Contributor

does CanSetRefreshScheduleManaged merit its separate unit tests now?

overlord/devicestate/devicestate_test.go
+ err = assertstate.Add(s.state, snapdWithSnapControlDecl)
+ c.Assert(err, IsNil)
+
+ c.Check(devicestate.RefreshScheduleManaged(st), Equals, true)
@pedronis

pedronis Nov 17, 2017

Contributor

would probably be good to check some of the false cases too or maybe those would go with the CanSetRefreshScheduleManaged tests

overlord/snapstate/snapmgr.go
@@ -491,6 +503,9 @@ func (m *SnapManager) NextRefresh() time.Time {
// RefreshSchedule returns the current refresh schedule.
// The caller should be holding the state lock.
func (m *SnapManager) RefreshSchedule() string {
+ // This call ensures "m.currentRefreshSchedule" is up-to-date
+ // with the latest configuration settings.
+ m.checkRefreshSchedule()
@pedronis

pedronis Nov 17, 2017

Contributor

should we return the error from this? I would still think yes

@mvo5

mvo5 Nov 21, 2017

Collaborator

I think this shows that the code here is a bit problematic and need a slight refactor. I'm not sure yet about the "how" but the pattern above smells funny and the fact that there is a comment to explain the intend does not make it better. I will try to come up with something better.

I think the last bit is problematic

overlord/snapstate/snapmgr.go
+ // check for refresh hints from the store every 24h
+ // to be able to warn in the future
+ var lastRefreshHints time.Time
+ if err := m.state.Get("last-refresh-hints", &lastRefreshHints); err != nil && err != state.ErrNoState {
@pedronis

pedronis Nov 17, 2017

Contributor

two comments,

  1. as far as I understand we always want to do this (because the schedule can be slower than once a day even in other cases)
  2. this is a strange place to do this anyway in terms of sane error reporting among other things (for example that we call this from the accessor)
@mvo5

mvo5 Nov 21, 2017

Collaborator

Thanks, I misunderstood and was not aware of (1). Fixed now. And agreed to (2), I moved it but want to look at this a bit harder to see if the existing code can be written in a slightly cleaner way (its a bit entangled right now).

Contributor

bboozzoo commented Nov 20, 2017

some go vet failures;

overlord/devicestate/devicestate_test.go:1988: interfaces.PlugRef composite literal uses unkeyed fields
overlord/devicestate/devicestate_test.go:1989: interfaces.SlotRef composite literal uses unkeyed fields

linode:ubuntu-core-16-64:tests/main/interfaces-snapd-control-with-manage:

+ snap set core refresh.schedule=managed
error: cannot perform the following tasks:
- Run configuration of "core" snap (cannot set schedule to managed)
+ jq '.data["last-refresh-hints"]'
/bin/bash: line 84: jq: command not found
+ cat /var/lib/snapd/state.json

@mvo5 mvo5 removed the Blocked label Nov 23, 2017

bboozzoo and others added some commits Nov 3, 2017

cmd/snap-seccomp: fix uid/gid restrictions tests on Arch
'daemon' user UID is not guaranteed to be 1. Instead of hardcoding the uid,
determine it at runtime.

Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
tests: set -e, -o pipefail in prepare-restore.sh
My recent refactoring has apparently broke broken builds more than they
were before, because various failures would be ignored and build would
happily roll on, into the wall ahead.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
tests: remove obsolete workaround
Snapd used to have a bug where "snap install" would make wrong request
to the store if the locally available file was actually the full snap
already. This workaround is no longer necessary.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
tests: simplify copying of cached snaps
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
tests: cache snaps to $TESTSLIB/cache
The testbed prepare code downloads some snaps to /tmp and subsequently
copies them to /var/lib/snapd/snap where they can be reused by `snap
install` operations. This patch changes that to use $TESTLIB/cache which
may be present locally and may already have the required files, thus
cutting a significant chunk of network traffic.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
snap: use existing files in `snap download` if digest/size matches
When `snap download` sees an existing file it will only re-download
if the digest/size does not match. Otherwise the local file is
used and not downloaded from the store.
tests: merge prepare-project.sh into prepare-restore.sh
The function definitions in prepare-project.sh were moved to the top of
the file, the source-time side effects replace the call to
prepare_project function that was already present in prepare-restore.sh

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
tests: move source/import statements to the top
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
tests: quote ${archive_compression}
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
tests: don't cp --link, it doesn't handle xdev
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
tests/lib: fix shellcheck errors
Fix the following warnings:

In ./tests/lib/prepare.sh line 198:
            cd $TESTSLIB/cache/
               ^-- SC2086: Double quote to prevent globbing and word splitting.

In ./tests/lib/store.sh line 45:
    snap ack $p
             ^-- SC2086: Double quote to prevent globbing and word splitting.

In ./tests/lib/store.sh line 47:
    snap ack $p
             ^-- SC2086: Double quote to prevent globbing and word splitting.

Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>

+1, small wondering

overlord/devicestate/devicestate.go
+
+// refreshScheduleManaged returns true if the refresh schedule of the
+// device is managed by an external snap
+func refreshScheduleManaged(st *state.State) bool {
@pedronis

pedronis Nov 27, 2017

Contributor

no strong opinion either way, but this could be moved to snapstate in the new autorefresh.go file and keep only CanSetRefreshScheduleManaged as the hook?

@mvo5

mvo5 Nov 27, 2017

Collaborator

I like your suggestion, I think:

        snapstate.CanAutoRefresh = canAutoRefresh
-       snapstate.RefreshScheduleManaged = refreshScheduleManaged
+       snapstate.CanSetRefreshScheduleManaged = CanSetRefreshScheduleManaged

alone is worth it, nice and symmetrical.

snapstate, devicestate: refactor so that we only need to hook snapsta…
…te.CanSetRefreshScheduleManaged from devicestate into snapstate (thanks Samuele)

A few details, and LGTM once you address them to your own satisfaction. Thanks for implementing this.

+type cmdCanSetRefreshScheduleManaged struct{}
+
+func init() {
+ cmd := addDebugCommand("can-set-refresh-schedule-managed",
@niemeyer

niemeyer Nov 28, 2017

Contributor

can-manage-refreshes?

@@ -20,6 +20,9 @@
package corecfg
@niemeyer

niemeyer Nov 28, 2017

Contributor

Let's please not change this right now as it's an unrelated change that will move a lot of code around, and thus make this unrelated PR more confusing, but it feels like this package is misplaced at the top level. This is quite dependent on the whole infrastructure of the overlord being live, and also strongly tied to two pieces of it: the hook manager and the config manager. Something like "configstate/configcore" might be a more appropriate place for it (homage to the io/ioutil and path/filepath naming style).

@mvo5

mvo5 Nov 28, 2017

Collaborator

Indeed, that makes sense. I will work on this in a followup PR.

overlord/devicestate/devicestate.go
@@ -223,3 +225,44 @@ func ProxyStore(st *state.State) (*asserts.Store, error) {
return a.(*asserts.Store), nil
}
+
+// plugConnected returns true if the given snap/plug names are connected
+func plugConnected(st *state.State, snapName, plugName string) bool {
@niemeyer

niemeyer Nov 28, 2017

Contributor

Is this really plugConnected/plugName or more like interfaceConnected/ifaceName?

overlord/devicestate/devicestate.go
+
+// CanSetRefreshScheduleManaged returns true if the device can be
+// switched to the "core.refresh.schedule=managed" mode.
+func CanSetRefreshScheduleManaged(st *state.State) bool {
@niemeyer

niemeyer Nov 28, 2017

Contributor

CanManageRefreshes?

+ // The snap must have a snap declaration (implies that
+ // its from the store)
+ if _, err := assertstate.SnapDeclaration(st, info.SideInfo.SnapID); err != nil {
+ continue
@niemeyer

niemeyer Nov 28, 2017

Contributor

The resilience here is appreciated, thanks!

@pedronis pedronis merged commit 5d384d5 into snapcore:master Nov 29, 2017

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment