github.com/rigado/snapd@v2.42.5-go-mod+incompatible/overlord/snapstate/autorefresh.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package snapstate
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"time"
    26  
    27  	"github.com/snapcore/snapd/httputil"
    28  	"github.com/snapcore/snapd/i18n"
    29  	"github.com/snapcore/snapd/logger"
    30  	"github.com/snapcore/snapd/overlord/auth"
    31  	"github.com/snapcore/snapd/overlord/configstate/config"
    32  	"github.com/snapcore/snapd/overlord/state"
    33  	"github.com/snapcore/snapd/release"
    34  	"github.com/snapcore/snapd/snap"
    35  	"github.com/snapcore/snapd/strutil"
    36  	"github.com/snapcore/snapd/timeutil"
    37  	"github.com/snapcore/snapd/timings"
    38  )
    39  
    40  // the default refresh pattern
    41  const defaultRefreshSchedule = "00:00~24:00/4"
    42  
    43  // cannot keep without refreshing for more than maxPostponement
    44  const maxPostponement = 60 * 24 * time.Hour
    45  
    46  // cannot inhibit refreshes for more than maxInhibition
    47  const maxInhibition = 7 * 24 * time.Hour
    48  
    49  // hooks setup by devicestate
    50  var (
    51  	CanAutoRefresh        func(st *state.State) (bool, error)
    52  	CanManageRefreshes    func(st *state.State) bool
    53  	IsOnMeteredConnection func() (bool, error)
    54  )
    55  
    56  // refreshRetryDelay specified the minimum time to retry failed refreshes
    57  var refreshRetryDelay = 20 * time.Minute
    58  
    59  // autoRefresh will ensure that snaps are refreshed automatically
    60  // according to the refresh schedule.
    61  type autoRefresh struct {
    62  	state *state.State
    63  
    64  	lastRefreshSchedule string
    65  	nextRefresh         time.Time
    66  	lastRefreshAttempt  time.Time
    67  }
    68  
    69  func newAutoRefresh(st *state.State) *autoRefresh {
    70  	return &autoRefresh{
    71  		state: st,
    72  	}
    73  }
    74  
    75  // RefreshSchedule will return a user visible string with the current schedule
    76  // for the automatic refreshes and a flag indicating whether the schedule is a
    77  // legacy one.
    78  func (m *autoRefresh) RefreshSchedule() (schedule string, legacy bool, err error) {
    79  	_, schedule, legacy, err = m.refreshScheduleWithDefaultsFallback()
    80  	return schedule, legacy, err
    81  }
    82  
    83  // NextRefresh returns when the next automatic refresh will happen.
    84  func (m *autoRefresh) NextRefresh() time.Time {
    85  	return m.nextRefresh
    86  }
    87  
    88  // LastRefresh returns when the last refresh happened.
    89  func (m *autoRefresh) LastRefresh() (time.Time, error) {
    90  	return getTime(m.state, "last-refresh")
    91  }
    92  
    93  // EffectiveRefreshHold returns the time until to which refreshes are
    94  // held if refresh.hold configuration is set and accounting for the
    95  // max postponement since the last refresh.
    96  func (m *autoRefresh) EffectiveRefreshHold() (time.Time, error) {
    97  	var holdTime time.Time
    98  
    99  	tr := config.NewTransaction(m.state)
   100  	err := tr.Get("core", "refresh.hold", &holdTime)
   101  	if err != nil && !config.IsNoOption(err) {
   102  		return time.Time{}, err
   103  	}
   104  
   105  	// cannot hold beyond last-refresh + max-postponement
   106  	lastRefresh, err := m.LastRefresh()
   107  	if err != nil {
   108  		return time.Time{}, err
   109  	}
   110  	if lastRefresh.IsZero() {
   111  		seedTime, err := getTime(m.state, "seed-time")
   112  		if err != nil {
   113  			return time.Time{}, err
   114  		}
   115  		if seedTime.IsZero() {
   116  			// no reference to know whether holding is reasonable
   117  			return time.Time{}, nil
   118  		}
   119  		lastRefresh = seedTime
   120  	}
   121  
   122  	limitTime := lastRefresh.Add(maxPostponement)
   123  	if holdTime.After(limitTime) {
   124  		return limitTime, nil
   125  	}
   126  
   127  	return holdTime, nil
   128  }
   129  
   130  // clearRefreshHold clears refresh.hold configuration.
   131  func (m *autoRefresh) clearRefreshHold() {
   132  	tr := config.NewTransaction(m.state)
   133  	tr.Set("core", "refresh.hold", nil)
   134  	tr.Commit()
   135  }
   136  
   137  // AtSeed configures refresh policies at end of seeding.
   138  func (m *autoRefresh) AtSeed() error {
   139  	// on classic hold refreshes for 2h after seeding
   140  	if release.OnClassic {
   141  		var t1 time.Time
   142  		tr := config.NewTransaction(m.state)
   143  		err := tr.Get("core", "refresh.hold", &t1)
   144  		if !config.IsNoOption(err) {
   145  			// already set or error
   146  			return err
   147  		}
   148  		// TODO: have a policy that if the snapd exe itself
   149  		// is older than X weeks/months we skip the holding?
   150  		now := time.Now().UTC()
   151  		tr.Set("core", "refresh.hold", now.Add(2*time.Hour))
   152  		tr.Commit()
   153  		m.nextRefresh = now
   154  	}
   155  	return nil
   156  }
   157  
   158  func canRefreshOnMeteredConnection(st *state.State) (bool, error) {
   159  	tr := config.NewTransaction(st)
   160  	var onMetered string
   161  	err := tr.GetMaybe("core", "refresh.metered", &onMetered)
   162  	if err != nil && err != state.ErrNoState {
   163  		return false, err
   164  	}
   165  
   166  	return onMetered != "hold", nil
   167  }
   168  
   169  func (m *autoRefresh) canRefreshRespectingMetered(now, lastRefresh time.Time) (can bool, err error) {
   170  	can, err = canRefreshOnMeteredConnection(m.state)
   171  	if err != nil {
   172  		return false, err
   173  	}
   174  	if can {
   175  		return true, nil
   176  	}
   177  
   178  	// ignore any errors that occurred while checking if we are on a metered
   179  	// connection
   180  	metered, _ := IsOnMeteredConnection()
   181  	if !metered {
   182  		return true, nil
   183  	}
   184  
   185  	if now.Sub(lastRefresh) >= maxPostponement {
   186  		// TODO use warnings when the infra becomes available
   187  		logger.Noticef("Auto refresh disabled while on metered connections, but pending for too long (%d days). Trying to refresh now.", int(maxPostponement.Hours()/24))
   188  		return true, nil
   189  	}
   190  
   191  	logger.Debugf("Auto refresh disabled on metered connections")
   192  
   193  	return false, nil
   194  }
   195  
   196  // Ensure ensures that we refresh all installed snaps periodically
   197  func (m *autoRefresh) Ensure() error {
   198  	m.state.Lock()
   199  	defer m.state.Unlock()
   200  
   201  	// see if it even makes sense to try to refresh
   202  	if CanAutoRefresh == nil {
   203  		return nil
   204  	}
   205  	if ok, err := CanAutoRefresh(m.state); err != nil || !ok {
   206  		return err
   207  	}
   208  
   209  	// get lastRefresh and schedule
   210  	lastRefresh, err := m.LastRefresh()
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	refreshSchedule, refreshScheduleStr, _, err := m.refreshScheduleWithDefaultsFallback()
   216  	if err != nil {
   217  		return err
   218  	}
   219  	if len(refreshSchedule) == 0 {
   220  		m.nextRefresh = time.Time{}
   221  		return nil
   222  	}
   223  	// we already have a refresh time, check if we got a new config
   224  	if !m.nextRefresh.IsZero() {
   225  		if m.lastRefreshSchedule != refreshScheduleStr {
   226  			// the refresh schedule has changed
   227  			logger.Debugf("Refresh timer changed.")
   228  			m.nextRefresh = time.Time{}
   229  		}
   230  	}
   231  	m.lastRefreshSchedule = refreshScheduleStr
   232  
   233  	// ensure nothing is in flight already
   234  	if autoRefreshInFlight(m.state) {
   235  		return nil
   236  	}
   237  
   238  	now := time.Now()
   239  	// compute next refresh attempt time (if needed)
   240  	if m.nextRefresh.IsZero() {
   241  		// store attempts in memory so that we can backoff
   242  		if !lastRefresh.IsZero() {
   243  			delta := timeutil.Next(refreshSchedule, lastRefresh, maxPostponement)
   244  			now = time.Now()
   245  			m.nextRefresh = now.Add(delta)
   246  		} else {
   247  			// make sure either seed-time or last-refresh
   248  			// are set for hold code below
   249  			m.ensureLastRefreshAnchor()
   250  			// immediate
   251  			m.nextRefresh = now
   252  		}
   253  		logger.Debugf("Next refresh scheduled for %s.", m.nextRefresh.Format(time.RFC3339))
   254  	}
   255  
   256  	// should we hold back refreshes?
   257  	holdTime, err := m.EffectiveRefreshHold()
   258  	if err != nil {
   259  		return err
   260  	}
   261  	if holdTime.After(now) {
   262  		return nil
   263  	}
   264  	if !holdTime.IsZero() {
   265  		// expired hold case
   266  		m.clearRefreshHold()
   267  		if m.nextRefresh.Before(holdTime) {
   268  			// next refresh is obsolete, compute the next one
   269  			delta := timeutil.Next(refreshSchedule, holdTime, maxPostponement)
   270  			now = time.Now()
   271  			m.nextRefresh = now.Add(delta)
   272  		}
   273  	}
   274  
   275  	// do refresh attempt (if needed)
   276  	if !m.nextRefresh.After(now) {
   277  		var can bool
   278  		can, err = m.canRefreshRespectingMetered(now, lastRefresh)
   279  		if err != nil {
   280  			return err
   281  		}
   282  		if !can {
   283  			// clear nextRefresh so that another refresh time is calculated
   284  			m.nextRefresh = time.Time{}
   285  			return nil
   286  		}
   287  
   288  		// Check that we have reasonable delays between attempts.
   289  		// If the store is under stress we need to make sure we do not
   290  		// hammer it too often
   291  		if !m.lastRefreshAttempt.IsZero() && m.lastRefreshAttempt.Add(refreshRetryDelay).After(time.Now()) {
   292  			return nil
   293  		}
   294  
   295  		err = m.launchAutoRefresh()
   296  		if _, ok := err.(*httputil.PerstistentNetworkError); !ok {
   297  			m.nextRefresh = time.Time{}
   298  		} // else - refresh will be retried after refreshRetryDelay
   299  	}
   300  
   301  	return err
   302  }
   303  
   304  func (m *autoRefresh) ensureLastRefreshAnchor() {
   305  	seedTime, _ := getTime(m.state, "seed-time")
   306  	if !seedTime.IsZero() {
   307  		return
   308  	}
   309  
   310  	// last core refresh
   311  	coreRefreshDate := snap.InstallDate("core")
   312  	if !coreRefreshDate.IsZero() {
   313  		m.state.Set("last-refresh", coreRefreshDate)
   314  		return
   315  	}
   316  
   317  	// fallback to executable time
   318  	st, err := os.Stat("/proc/self/exe")
   319  	if err == nil {
   320  		m.state.Set("last-refresh", st.ModTime())
   321  		return
   322  	}
   323  }
   324  
   325  // refreshScheduleWithDefaultsFallback returns the current refresh schedule
   326  // and refresh string. When an invalid refresh schedule is set by the user
   327  // the refresh schedule is automatically reset to the default.
   328  //
   329  // TODO: we can remove the refreshSchedule reset because we have validation
   330  //       of the schedule now.
   331  func (m *autoRefresh) refreshScheduleWithDefaultsFallback() (ts []*timeutil.Schedule, scheduleAsStr string, legacy bool, err error) {
   332  	if managed, legacy := refreshScheduleManaged(m.state); managed {
   333  		if m.lastRefreshSchedule != "managed" {
   334  			logger.Noticef("refresh is managed via the snapd-control interface")
   335  			m.lastRefreshSchedule = "managed"
   336  		}
   337  		return nil, "managed", legacy, nil
   338  	}
   339  
   340  	tr := config.NewTransaction(m.state)
   341  	// try the new refresh.timer config option first
   342  	err = tr.Get("core", "refresh.timer", &scheduleAsStr)
   343  	if err != nil && !config.IsNoOption(err) {
   344  		return nil, "", false, err
   345  	}
   346  	if scheduleAsStr != "" {
   347  		ts, err = timeutil.ParseSchedule(scheduleAsStr)
   348  		if err != nil {
   349  			logger.Noticef("cannot use refresh.timer configuration: %s", err)
   350  			return refreshScheduleDefault()
   351  		}
   352  		return ts, scheduleAsStr, false, nil
   353  	}
   354  
   355  	// fallback to legacy refresh.schedule setting when the new
   356  	// config option is not set
   357  	err = tr.Get("core", "refresh.schedule", &scheduleAsStr)
   358  	if err != nil && !config.IsNoOption(err) {
   359  		return nil, "", false, err
   360  	}
   361  	if scheduleAsStr != "" {
   362  		ts, err = timeutil.ParseLegacySchedule(scheduleAsStr)
   363  		if err != nil {
   364  			logger.Noticef("cannot use refresh.schedule configuration: %s", err)
   365  			return refreshScheduleDefault()
   366  		}
   367  		return ts, scheduleAsStr, true, nil
   368  	}
   369  
   370  	return refreshScheduleDefault()
   371  }
   372  
   373  // launchAutoRefresh creates the auto-refresh taskset and a change for it.
   374  func (m *autoRefresh) launchAutoRefresh() error {
   375  	perfTimings := timings.New(map[string]string{"ensure": "auto-refresh"})
   376  	tm := perfTimings.StartSpan("auto-refresh", "query store and setup auto-refresh change")
   377  	defer func() {
   378  		tm.Stop()
   379  		perfTimings.Save(m.state)
   380  	}()
   381  
   382  	m.lastRefreshAttempt = time.Now()
   383  	updated, tasksets, err := AutoRefresh(auth.EnsureContextTODO(), m.state)
   384  	if _, ok := err.(*httputil.PerstistentNetworkError); ok {
   385  		logger.Noticef("Cannot prepare auto-refresh change due to a permanent network error: %s", err)
   386  		return err
   387  	}
   388  	m.state.Set("last-refresh", time.Now())
   389  	if err != nil {
   390  		logger.Noticef("Cannot prepare auto-refresh change: %s", err)
   391  		return err
   392  	}
   393  
   394  	var msg string
   395  	switch len(updated) {
   396  	case 0:
   397  		logger.Noticef(i18n.G("auto-refresh: all snaps are up-to-date"))
   398  		return nil
   399  	case 1:
   400  		msg = fmt.Sprintf(i18n.G("Auto-refresh snap %q"), updated[0])
   401  	case 2, 3:
   402  		quoted := strutil.Quoted(updated)
   403  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   404  		msg = fmt.Sprintf(i18n.G("Auto-refresh snaps %s"), quoted)
   405  	default:
   406  		msg = fmt.Sprintf(i18n.G("Auto-refresh %d snaps"), len(updated))
   407  	}
   408  
   409  	chg := m.state.NewChange("auto-refresh", msg)
   410  	for _, ts := range tasksets {
   411  		chg.AddAll(ts)
   412  	}
   413  	chg.Set("snap-names", updated)
   414  	chg.Set("api-data", map[string]interface{}{"snap-names": updated})
   415  	perfTimings.AddTag("change-id", chg.ID())
   416  
   417  	return nil
   418  }
   419  
   420  func refreshScheduleDefault() (ts []*timeutil.Schedule, scheduleStr string, legacy bool, err error) {
   421  	refreshSchedule, err := timeutil.ParseSchedule(defaultRefreshSchedule)
   422  	if err != nil {
   423  		panic(fmt.Sprintf("defaultRefreshSchedule cannot be parsed: %s", err))
   424  	}
   425  
   426  	return refreshSchedule, defaultRefreshSchedule, false, nil
   427  }
   428  
   429  func autoRefreshInFlight(st *state.State) bool {
   430  	for _, chg := range st.Changes() {
   431  		if chg.Kind() == "auto-refresh" && !chg.Status().Ready() {
   432  			return true
   433  		}
   434  	}
   435  	return false
   436  }
   437  
   438  // refreshScheduleManaged returns true if the refresh schedule of the
   439  // device is managed by an external snap
   440  func refreshScheduleManaged(st *state.State) (managed bool, legacy bool) {
   441  	var confStr string
   442  
   443  	// this will only be "nil" if running in tests
   444  	if CanManageRefreshes == nil {
   445  		return false, legacy
   446  	}
   447  
   448  	// check new style timer first
   449  	tr := config.NewTransaction(st)
   450  	err := tr.Get("core", "refresh.timer", &confStr)
   451  	if err != nil && !config.IsNoOption(err) {
   452  		return false, legacy
   453  	}
   454  	// if not set, fallback to refresh.schedule
   455  	if confStr == "" {
   456  		if err := tr.Get("core", "refresh.schedule", &confStr); err != nil {
   457  			return false, legacy
   458  		}
   459  		legacy = true
   460  	}
   461  
   462  	if confStr != "managed" {
   463  		return false, legacy
   464  	}
   465  	return CanManageRefreshes(st), legacy
   466  }
   467  
   468  // getTime retrieves a time from a state value.
   469  func getTime(st *state.State, timeKey string) (time.Time, error) {
   470  	var t1 time.Time
   471  	err := st.Get(timeKey, &t1)
   472  	if err != nil && err != state.ErrNoState {
   473  		return time.Time{}, err
   474  	}
   475  	return t1, nil
   476  }
   477  
   478  // inhibitRefresh returns an error if refresh is inhibited by running apps.
   479  //
   480  // Internally the snap state is updated to remember when the inhibition first
   481  // took place. Apps can inhibit refreshes for up to "maxInhibition", beyond
   482  // that period the refresh will go ahead despite application activity.
   483  func inhibitRefresh(st *state.State, snapst *SnapState, info *snap.Info, checker func(*snap.Info) error) error {
   484  	if err := checker(info); err != nil {
   485  		now := time.Now()
   486  		if snapst.RefreshInhibitedTime == nil {
   487  			// Store the instant when the snap was first inhibited.
   488  			// This is reset to nil on successful refresh.
   489  			snapst.RefreshInhibitedTime = &now
   490  			Set(st, info.InstanceName(), snapst)
   491  			return err
   492  		}
   493  
   494  		if now.Sub(*snapst.RefreshInhibitedTime) < maxInhibition {
   495  			// If we are still in the allowed window then just return
   496  			// the error but don't change the snap state again.
   497  			return err
   498  		}
   499  	}
   500  	return nil
   501  }