Permalink
Fetching contributors…
Cannot retrieve contributors at this time
406 lines (343 sloc) 11.7 KB
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016-2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package hookstate
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"gopkg.in/tomb.v2"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/errtracker"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
)
type hijackFunc func(ctx *Context) error
type hijackKey struct{ hook, snap string }
// HookManager is responsible for the maintenance of hooks in the system state.
// It runs hooks when they're requested, assuming they're present in the given
// snap. Otherwise they're skipped with no error.
type HookManager struct {
state *state.State
runner *state.TaskRunner
repository *repository
contextsMutex sync.RWMutex
contexts map[string]*Context
hijackMap map[hijackKey]hijackFunc
}
// Handler is the interface a client must satify to handle hooks.
type Handler interface {
// Before is called right before the hook is to be run.
Before() error
// Done is called right after the hook has finished successfully.
Done() error
// Error is called if the hook encounters an error while running.
Error(err error) error
}
// HandlerGenerator is the function signature required to register for hooks.
type HandlerGenerator func(*Context) Handler
// HookSetup is a reference to a hook within a specific snap.
type HookSetup struct {
Snap string `json:"snap"`
Revision snap.Revision `json:"revision"`
Hook string `json:"hook"`
Optional bool `json:"optional,omitempty"`
Timeout time.Duration `json:"timeout,omitempty"`
IgnoreError bool `json:"ignore-error,omitempty"`
TrackError bool `json:"track-error,omitempty"`
}
// Manager returns a new HookManager.
func Manager(s *state.State) (*HookManager, error) {
runner := state.NewTaskRunner(s)
// Make sure we only run 1 hook task for given snap at a time
runner.SetBlocked(func(thisTask *state.Task, running []*state.Task) bool {
// check if we're a hook task, probably not needed but let's take extra care
if thisTask.Kind() != "run-hook" {
return false
}
var hooksup HookSetup
if thisTask.Get("hook-setup", &hooksup) != nil {
return false
}
thisSnapName := hooksup.Snap
// examine all hook tasks, block thisTask if we find any other hook task affecting same snap
for _, t := range running {
if t.Kind() != "run-hook" || t.Get("hook-setup", &hooksup) != nil {
continue // ignore errors and continue checking remaining tasks
}
if hooksup.Snap == thisSnapName {
// found hook task affecting same snap, block thisTask.
return true
}
}
return false
})
manager := &HookManager{
state: s,
runner: runner,
repository: newRepository(),
contexts: make(map[string]*Context),
hijackMap: make(map[hijackKey]hijackFunc),
}
runner.AddHandler("run-hook", manager.doRunHook, nil)
// Compatibility with snapd between 2.29 and 2.30 in edge only.
// We generated a configure-snapd task on core refreshes and
// for compatibility we need to handle those.
runner.AddHandler("configure-snapd", func(*state.Task, *tomb.Tomb) error {
return nil
}, nil)
setupHooks(manager)
return manager, nil
}
// Register registers a function to create Handler values whenever hooks
// matching the provided pattern are run.
func (m *HookManager) Register(pattern *regexp.Regexp, generator HandlerGenerator) {
m.repository.addHandlerGenerator(pattern, generator)
}
func (m *HookManager) KnownTaskKinds() []string {
return m.runner.KnownTaskKinds()
}
// Ensure implements StateManager.Ensure.
func (m *HookManager) Ensure() error {
m.runner.Ensure()
return nil
}
// Wait implements StateManager.Wait.
func (m *HookManager) Wait() {
m.runner.Wait()
}
// Stop implements StateManager.Stop.
func (m *HookManager) Stop() {
m.runner.Stop()
}
func (m *HookManager) hijacked(hookName, snapName string) hijackFunc {
return m.hijackMap[hijackKey{hookName, snapName}]
}
func (m *HookManager) RegisterHijack(hookName, snapName string, f hijackFunc) {
if _, ok := m.hijackMap[hijackKey{hookName, snapName}]; ok {
panic(fmt.Sprintf("hook %s for snap %s already hijacked", hookName, snapName))
}
m.hijackMap[hijackKey{hookName, snapName}] = f
}
func (m *HookManager) ephemeralContext(cookieID string) (context *Context, err error) {
var contexts map[string]string
m.state.Lock()
defer m.state.Unlock()
err = m.state.Get("snap-cookies", &contexts)
if err != nil {
return nil, fmt.Errorf("cannot get snap cookies: %v", err)
}
if snapName, ok := contexts[cookieID]; ok {
// create new ephemeral cookie
context, err = NewContext(nil, m.state, &HookSetup{Snap: snapName}, nil, cookieID)
return context, err
}
return nil, fmt.Errorf("invalid snap cookie requested")
}
// Context obtains the context for the given cookie ID.
func (m *HookManager) Context(cookieID string) (*Context, error) {
m.contextsMutex.RLock()
defer m.contextsMutex.RUnlock()
var err error
context, ok := m.contexts[cookieID]
if !ok {
context, err = m.ephemeralContext(cookieID)
if err != nil {
return nil, err
}
}
return context, nil
}
func hookSetup(task *state.Task) (*HookSetup, *snapstate.SnapState, error) {
var hooksup HookSetup
err := task.Get("hook-setup", &hooksup)
if err != nil {
return nil, nil, fmt.Errorf("cannot extract hook setup from task: %s", err)
}
var snapst snapstate.SnapState
err = snapstate.Get(task.State(), hooksup.Snap, &snapst)
if err != nil && err != state.ErrNoState {
return nil, nil, fmt.Errorf("cannot handle %q snap: %v", hooksup.Snap, err)
}
return &hooksup, &snapst, nil
}
// doRunHook actually runs the hook that was requested.
//
// Note that this method is synchronous, as the task is already running in a
// goroutine.
func (m *HookManager) doRunHook(task *state.Task, tomb *tomb.Tomb) error {
task.State().Lock()
hooksup, snapst, err := hookSetup(task)
task.State().Unlock()
if err != nil {
return err
}
mustHijack := m.hijacked(hooksup.Hook, hooksup.Snap) != nil
hookExists := false
if !mustHijack {
// not hijacked, snap must be installed
if !snapst.IsInstalled() {
return fmt.Errorf("cannot find %q snap", hooksup.Snap)
}
info, err := snapst.CurrentInfo()
if err != nil {
return fmt.Errorf("cannot read %q snap details: %v", hooksup.Snap, err)
}
hookExists = info.Hooks[hooksup.Hook] != nil
if !hookExists && !hooksup.Optional {
return fmt.Errorf("snap %q has no %q hook", hooksup.Snap, hooksup.Hook)
}
}
context, err := NewContext(task, task.State(), hooksup, nil, "")
if err != nil {
return err
}
// Obtain a handler for this hook. The repository returns a list since it's
// possible for regular expressions to overlap, but multiple handlers is an
// error (as is no handler).
handlers := m.repository.generateHandlers(context)
handlersCount := len(handlers)
if handlersCount == 0 {
// Do not report error if hook handler doesn't exist as long as the hook is optional.
// This is to avoid issues when downgrading to an old core snap that doesn't know about
// particular hook type and a task for it exists (e.g. "post-refresh" hook).
if hooksup.Optional {
return nil
}
return fmt.Errorf("internal error: no registered handlers for hook %q", hooksup.Hook)
}
if handlersCount > 1 {
return fmt.Errorf("internal error: %d handlers registered for hook %q, expected 1", handlersCount, hooksup.Hook)
}
context.handler = handlers[0]
contextID := context.ID()
m.contextsMutex.Lock()
m.contexts[contextID] = context
m.contextsMutex.Unlock()
defer func() {
m.contextsMutex.Lock()
delete(m.contexts, contextID)
m.contextsMutex.Unlock()
}()
if err = context.Handler().Before(); err != nil {
return err
}
// some hooks get hijacked, e.g. the core configuration
var output []byte
if f := m.hijacked(hooksup.Hook, hooksup.Snap); f != nil {
err = f(context)
} else if hookExists {
output, err = runHook(context, tomb)
}
if err != nil {
if hooksup.TrackError {
trackHookError(context, output, err)
}
err = osutil.OutputErr(output, err)
if hooksup.IgnoreError {
task.State().Lock()
task.Errorf("ignoring failure in hook %q: %v", hooksup.Hook, err)
task.State().Unlock()
} else {
if handlerErr := context.Handler().Error(err); handlerErr != nil {
return handlerErr
}
return fmt.Errorf("run hook %q: %v", hooksup.Hook, err)
}
}
if err = context.Handler().Done(); err != nil {
return err
}
context.Lock()
defer context.Unlock()
if err = context.Done(); err != nil {
return err
}
return nil
}
func runHookImpl(c *Context, tomb *tomb.Tomb) ([]byte, error) {
return runHookAndWait(c.SnapName(), c.SnapRevision(), c.HookName(), c.ID(), c.Timeout(), tomb)
}
var runHook = runHookImpl
// MockRunHook mocks the actual invocation of hooks for tests.
func MockRunHook(hookInvoke func(c *Context, tomb *tomb.Tomb) ([]byte, error)) (restore func()) {
oldRunHook := runHook
runHook = hookInvoke
return func() {
runHook = oldRunHook
}
}
var osReadlink = os.Readlink
// snapCmd returns the "snap" command to run. If snapd is re-execed
// it will be the snap command from the core snap, otherwise it will
// be the system "snap" command (c.f. LP: #1668738).
func snapCmd() string {
// sensible default, assume PATH is correct
snapCmd := "snap"
exe, err := osReadlink("/proc/self/exe")
if err != nil {
logger.Noticef("cannot read /proc/self/exe: %v, using default snap command", err)
return snapCmd
}
if !strings.HasPrefix(exe, dirs.SnapMountDir) {
return snapCmd
}
// snap is running from the core snap, we know the relative
// location of "snap" from "snapd"
return filepath.Join(filepath.Dir(exe), "../../bin/snap")
}
var defaultHookTimeout = 10 * time.Minute
func runHookAndWait(snapName string, revision snap.Revision, hookName, hookContext string, timeout time.Duration, tomb *tomb.Tomb) ([]byte, error) {
argv := []string{snapCmd(), "run", "--hook", hookName, "-r", revision.String(), snapName}
if timeout == 0 {
timeout = defaultHookTimeout
}
env := []string{
// Make sure the hook has its context defined so it can
// communicate via the REST API.
fmt.Sprintf("SNAP_COOKIE=%s", hookContext),
// Set SNAP_CONTEXT too for compatibility with old snapctl
// binary when transitioning to a new core - otherwise configure
// hook would fail during transition.
fmt.Sprintf("SNAP_CONTEXT=%s", hookContext),
}
return osutil.RunAndWait(argv, env, timeout, tomb)
}
var errtrackerReport = errtracker.Report
func trackHookError(context *Context, output []byte, err error) {
errmsg := fmt.Sprintf("hook %s in snap %q failed: %v", context.HookName(), context.SnapName(), osutil.OutputErr(output, err))
dupSig := fmt.Sprintf("hook:%s:%s:%s\n%s", context.SnapName(), context.HookName(), err, output)
extra := map[string]string{
"HookName": context.HookName(),
}
if context.setup.IgnoreError {
extra["IgnoreError"] = "1"
}
oopsid, err := errtrackerReport(context.SnapName(), errmsg, dupSig, extra)
if err == nil {
logger.Noticef("Reported hook failure from %q for snap %q as %s", context.HookName(), context.SnapName(), oopsid)
} else {
logger.Debugf("Cannot report hook failure: %s", err)
}
}