github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/overlord/hookstate/hookmgr.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-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 hookstate 21 22 import ( 23 "context" 24 "fmt" 25 "os" 26 "path/filepath" 27 "regexp" 28 "strings" 29 "sync" 30 "sync/atomic" 31 "time" 32 33 "gopkg.in/tomb.v2" 34 35 "github.com/snapcore/snapd/dirs" 36 "github.com/snapcore/snapd/errtracker" 37 "github.com/snapcore/snapd/logger" 38 "github.com/snapcore/snapd/osutil" 39 "github.com/snapcore/snapd/overlord/configstate/settings" 40 "github.com/snapcore/snapd/overlord/snapstate" 41 "github.com/snapcore/snapd/overlord/state" 42 "github.com/snapcore/snapd/snap" 43 ) 44 45 type hijackFunc func(ctx *Context) error 46 type hijackKey struct{ hook, snap string } 47 48 // HookManager is responsible for the maintenance of hooks in the system state. 49 // It runs hooks when they're requested, assuming they're present in the given 50 // snap. Otherwise they're skipped with no error. 51 type HookManager struct { 52 state *state.State 53 repository *repository 54 55 contextsMutex sync.RWMutex 56 contexts map[string]*Context 57 58 hijackMap map[hijackKey]hijackFunc 59 60 runningHooks int32 61 runner *state.TaskRunner 62 } 63 64 // Handler is the interface a client must satify to handle hooks. 65 type Handler interface { 66 // Before is called right before the hook is to be run. 67 Before() error 68 69 // Done is called right after the hook has finished successfully. 70 Done() error 71 72 // Error is called if the hook encounters an error while running. 73 Error(err error) error 74 } 75 76 // HandlerGenerator is the function signature required to register for hooks. 77 type HandlerGenerator func(*Context) Handler 78 79 // HookSetup is a reference to a hook within a specific snap. 80 type HookSetup struct { 81 Snap string `json:"snap"` 82 Revision snap.Revision `json:"revision"` 83 Hook string `json:"hook"` 84 Timeout time.Duration `json:"timeout,omitempty"` 85 Optional bool `json:"optional,omitempty"` // do not error if script is missing 86 Always bool `json:"always,omitempty"` // run handler even if script is missing 87 IgnoreError bool `json:"ignore-error,omitempty"` // do not run handler's Error() on error 88 TrackError bool `json:"track-error,omitempty"` // report hook error to oopsie 89 } 90 91 // Manager returns a new HookManager. 92 func Manager(s *state.State, runner *state.TaskRunner) (*HookManager, error) { 93 // Make sure we only run 1 hook task for given snap at a time 94 runner.AddBlocked(func(thisTask *state.Task, running []*state.Task) bool { 95 // check if we're a hook task, probably not needed but let's take extra care 96 if thisTask.Kind() != "run-hook" { 97 return false 98 } 99 var hooksup HookSetup 100 if thisTask.Get("hook-setup", &hooksup) != nil { 101 return false 102 } 103 thisSnapName := hooksup.Snap 104 // examine all hook tasks, block thisTask if we find any other hook task affecting same snap 105 for _, t := range running { 106 if t.Kind() != "run-hook" || t.Get("hook-setup", &hooksup) != nil { 107 continue // ignore errors and continue checking remaining tasks 108 } 109 if hooksup.Snap == thisSnapName { 110 // found hook task affecting same snap, block thisTask. 111 return true 112 } 113 } 114 return false 115 }) 116 117 manager := &HookManager{ 118 state: s, 119 repository: newRepository(), 120 contexts: make(map[string]*Context), 121 hijackMap: make(map[hijackKey]hijackFunc), 122 runner: runner, 123 } 124 125 runner.AddHandler("run-hook", manager.doRunHook, manager.undoRunHook) 126 // Compatibility with snapd between 2.29 and 2.30 in edge only. 127 // We generated a configure-snapd task on core refreshes and 128 // for compatibility we need to handle those. 129 runner.AddHandler("configure-snapd", func(*state.Task, *tomb.Tomb) error { 130 return nil 131 }, nil) 132 133 setupHooks(manager) 134 135 snapstate.AddAffectedSnapsByAttr("hook-setup", manager.hookAffectedSnaps) 136 137 return manager, nil 138 } 139 140 // Register registers a function to create Handler values whenever hooks 141 // matching the provided pattern are run. 142 func (m *HookManager) Register(pattern *regexp.Regexp, generator HandlerGenerator) { 143 m.repository.addHandlerGenerator(pattern, generator) 144 } 145 146 // Ensure implements StateManager.Ensure. 147 func (m *HookManager) Ensure() error { 148 return nil 149 } 150 151 // StopHooks kills all currently running hooks and returns after 152 // that's done. 153 func (m *HookManager) StopHooks() { 154 m.runner.StopKinds("run-hook") 155 } 156 157 func (m *HookManager) hijacked(hookName, instanceName string) hijackFunc { 158 return m.hijackMap[hijackKey{hookName, instanceName}] 159 } 160 161 func (m *HookManager) RegisterHijack(hookName, instanceName string, f hijackFunc) { 162 if _, ok := m.hijackMap[hijackKey{hookName, instanceName}]; ok { 163 panic(fmt.Sprintf("hook %s for snap %s already hijacked", hookName, instanceName)) 164 } 165 m.hijackMap[hijackKey{hookName, instanceName}] = f 166 } 167 168 func (m *HookManager) hookAffectedSnaps(t *state.Task) ([]string, error) { 169 var hooksup HookSetup 170 if err := t.Get("hook-setup", &hooksup); err != nil { 171 return nil, fmt.Errorf("internal error: cannot obtain hook data from task: %s", t.Summary()) 172 173 } 174 175 if m.hijacked(hooksup.Hook, hooksup.Snap) != nil { 176 // assume being these internal they should not 177 // generate conflicts 178 return nil, nil 179 } 180 181 return []string{hooksup.Snap}, nil 182 } 183 184 func (m *HookManager) ephemeralContext(cookieID string) (context *Context, err error) { 185 var contexts map[string]string 186 m.state.Lock() 187 defer m.state.Unlock() 188 err = m.state.Get("snap-cookies", &contexts) 189 if err != nil { 190 return nil, fmt.Errorf("cannot get snap cookies: %v", err) 191 } 192 if instanceName, ok := contexts[cookieID]; ok { 193 // create new ephemeral cookie 194 context, err = NewContext(nil, m.state, &HookSetup{Snap: instanceName}, nil, cookieID) 195 return context, err 196 } 197 return nil, fmt.Errorf("invalid snap cookie requested") 198 } 199 200 // Context obtains the context for the given cookie ID. 201 func (m *HookManager) Context(cookieID string) (*Context, error) { 202 m.contextsMutex.RLock() 203 defer m.contextsMutex.RUnlock() 204 205 var err error 206 context, ok := m.contexts[cookieID] 207 if !ok { 208 context, err = m.ephemeralContext(cookieID) 209 if err != nil { 210 return nil, err 211 } 212 } 213 214 return context, nil 215 } 216 217 func hookSetup(task *state.Task, key string) (*HookSetup, *snapstate.SnapState, error) { 218 var hooksup HookSetup 219 err := task.Get(key, &hooksup) 220 if err != nil { 221 return nil, nil, err 222 } 223 224 var snapst snapstate.SnapState 225 err = snapstate.Get(task.State(), hooksup.Snap, &snapst) 226 if err != nil && err != state.ErrNoState { 227 return nil, nil, fmt.Errorf("cannot handle %q snap: %v", hooksup.Snap, err) 228 } 229 230 return &hooksup, &snapst, nil 231 } 232 233 // NumRunningHooks returns the number of hooks running at the moment. 234 func (m *HookManager) NumRunningHooks() int { 235 return int(atomic.LoadInt32(&m.runningHooks)) 236 } 237 238 // GracefullyWaitRunningHooks waits for currently running hooks to finish up to the default hook timeout. Returns true if there are no more running hooks on exit. 239 func (m *HookManager) GracefullyWaitRunningHooks() bool { 240 toutC := time.After(defaultHookTimeout) 241 doWait := true 242 for m.NumRunningHooks() > 0 && doWait { 243 select { 244 case <-time.After(1 * time.Second): 245 case <-toutC: 246 doWait = false 247 } 248 } 249 return m.NumRunningHooks() == 0 250 } 251 252 // doRunHook actually runs the hook that was requested. 253 // 254 // Note that this method is synchronous, as the task is already running in a 255 // goroutine. 256 func (m *HookManager) doRunHook(task *state.Task, tomb *tomb.Tomb) error { 257 task.State().Lock() 258 hooksup, snapst, err := hookSetup(task, "hook-setup") 259 task.State().Unlock() 260 if err != nil { 261 return fmt.Errorf("cannot extract hook setup from task: %s", err) 262 } 263 264 return m.runHookForTask(task, tomb, snapst, hooksup) 265 } 266 267 // undoRunHook runs the undo-hook that was requested. 268 // 269 // Note that this method is synchronous, as the task is already running in a 270 // goroutine. 271 func (m *HookManager) undoRunHook(task *state.Task, tomb *tomb.Tomb) error { 272 task.State().Lock() 273 hooksup, snapst, err := hookSetup(task, "undo-hook-setup") 274 task.State().Unlock() 275 if err != nil { 276 if err == state.ErrNoState { 277 // no undo hook setup 278 return nil 279 } 280 return fmt.Errorf("cannot extract undo hook setup from task: %s", err) 281 } 282 283 return m.runHookForTask(task, tomb, snapst, hooksup) 284 } 285 286 func (m *HookManager) EphemeralRunHook(ctx context.Context, hooksup *HookSetup, contextData map[string]interface{}) (*Context, error) { 287 var snapst snapstate.SnapState 288 m.state.Lock() 289 err := snapstate.Get(m.state, hooksup.Snap, &snapst) 290 m.state.Unlock() 291 if err != nil { 292 return nil, fmt.Errorf("cannot run ephemeral hook %q for snap %q: %v", hooksup.Hook, hooksup.Snap, err) 293 } 294 295 context, err := newEphemeralHookContextWithData(m.state, hooksup, contextData) 296 if err != nil { 297 return nil, err 298 } 299 300 tomb, _ := tomb.WithContext(ctx) 301 if err := m.runHook(context, &snapst, hooksup, tomb); err != nil { 302 return nil, err 303 } 304 return context, nil 305 } 306 307 func (m *HookManager) runHookForTask(task *state.Task, tomb *tomb.Tomb, snapst *snapstate.SnapState, hooksup *HookSetup) error { 308 context, err := NewContext(task, m.state, hooksup, nil, "") 309 if err != nil { 310 return err 311 } 312 return m.runHook(context, snapst, hooksup, tomb) 313 } 314 315 func (m *HookManager) runHook(context *Context, snapst *snapstate.SnapState, hooksup *HookSetup, tomb *tomb.Tomb) error { 316 mustHijack := m.hijacked(hooksup.Hook, hooksup.Snap) != nil 317 hookExists := false 318 if !mustHijack { 319 // not hijacked, snap must be installed 320 if !snapst.IsInstalled() { 321 return fmt.Errorf("cannot find %q snap", hooksup.Snap) 322 } 323 324 info, err := snapst.CurrentInfo() 325 if err != nil { 326 return fmt.Errorf("cannot read %q snap details: %v", hooksup.Snap, err) 327 } 328 329 hookExists = info.Hooks[hooksup.Hook] != nil 330 if !hookExists && !hooksup.Optional { 331 return fmt.Errorf("snap %q has no %q hook", hooksup.Snap, hooksup.Hook) 332 } 333 } 334 335 if hookExists || mustHijack { 336 // we will run something, not a noop 337 if ok, _ := m.state.Restarting(); ok { 338 // don't start running a hook if we are restarting 339 return &state.Retry{} 340 } 341 342 // keep count of running hooks 343 atomic.AddInt32(&m.runningHooks, 1) 344 defer atomic.AddInt32(&m.runningHooks, -1) 345 } else if !hooksup.Always { 346 // a noop with no 'always' flag: bail 347 return nil 348 } 349 350 // Obtain a handler for this hook. The repository returns a list since it's 351 // possible for regular expressions to overlap, but multiple handlers is an 352 // error (as is no handler). 353 handlers := m.repository.generateHandlers(context) 354 handlersCount := len(handlers) 355 if handlersCount == 0 { 356 // Do not report error if hook handler doesn't exist as long as the hook is optional. 357 // This is to avoid issues when downgrading to an old core snap that doesn't know about 358 // particular hook type and a task for it exists (e.g. "post-refresh" hook). 359 if hooksup.Optional { 360 return nil 361 } 362 return fmt.Errorf("internal error: no registered handlers for hook %q", hooksup.Hook) 363 } 364 if handlersCount > 1 { 365 return fmt.Errorf("internal error: %d handlers registered for hook %q, expected 1", handlersCount, hooksup.Hook) 366 } 367 context.handler = handlers[0] 368 369 contextID := context.ID() 370 m.contextsMutex.Lock() 371 m.contexts[contextID] = context 372 m.contextsMutex.Unlock() 373 374 defer func() { 375 m.contextsMutex.Lock() 376 delete(m.contexts, contextID) 377 m.contextsMutex.Unlock() 378 }() 379 380 if err := context.Handler().Before(); err != nil { 381 return err 382 } 383 384 // some hooks get hijacked, e.g. the core configuration 385 var err error 386 var output []byte 387 if f := m.hijacked(hooksup.Hook, hooksup.Snap); f != nil { 388 err = f(context) 389 } else if hookExists { 390 output, err = runHook(context, tomb) 391 } 392 if err != nil { 393 if hooksup.TrackError { 394 trackHookError(context, output, err) 395 } 396 err = osutil.OutputErr(output, err) 397 if hooksup.IgnoreError { 398 context.Lock() 399 context.Errorf("ignoring failure in hook %q: %v", hooksup.Hook, err) 400 context.Unlock() 401 } else { 402 if handlerErr := context.Handler().Error(err); handlerErr != nil { 403 return handlerErr 404 } 405 406 return fmt.Errorf("run hook %q: %v", hooksup.Hook, err) 407 } 408 } 409 410 if err = context.Handler().Done(); err != nil { 411 return err 412 } 413 414 context.Lock() 415 defer context.Unlock() 416 if err = context.Done(); err != nil { 417 return err 418 } 419 420 return nil 421 } 422 423 func runHookImpl(c *Context, tomb *tomb.Tomb) ([]byte, error) { 424 return runHookAndWait(c.InstanceName(), c.SnapRevision(), c.HookName(), c.ID(), c.Timeout(), tomb) 425 } 426 427 var runHook = runHookImpl 428 429 // MockRunHook mocks the actual invocation of hooks for tests. 430 func MockRunHook(hookInvoke func(c *Context, tomb *tomb.Tomb) ([]byte, error)) (restore func()) { 431 oldRunHook := runHook 432 runHook = hookInvoke 433 return func() { 434 runHook = oldRunHook 435 } 436 } 437 438 var osReadlink = os.Readlink 439 440 // snapCmd returns the "snap" command to run. If snapd is re-execed 441 // it will be the snap command from the core snap, otherwise it will 442 // be the system "snap" command (c.f. LP: #1668738). 443 func snapCmd() string { 444 // sensible default, assume PATH is correct 445 snapCmd := "snap" 446 447 exe, err := osReadlink("/proc/self/exe") 448 if err != nil { 449 logger.Noticef("cannot read /proc/self/exe: %v, using default snap command", err) 450 return snapCmd 451 } 452 if !strings.HasPrefix(exe, dirs.SnapMountDir) { 453 return snapCmd 454 } 455 456 // snap is running from the core snap, we know the relative 457 // location of "snap" from "snapd" 458 return filepath.Join(filepath.Dir(exe), "../../bin/snap") 459 } 460 461 var defaultHookTimeout = 10 * time.Minute 462 463 func runHookAndWait(snapName string, revision snap.Revision, hookName, hookContext string, timeout time.Duration, tomb *tomb.Tomb) ([]byte, error) { 464 argv := []string{snapCmd(), "run", "--hook", hookName, "-r", revision.String(), snapName} 465 if timeout == 0 { 466 timeout = defaultHookTimeout 467 } 468 469 env := []string{ 470 // Make sure the hook has its context defined so it can 471 // communicate via the REST API. 472 fmt.Sprintf("SNAP_COOKIE=%s", hookContext), 473 // Set SNAP_CONTEXT too for compatibility with old snapctl 474 // binary when transitioning to a new core - otherwise configure 475 // hook would fail during transition. 476 fmt.Sprintf("SNAP_CONTEXT=%s", hookContext), 477 } 478 479 return osutil.RunAndWait(argv, env, timeout, tomb) 480 } 481 482 var errtrackerReport = errtracker.Report 483 484 func trackHookError(context *Context, output []byte, err error) { 485 errmsg := fmt.Sprintf("hook %s in snap %q failed: %v", context.HookName(), context.InstanceName(), osutil.OutputErr(output, err)) 486 dupSig := fmt.Sprintf("hook:%s:%s:%s\n%s", context.InstanceName(), context.HookName(), err, output) 487 extra := map[string]string{ 488 "HookName": context.HookName(), 489 } 490 if context.setup.IgnoreError { 491 extra["IgnoreError"] = "1" 492 } 493 494 context.state.Lock() 495 problemReportsDisabled := settings.ProblemReportsDisabled(context.state) 496 context.state.Unlock() 497 if !problemReportsDisabled { 498 oopsid, err := errtrackerReport(context.InstanceName(), errmsg, dupSig, extra) 499 if err == nil { 500 logger.Noticef("Reported hook failure from %q for snap %q as %s", context.HookName(), context.InstanceName(), oopsid) 501 } else { 502 logger.Debugf("Cannot report hook failure: %s", err) 503 } 504 } 505 }