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