// runHook executes the supplied hook.Info in an appropriate hook context. If // the hook itself fails to execute, it returns errHookFailed. func (u *Uniter) runHook(hi hook.Info) (err error) { // Prepare context. if err = hi.Validate(); err != nil { return err } hookName := string(hi.Kind) relationId := -1 if hi.Kind.IsRelation() { relationId = hi.RelationId if hookName, err = u.relationers[relationId].PrepareHook(hi); err != nil { return err } } hctxId := fmt.Sprintf("%s:%s:%d", u.unit.Name(), hookName, u.rand.Int63()) lockMessage := fmt.Sprintf("%s: running hook %q", u.unit.Name(), hookName) if err = u.acquireHookLock(lockMessage); err != nil { return err } defer u.hookLock.Unlock() hctx, err := u.getHookContext(hctxId, relationId, hi.RemoteUnit) if err != nil { return err } srv, socketPath, err := u.startJujucServer(hctx) if err != nil { return err } defer srv.Close() // Run the hook. if err := u.writeState(RunHook, Pending, &hi, nil); err != nil { return err } logger.Infof("running %q hook", hookName) ranHook := true err = hctx.RunHook(hookName, u.charmPath, u.toolsDir, socketPath) if IsMissingHookError(err) { ranHook = false } else if err != nil { logger.Errorf("hook failed: %s", err) u.notifyHookFailed(hookName, hctx) return errHookFailed } if err := u.writeState(RunHook, Done, &hi, nil); err != nil { return err } if ranHook { logger.Infof("ran %q hook", hookName) u.notifyHookCompleted(hookName, hctx) } else { logger.Infof("skipped %q hook (missing)", hookName) } return u.commitHook(hi) }
// NewRunHook is part of the Factory interface. func (f *factory) NewRunHook(hookInfo hook.Info) (Operation, error) { if err := hookInfo.Validate(); err != nil { return nil, err } return &runHook{ info: hookInfo, callbacks: f.config.Callbacks, runnerFactory: f.config.RunnerFactory, }, nil }
func (s *storageResolver) nextHookOp( tag names.StorageTag, snap remotestate.StorageSnapshot, opFactory operation.Factory, ) (operation.Operation, error) { logger.Debugf("next hook op for %v: %+v", tag, snap) if !snap.Attached { return nil, resolver.ErrNoOperation } storageAttachment, ok := s.storage.storageAttachments[tag] if !ok { return nil, resolver.ErrNoOperation } switch snap.Life { case params.Alive: if storageAttachment.attached { // Storage attachments currently do not change // (apart from lifecycle) after being provisioned. // We don't process unprovisioned storage here, // so there's nothing to do. return nil, resolver.ErrNoOperation } case params.Dying: if !storageAttachment.attached { // Nothing to do: attachment is dying, but // the storage-attached hook has not been // consumed. return nil, resolver.ErrNoOperation } case params.Dead: // Storage must have been Dying to become Dead; // no further action is required. return nil, resolver.ErrNoOperation } hookInfo := hook.Info{ StorageId: tag.Id(), } if snap.Life == params.Alive { hookInfo.Kind = hooks.StorageAttached } else { hookInfo.Kind = hooks.StorageDetaching } context := &contextStorage{ tag: tag, kind: storage.StorageKind(snap.Kind), location: snap.Location, } storageAttachment.ContextStorageAttachment = context s.storage.storageAttachments[tag] = storageAttachment logger.Debugf("queued hook: %v", hookInfo) return opFactory.NewRunHook(hookInfo) }
// NewHookRunner exists to satisfy the Factory interface. func (f *factory) NewHookRunner(hookInfo hook.Info) (Runner, error) { if err := hookInfo.Validate(); err != nil { return nil, errors.Trace(err) } ctx, err := f.contextFactory.HookContext(hookInfo) if err != nil { return nil, errors.Trace(err) } runner := NewRunner(ctx, f.paths) return runner, nil }
// Commit updates relation state to include the fact of the hook's execution, // records the impact of start and collect-metrics hooks, and queues follow-up // config-changed hooks to directly follow install and upgrade-charm hooks. // Commit is part of the Operation interface. func (rh *runHook) Commit(state State) (*State, error) { if err := rh.callbacks.CommitHook(rh.info); err != nil { return nil, err } change := stateChange{ Kind: Continue, Step: Pending, } var hi *hook.Info = &hook.Info{Kind: hooks.ConfigChanged} switch rh.info.Kind { case hooks.ConfigChanged: if state.Started { break } hi.Kind = hooks.Start fallthrough case hooks.UpgradeCharm: change = stateChange{ Kind: RunHook, Step: Queued, Hook: hi, } } newState := change.apply(state) switch rh.info.Kind { case hooks.Start: newState.Started = true case hooks.Stop: newState.Stopped = true case hooks.CollectMetrics: newState.CollectMetricsTime = time.Now().Unix() case hooks.UpdateStatus: newState.UpdateStatusTime = time.Now().Unix() } return newState, nil }
func (s *RelationerSuite) assertHook(c *gc.C, expect hook.Info) { s.BackingState.StartSync() // We must ensure the local state dir exists first. c.Assert(s.dir.Ensure(), gc.IsNil) select { case hi, ok := <-s.hooks: c.Assert(ok, gc.Equals, true) expect.ChangeVersion = hi.ChangeVersion c.Assert(hi, gc.DeepEquals, expect) c.Assert(s.dir.Write(hi), gc.Equals, nil) case <-time.After(coretesting.LongWait): c.Fatalf("timed out waiting for %#v", expect) } }
func (s *RelationerSuite) TestPrepareCommitHooks(c *gc.C) { r := uniter.NewRelationer(s.apiRelUnit, s.dir, s.hooks) err := r.Join() c.Assert(err, gc.IsNil) ctx := r.Context() c.Assert(ctx.UnitNames(), gc.HasLen, 0) // Check preparing an invalid hook changes nothing. changed := hook.Info{ Kind: hooks.RelationChanged, RemoteUnit: "u/1", ChangeVersion: 7, } _, err = r.PrepareHook(changed) c.Assert(err, gc.ErrorMatches, `inappropriate "relation-changed" for "u/1": unit has not joined`) c.Assert(ctx.UnitNames(), gc.HasLen, 0) c.Assert(s.dir.State().Members, gc.HasLen, 0) // Check preparing a valid hook updates the context, but not persistent // relation state. joined := hook.Info{ Kind: hooks.RelationJoined, RemoteUnit: "u/1", } name, err := r.PrepareHook(joined) c.Assert(err, gc.IsNil) c.Assert(s.dir.State().Members, gc.HasLen, 0) c.Assert(name, gc.Equals, "ring-relation-joined") c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"}) // Check that preparing the following hook fails as before... _, err = r.PrepareHook(changed) c.Assert(err, gc.ErrorMatches, `inappropriate "relation-changed" for "u/1": unit has not joined`) c.Assert(s.dir.State().Members, gc.HasLen, 0) c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"}) // ...but that committing the previous hook updates the persistent // relation state... err = r.CommitHook(joined) c.Assert(err, gc.IsNil) c.Assert(s.dir.State().Members, gc.DeepEquals, map[string]int64{"u/1": 0}) c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"}) // ...and allows us to prepare the next hook... name, err = r.PrepareHook(changed) c.Assert(err, gc.IsNil) c.Assert(name, gc.Equals, "ring-relation-changed") c.Assert(s.dir.State().Members, gc.DeepEquals, map[string]int64{"u/1": 0}) c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"}) // ...and commit it. err = r.CommitHook(changed) c.Assert(err, gc.IsNil) c.Assert(s.dir.State().Members, gc.DeepEquals, map[string]int64{"u/1": 7}) c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"}) // To verify implied behaviour above, prepare a new joined hook with // missing membership information, and check relation context // membership is updated appropriately... joined.RemoteUnit = "u/2" joined.ChangeVersion = 3 name, err = r.PrepareHook(joined) c.Assert(err, gc.IsNil) c.Assert(s.dir.State().Members, gc.HasLen, 1) c.Assert(name, gc.Equals, "ring-relation-joined") c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1", "u/2"}) // ...and so is relation state on commit. err = r.CommitHook(joined) c.Assert(err, gc.IsNil) c.Assert(s.dir.State().Members, gc.DeepEquals, map[string]int64{"u/1": 7, "u/2": 3}) c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1", "u/2"}) }
func (s *RelationerSuite) TestPrepareCommitHooks(c *gc.C) { r := uniter.NewRelationer(s.apiRelUnit, s.dir, s.hooks) err := r.Join() c.Assert(err, jc.ErrorIsNil) assertMembers := func(expect map[string]int64) { c.Assert(s.dir.State().Members, jc.DeepEquals, expect) expectNames := make([]string, 0, len(expect)) for name := range expect { expectNames = append(expectNames, name) } c.Assert(r.ContextInfo().MemberNames, jc.SameContents, expectNames) } assertMembers(map[string]int64{}) // Check preparing an invalid hook changes nothing. changed := hook.Info{ Kind: hooks.RelationChanged, RemoteUnit: "u/1", ChangeVersion: 7, } _, err = r.PrepareHook(changed) c.Assert(err, gc.ErrorMatches, `inappropriate "relation-changed" for "u/1": unit has not joined`) assertMembers(map[string]int64{}) // Check preparing a valid hook updates neither the context nor persistent // relation state. joined := hook.Info{ Kind: hooks.RelationJoined, RemoteUnit: "u/1", } name, err := r.PrepareHook(joined) c.Assert(err, jc.ErrorIsNil) c.Assert(name, gc.Equals, "ring-relation-joined") assertMembers(map[string]int64{}) // Check that preparing the following hook fails as before... _, err = r.PrepareHook(changed) c.Assert(err, gc.ErrorMatches, `inappropriate "relation-changed" for "u/1": unit has not joined`) assertMembers(map[string]int64{}) // ...but that committing the previous hook updates the persistent // relation state... err = r.CommitHook(joined) c.Assert(err, jc.ErrorIsNil) assertMembers(map[string]int64{"u/1": 0}) // ...and allows us to prepare the next hook... name, err = r.PrepareHook(changed) c.Assert(err, jc.ErrorIsNil) c.Assert(name, gc.Equals, "ring-relation-changed") assertMembers(map[string]int64{"u/1": 0}) // ...and commit it. err = r.CommitHook(changed) c.Assert(err, jc.ErrorIsNil) assertMembers(map[string]int64{"u/1": 7}) // To verify implied behaviour above, prepare a new joined hook with // missing membership information, and check relation context // membership is stil not updated... joined.RemoteUnit = "u/2" joined.ChangeVersion = 3 name, err = r.PrepareHook(joined) c.Assert(err, jc.ErrorIsNil) c.Assert(name, gc.Equals, "ring-relation-joined") assertMembers(map[string]int64{"u/1": 7}) // ...until commit, at which point so is relation state. err = r.CommitHook(joined) c.Assert(err, jc.ErrorIsNil) assertMembers(map[string]int64{"u/1": 7, "u/2": 3}) }
func (s *storageResolver) nextHookOp( tag names.StorageTag, snap remotestate.StorageSnapshot, opFactory operation.Factory, ) (operation.Operation, error) { logger.Debugf("next hook op for %v: %+v", tag, snap) if snap.Life == params.Dead { // Storage must have been Dying to become Dead; // no further action is required. return nil, resolver.ErrNoOperation } hookInfo := hook.Info{StorageId: tag.Id()} switch snap.Life { case params.Alive: storageAttachment, ok := s.storage.storageAttachments[tag] if ok && storageAttachment.attached { // Once the storage is attached, we only care about // lifecycle state changes. return nil, resolver.ErrNoOperation } // The storage-attached hook has not been committed, so add the // storage to the pending set. s.storage.pending.Add(tag) if !snap.Attached { // The storage attachment has not been provisioned yet, // so just ignore it for now. We'll be notified again // when it has been provisioned. return nil, resolver.ErrNoOperation } // The storage is alive, but we haven't previously run the // "storage-attached" hook. Do so now. hookInfo.Kind = hooks.StorageAttached case params.Dying: storageAttachment, ok := s.storage.storageAttachments[tag] if !ok || !storageAttachment.attached { // Nothing to do: attachment is dying, but // the storage-attached hook has not been // issued. return nil, resolver.ErrNoOperation } // The storage is dying, but we haven't previously run the // "storage-detached" hook. Do so now. hookInfo.Kind = hooks.StorageDetaching } // Update the local state to reflect what we're about to report // to a hook. stateFile, err := readStateFile(s.storage.storageStateDir, tag) if err != nil { return nil, errors.Trace(err) } s.storage.storageAttachments[tag] = storageAttachment{ stateFile, &contextStorage{ tag: tag, kind: storage.StorageKind(snap.Kind), location: snap.Location, }, } return opFactory.NewRunHook(hookInfo) }
// runHook executes the supplied hook.Info in an appropriate hook context. If // the hook itself fails to execute, it returns errHookFailed. func (u *Uniter) runHook(hi hook.Info) (err error) { // Prepare context. if err = hi.Validate(); err != nil { return err } hookName := string(hi.Kind) actionParams := map[string]interface{}(nil) relationId := -1 if hi.Kind.IsRelation() { relationId = hi.RelationId if hookName, err = u.relationers[relationId].PrepareHook(hi); err != nil { return err } } else if hi.Kind == hooks.ActionRequested { action, err := u.st.Action(names.NewActionTag(hi.ActionId)) if err != nil { return err } actionParams = action.Params() hookName = action.Name() } hctxId := fmt.Sprintf("%s:%s:%d", u.unit.Name(), hookName, u.rand.Int63()) lockMessage := fmt.Sprintf("%s: running hook %q", u.unit.Name(), hookName) if err = u.acquireHookLock(lockMessage); err != nil { return err } defer u.hookLock.Unlock() hctx, err := u.getHookContext(hctxId, relationId, hi.RemoteUnit, actionParams) if err != nil { return err } srv, socketPath, err := u.startJujucServer(hctx) if err != nil { return err } defer srv.Close() // Run the hook. if err := u.writeState(RunHook, Pending, &hi, nil); err != nil { return err } logger.Infof("running %q hook", hookName) ranHook := true // The reason for the switch at this point is that once inside RunHook, // we don't know whether we're running an Action or a regular Hook. // RunAction simply calls the exact same method as RunHook, but with // the location as "actions" instead of "hooks". if hi.Kind == hooks.ActionRequested { err = hctx.RunAction(hookName, u.charmPath, u.toolsDir, socketPath) } else { err = hctx.RunHook(hookName, u.charmPath, u.toolsDir, socketPath) } if IsMissingHookError(err) { ranHook = false } else if err != nil { logger.Errorf("hook failed: %s", err) u.notifyHookFailed(hookName, hctx) return errHookFailed } if err := u.writeState(RunHook, Done, &hi, nil); err != nil { return err } if ranHook { logger.Infof("ran %q hook", hookName) u.notifyHookCompleted(hookName, hctx) } else { logger.Infof("skipped %q hook (missing)", hookName) } return u.commitHook(hi) }
// deploy deploys the supplied charm URL, and sets follow-up hook operation state // as indicated by reason. func (u *Uniter) deploy(curl *corecharm.URL, reason Op) error { if reason != Install && reason != Upgrade { panic(fmt.Errorf("%q is not a deploy operation", reason)) } var hi *hook.Info if u.s != nil && (u.s.Op == RunHook || u.s.Op == Upgrade) { // If this upgrade interrupts a RunHook, we need to preserve the hook // info so that we can return to the appropriate error state. However, // if we're resuming (or have force-interrupted) an Upgrade, we also // need to preserve whatever hook info was preserved when we initially // started upgrading, to ensure we still return to the correct state. hi = u.s.Hook } if u.s == nil || u.s.OpStep != Done { // Get the new charm bundle before announcing intention to use it. logger.Infof("fetching charm %q", curl) sch, err := u.st.Charm(curl) if err != nil { return err } if err = u.deployer.Stage(sch, u.tomb.Dying()); err != nil { return err } // Set the new charm URL - this returns when the operation is complete, // at which point we can refresh the local copy of the unit to get a // version with the correct charm URL, and can go ahead and deploy // the charm proper. if err := u.f.SetCharm(curl); err != nil { return err } if err := u.unit.Refresh(); err != nil { return err } logger.Infof("deploying charm %q", curl) if err = u.writeState(reason, Pending, hi, curl); err != nil { return err } if err = u.deployer.Deploy(); err != nil { return err } if err = u.writeState(reason, Done, hi, curl); err != nil { return err } } logger.Infof("charm %q is deployed", curl) status := Queued if hi != nil { // If a hook operation was interrupted, restore it. status = Pending } else { // Otherwise, queue the relevant post-deploy hook. hi = &hook.Info{} switch reason { case Install: hi.Kind = hooks.Install case Upgrade: hi.Kind = hooks.UpgradeCharm } } return u.writeState(RunHook, status, hi, nil) }
// NewHookRunner exists to satisfy the Factory interface. func (f *factory) NewHookRunner(hookInfo hook.Info) (Runner, error) { if err := hookInfo.Validate(); err != nil { return nil, errors.Trace(err) } ctx, err := f.coreContext() if err != nil { return nil, errors.Trace(err) } hookName := string(hookInfo.Kind) if hookInfo.Kind.IsRelation() { ctx.relationId = hookInfo.RelationId ctx.remoteUnitName = hookInfo.RemoteUnit relation, found := ctx.relations[hookInfo.RelationId] if !found { return nil, errors.Errorf("unknown relation id: %v", hookInfo.RelationId) } if hookInfo.Kind == hooks.RelationDeparted { relation.cache.RemoveMember(hookInfo.RemoteUnit) } else if hookInfo.RemoteUnit != "" { // Clear remote settings cache for changing remote unit. relation.cache.InvalidateMember(hookInfo.RemoteUnit) } hookName = fmt.Sprintf("%s-%s", relation.Name(), hookInfo.Kind) } if hookInfo.Kind.IsStorage() { ctx.storageTag = names.NewStorageTag(hookInfo.StorageId) if _, found := ctx.storage.Storage(ctx.storageTag); !found { return nil, errors.Errorf("unknown storage id: %v", hookInfo.StorageId) } storageName, err := names.StorageName(hookInfo.StorageId) if err != nil { return nil, errors.Trace(err) } hookName = fmt.Sprintf("%s-%s", storageName, hookName) } // Metrics are only sent from the collect-metrics hook. if hookInfo.Kind == hooks.CollectMetrics { ch, err := f.getCharm() if err != nil { return nil, errors.Trace(err) } ctx.definedMetrics = ch.Metrics() chURL, err := f.unit.CharmURL() if err != nil { return nil, errors.Trace(err) } charmMetrics := map[string]charm.Metric{} if ch.Metrics() != nil { charmMetrics = ch.Metrics().Metrics } ctx.metricsRecorder, err = metrics.NewJSONMetricRecorder( f.paths.GetMetricsSpoolDir(), charmMetrics, chURL.String()) if err != nil { return nil, errors.Trace(err) } } ctx.id = f.newId(hookName) runner := NewRunner(ctx, f.paths) return runner, nil }
// runHook executes the supplied hook.Info in an appropriate hook context. If // the hook itself fails to execute, it returns errHookFailed. func (u *Uniter) runHook(hi hook.Info) (err error) { // Prepare context. if err = hi.Validate(); err != nil { return err } hookName := string(hi.Kind) actionParams := map[string]interface{}(nil) // This value is needed to pass results of Action param validation // in case of error or invalidation. This is probably bad form; it // will be corrected in PR refactoring HookContext. // TODO(binary132): handle errors before grabbing hook context. var actionParamsErr error = nil relationId := -1 if hi.Kind.IsRelation() { relationId = hi.RelationId if hookName, err = u.relationers[relationId].PrepareHook(hi); err != nil { return err } } else if hi.Kind == hooks.ActionRequested { action, err := u.st.Action(names.NewActionTag(hi.ActionId)) if err != nil { return err } actionParams = action.Params() hookName = action.Name() _, actionParamsErr = u.validateAction(hookName, actionParams) } hctxId := fmt.Sprintf("%s:%s:%d", u.unit.Name(), hookName, u.rand.Int63()) lockMessage := fmt.Sprintf("%s: running hook %q", u.unit.Name(), hookName) if err = u.acquireHookLock(lockMessage); err != nil { return err } defer u.hookLock.Unlock() hctx, err := u.getHookContext(hctxId, relationId, hi.RemoteUnit, actionParams) if err != nil { return err } srv, socketPath, err := u.startJujucServer(hctx) if err != nil { return err } defer srv.Close() // Run the hook. if err := u.writeState(RunHook, Pending, &hi, nil); err != nil { return err } logger.Infof("running %q hook", hookName) ranHook := true // The reason for the conditional at this point is that once inside // RunHook, we don't know whether we're running an Action or a regular // Hook. RunAction simply calls the exact same method as RunHook, but // with the location as "actions" instead of "hooks". if hi.Kind == hooks.ActionRequested { if actionParamsErr != nil { logger.Errorf("action %q param validation failed: %s", hookName, actionParamsErr.Error()) u.notifyHookFailed(hookName, hctx) return u.commitHook(hi) } err = hctx.RunAction(hookName, u.charmPath, u.toolsDir, socketPath) } else { err = hctx.RunHook(hookName, u.charmPath, u.toolsDir, socketPath) } // Since the Action validation error was separated, regular error pathways // will still occur correctly. if IsMissingHookError(err) { ranHook = false } else if err != nil { logger.Errorf("hook failed: %s", err) u.notifyHookFailed(hookName, hctx) return errHookFailed } if err := u.writeState(RunHook, Done, &hi, nil); err != nil { return err } if ranHook { logger.Infof("ran %q hook", hookName) u.notifyHookCompleted(hookName, hctx) } else { logger.Infof("skipped %q hook (missing)", hookName) } return u.commitHook(hi) }