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