| // Copyright 2018 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package agentendpoint |
| |
| import ( |
| "context" |
| "fmt" |
| "time" |
| |
| "github.com/GoogleCloudPlatform/osconfig/agentconfig" |
| "github.com/GoogleCloudPlatform/osconfig/clog" |
| "github.com/GoogleCloudPlatform/osconfig/ospatch" |
| "google.golang.org/protobuf/encoding/protojson" |
| |
| "cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb" |
| ) |
| |
| func systemRebootRequired(ctx context.Context) (bool, error) { |
| return ospatch.SystemRebootRequired(ctx) |
| } |
| |
| type patchStep string |
| |
| const ( |
| prePatch = "PrePatch" |
| patching = "Patching" |
| postPatch = "PostPatch" |
| totalRebootCountLimit = 5 |
| ) |
| |
| type patchTask struct { |
| client *Client |
| |
| lastProgressState map[agentendpointpb.ApplyPatchesTaskProgress_State]time.Time |
| state *taskState |
| |
| TaskID string |
| Task *applyPatchesTask |
| StartedAt time.Time `json:",omitempty"` |
| PatchStep patchStep `json:",omitempty"` |
| PrePatchRebootCount int |
| PostPatchRebootCount int |
| |
| // TODO: add Attempts and track number of retries with backoff, jitter, etc. |
| } |
| |
| func (r *patchTask) saveState() error { |
| r.state.PatchTask = r |
| return r.state.save(taskStateFile) |
| } |
| |
| func (r *patchTask) complete(ctx context.Context) { |
| if err := (&taskState{}).save(taskStateFile); err != nil { |
| clog.Errorf(ctx, "Error saving state: %v", err) |
| } |
| } |
| |
| type applyPatchesTask struct { |
| *agentendpointpb.ApplyPatchesTask |
| } |
| |
| // MarshalJSON marshals a patchConfig using protojson. |
| func (a *applyPatchesTask) MarshalJSON() ([]byte, error) { |
| m := &protojson.MarshalOptions{AllowPartial: true, EmitUnpopulated: false} |
| return m.Marshal(a) |
| } |
| |
| // UnmarshalJSON unmarshals a patchConfig using protojson. |
| func (a *applyPatchesTask) UnmarshalJSON(b []byte) error { |
| a.ApplyPatchesTask = &agentendpointpb.ApplyPatchesTask{} |
| un := &protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true} |
| return un.Unmarshal(b, a.ApplyPatchesTask) |
| } |
| |
| func (r *patchTask) setStep(step patchStep) error { |
| r.PatchStep = step |
| if err := r.saveState(); err != nil { |
| return fmt.Errorf("error saving state: %v", err) |
| } |
| return nil |
| } |
| |
| func (r *patchTask) handleErrorState(ctx context.Context, msg string, err error) error { |
| if err == errServerCancel { |
| return r.reportCanceled(ctx) |
| } |
| return r.reportFailed(ctx, msg) |
| } |
| |
| func (r *patchTask) reportFailed(ctx context.Context, msg string) error { |
| clog.Errorf(ctx, "%v", msg) |
| return r.reportCompletedState(ctx, msg, &agentendpointpb.ReportTaskCompleteRequest_ApplyPatchesTaskOutput{ |
| ApplyPatchesTaskOutput: &agentendpointpb.ApplyPatchesTaskOutput{State: agentendpointpb.ApplyPatchesTaskOutput_FAILED}, |
| }) |
| } |
| |
| func (r *patchTask) reportCanceled(ctx context.Context) error { |
| clog.Infof(ctx, "Canceling patch execution") |
| return r.reportCompletedState(ctx, errServerCancel.Error(), &agentendpointpb.ReportTaskCompleteRequest_ApplyPatchesTaskOutput{ |
| // Is this right? Maybe there should be a canceled state instead. |
| ApplyPatchesTaskOutput: &agentendpointpb.ApplyPatchesTaskOutput{State: agentendpointpb.ApplyPatchesTaskOutput_FAILED}, |
| }) |
| } |
| |
| func (r *patchTask) reportCompletedState(ctx context.Context, errMsg string, output *agentendpointpb.ReportTaskCompleteRequest_ApplyPatchesTaskOutput) error { |
| req := &agentendpointpb.ReportTaskCompleteRequest{ |
| TaskId: r.TaskID, |
| TaskType: agentendpointpb.TaskType_APPLY_PATCHES, |
| ErrorMessage: errMsg, |
| Output: output, |
| } |
| if err := r.client.reportTaskComplete(ctx, req); err != nil { |
| return fmt.Errorf("error reporting completed state: %v", err) |
| } |
| return nil |
| } |
| |
| func (r *patchTask) reportContinuingState(ctx context.Context, patchState agentendpointpb.ApplyPatchesTaskProgress_State) error { |
| st, ok := r.lastProgressState[patchState] |
| if ok && st.After(time.Now().Add(sameStateTimeWindow)) { |
| // Don't resend the same state more than once every 5s. |
| return nil |
| } |
| |
| req := &agentendpointpb.ReportTaskProgressRequest{ |
| TaskId: r.TaskID, |
| TaskType: agentendpointpb.TaskType_APPLY_PATCHES, |
| Progress: &agentendpointpb.ReportTaskProgressRequest_ApplyPatchesTaskProgress{ |
| ApplyPatchesTaskProgress: &agentendpointpb.ApplyPatchesTaskProgress{State: patchState}, |
| }, |
| } |
| res, err := r.client.reportTaskProgress(ctx, req) |
| if err != nil { |
| return fmt.Errorf("error reporting state %s: %v", patchState, err) |
| } |
| if res.GetTaskDirective() == agentendpointpb.TaskDirective_STOP { |
| return errServerCancel |
| } |
| |
| if r.lastProgressState == nil { |
| r.lastProgressState = make(map[agentendpointpb.ApplyPatchesTaskProgress_State]time.Time) |
| } |
| r.lastProgressState[patchState] = time.Now() |
| return r.saveState() |
| } |
| |
| func (r *patchTask) prePatchReboot(ctx context.Context) error { |
| return r.rebootIfNeeded(ctx, true) |
| } |
| |
| func (r *patchTask) postPatchReboot(ctx context.Context) error { |
| return r.rebootIfNeeded(ctx, false) |
| } |
| |
| func (r *patchTask) rebootIfNeeded(ctx context.Context, prePatch bool) error { |
| var reboot bool |
| var err error |
| if r.Task.GetPatchConfig().GetRebootConfig() == agentendpointpb.PatchConfig_ALWAYS && !prePatch && r.PostPatchRebootCount == 0 { |
| reboot = true |
| clog.Infof(ctx, "PatchConfig RebootConfig set to %s.", agentendpointpb.PatchConfig_ALWAYS) |
| } else { |
| reboot, err = systemRebootRequired(ctx) |
| if err != nil { |
| return fmt.Errorf("error checking if a system reboot is required: %v", err) |
| } |
| if reboot { |
| clog.Infof(ctx, "System indicates a reboot is required.") |
| totalRebootCount := r.PrePatchRebootCount + r.PostPatchRebootCount |
| if totalRebootCount >= totalRebootCountLimit { |
| clog.Infof(ctx, "Detected abnormal number of reboots for a single patch task (%d). Not rebooting to prevent a possible boot loop", totalRebootCount) |
| return nil |
| } |
| } else { |
| clog.Infof(ctx, "System indicates a reboot is not required.") |
| } |
| } |
| |
| if !reboot { |
| return nil |
| } |
| |
| if r.Task.GetPatchConfig().GetRebootConfig() == agentendpointpb.PatchConfig_NEVER { |
| clog.Infof(ctx, "Skipping reboot because of PatchConfig RebootConfig set to %s.", agentendpointpb.PatchConfig_NEVER) |
| return nil |
| } |
| |
| if err := r.reportContinuingState(ctx, agentendpointpb.ApplyPatchesTaskProgress_REBOOTING); err != nil { |
| return err |
| } |
| |
| if r.Task.GetDryRun() { |
| clog.Infof(ctx, "Dry run - not rebooting for ApplyPatchesTask") |
| return nil |
| } |
| |
| if prePatch { |
| r.PrePatchRebootCount++ |
| } else { |
| r.PostPatchRebootCount++ |
| } |
| |
| if err := r.saveState(); err != nil { |
| return fmt.Errorf("error saving state: %v", err) |
| } |
| if err := rebootSystem(); err != nil { |
| return fmt.Errorf("failed to reboot system: %v", err) |
| } |
| |
| // Reboot can take a bit, pause here so other activities don't start. |
| for { |
| clog.Debugf(ctx, "Waiting for system reboot.") |
| time.Sleep(1 * time.Minute) |
| } |
| } |
| |
| func (r *patchTask) run(ctx context.Context) (err error) { |
| ctx = clog.WithLabels(ctx, r.state.Labels) |
| clog.Infof(ctx, "Beginning ApplyPatchesTask") |
| defer func() { |
| // This should not happen but the WUA libraries are complicated and |
| // recovering with an error is better than crashing. |
| if rec := recover(); rec != nil { |
| err = fmt.Errorf("Recovered from panic: %v", rec) |
| r.reportFailed(ctx, err.Error()) |
| return |
| } |
| r.complete(ctx) |
| if agentconfig.OSInventoryEnabled() { |
| go r.client.ReportInventory(ctx) |
| } |
| }() |
| |
| for { |
| clog.Debugf(ctx, "Running PatchStep %q.", r.PatchStep) |
| switch r.PatchStep { |
| default: |
| return r.reportFailed(ctx, fmt.Sprintf("unknown step: %q", r.PatchStep)) |
| case prePatch: |
| r.StartedAt = time.Now() |
| if err := r.setStep(patching); err != nil { |
| return r.reportFailed(ctx, fmt.Sprintf("Error saving agent step: %v", err)) |
| } |
| |
| if err := r.reportContinuingState(ctx, agentendpointpb.ApplyPatchesTaskProgress_STARTED); err != nil { |
| return r.handleErrorState(ctx, err.Error(), err) |
| } |
| if err := r.prePatchReboot(ctx); err != nil { |
| return r.handleErrorState(ctx, fmt.Sprintf("Error running prePatchReboot: %v", err), err) |
| } |
| case patching: |
| if err := r.reportContinuingState(ctx, agentendpointpb.ApplyPatchesTaskProgress_APPLYING_PATCHES); err != nil { |
| return r.handleErrorState(ctx, err.Error(), err) |
| } |
| if err := r.runUpdates(ctx); err != nil { |
| return r.handleErrorState(ctx, fmt.Sprintf("Failed to apply patches: %v", err), err) |
| } |
| if err := r.postPatchReboot(ctx); err != nil { |
| return r.handleErrorState(ctx, fmt.Sprintf("Error running postPatchReboot: %v", err), err) |
| } |
| // We have not rebooted so patching is complete. |
| if err := r.setStep(postPatch); err != nil { |
| return r.reportFailed(ctx, fmt.Sprintf("Error saving agent step: %v", err)) |
| } |
| case postPatch: |
| isRebootRequired, err := systemRebootRequired(ctx) |
| if err != nil { |
| return r.reportFailed(ctx, fmt.Sprintf("Error checking if system reboot is required: %v", err)) |
| } |
| |
| finalState := agentendpointpb.ApplyPatchesTaskOutput_SUCCEEDED |
| if isRebootRequired { |
| finalState = agentendpointpb.ApplyPatchesTaskOutput_SUCCEEDED_REBOOT_REQUIRED |
| } |
| |
| if err := r.reportCompletedState(ctx, "", &agentendpointpb.ReportTaskCompleteRequest_ApplyPatchesTaskOutput{ |
| ApplyPatchesTaskOutput: &agentendpointpb.ApplyPatchesTaskOutput{State: finalState}, |
| }); err != nil { |
| return fmt.Errorf("failed to report state %s: %v", finalState, err) |
| } |
| clog.Infof(ctx, "Successfully completed ApplyPatchesTask") |
| return nil |
| } |
| } |
| } |
| |
| // RunApplyPatches runs an ApplyPatchesTask. |
| func (c *Client) RunApplyPatches(ctx context.Context, task *agentendpointpb.Task) error { |
| r := &patchTask{ |
| state: &taskState{Labels: task.GetServiceLabels()}, |
| TaskID: task.GetTaskId(), |
| client: c, |
| Task: &applyPatchesTask{task.GetApplyPatchesTask()}, |
| } |
| r.setStep(prePatch) |
| |
| return r.run(ctx) |
| } |