From 7651e314fe7253a7b2f96376c88986524281e319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Mon, 23 Mar 2020 10:01:17 +0100 Subject: [PATCH] Adding download and apply commands --- app/standalone.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++ cli/cli.go | 24 +++++++- cli/commands.go | 7 +++ 3 files changed, 166 insertions(+), 1 deletion(-) diff --git a/app/standalone.go b/app/standalone.go index 7dfb5e7ed..854b72a96 100644 --- a/app/standalone.go +++ b/app/standalone.go @@ -48,6 +48,79 @@ type standaloneData struct { installers []installer.PayloadUpdatePerformer } +func DoStandaloneDownload(device *dev.DeviceManager, updateURI string, + clientConfig client.Config, + stateExec statescript.Executor) error { + + var image io.ReadCloser + var imageSize int64 + var err error + var upclient client.Updater + + if strings.HasPrefix(updateURI, "http:") || + strings.HasPrefix(updateURI, "https:") { + log.Infof("Performing remote update from: [%s].", updateURI) + + var ac *client.ApiClient + // we are having remote update + ac, err = client.NewApiClient(clientConfig) + if err != nil { + return errors.New("Can not initialize client for performing network update.") + } + upclient = client.NewUpdate() + + log.Debug("Client initialized. Start downloading image.") + + image, imageSize, err = upclient.FetchUpdate(ac, updateURI, 0) + log.Debugf("Image downloaded: %d [%v] [%v]", imageSize, image, err) + } else { + // perform update from local file + log.Infof("Start updating from local image file: [%s]", updateURI) + image, imageSize, err = installer.FetchUpdateFromFile(updateURI) + + log.Debugf("Fetching update from file results: [%v], %d, %v", image, imageSize, err) + } + + if image == nil || err != nil { + return errors.Wrapf(err, "Error while installing Artifact from command line") + } + defer image.Close() + + fmt.Fprintf(os.Stdout, "Installing Artifact of size %d...\n", imageSize) + p := utils.NewProgressWriter(imageSize) + tr := io.TeeReader(image, p) + + standaloneData, err := doStandaloneInstallStatesDownload(ioutil.NopCloser(tr), device, stateExec) + if err != nil { + return err + } + + err = storeStandaloneData(device.Store, standaloneData) + if err != nil { + log.Errorf("Could not update database: %s", err.Error()) + return err + } + + return nil +} + +// This will be run manually from command line ONLY +func DoStandaloneApply(device *dev.DeviceManager, stateExec statescript.Executor, rebootExitCode bool) error { + + standaloneData, err := restoreStandaloneData(device) + if err != nil { + log.Errorf("Could not restore standalone data: %s. Have you downloaded an image before applying it?", err.Error()) + return err + } + + err = doStandaloneInstallStatesApply(standaloneData, device, stateExec, rebootExitCode) + if err != nil { + return err + } + + return nil +} + // This will be run manually from command line ONLY func DoStandaloneInstall(device *dev.DeviceManager, updateURI string, clientConfig client.Config, @@ -180,6 +253,69 @@ func doStandaloneInstallStatesDownload(art io.ReadCloser, return standaloneData, nil } +func doStandaloneInstallStatesApply(standaloneData *standaloneData, device *dev.DeviceManager, stateExec statescript.Executor, + rebootExitCode bool) error { + + installers := standaloneData.installers + + rollbackSupport, err := determineRollbackSupport(installers) + if err != nil { + log.Error(err.Error()) + _ = doStandaloneFailureStates(device, standaloneData, stateExec, false, false, true) + return err + } + + // ArtifactInstall state + err = stateExec.ExecuteAll("ArtifactInstall", "Enter", false, nil) + if err != nil { + log.Errorf("ArtifactInstall_Enter script failed: %s", err.Error()) + callErrorScript("ArtifactInstall", stateExec) + _ = doStandaloneFailureStates(device, standaloneData, stateExec, true, true, true) + return err + } + for _, inst := range installers { + err = inst.InstallUpdate() + if err != nil { + log.Errorf("Installation failed: %s", err.Error()) + callErrorScript("ArtifactInstall", stateExec) + _ = doStandaloneFailureStates(device, standaloneData, stateExec, true, true, true) + return err + } + } + err = stateExec.ExecuteAll("ArtifactInstall", "Leave", false, nil) + if err != nil { + log.Errorf("ArtifactInstall_Leave script failed: %s", err.Error()) + callErrorScript("ArtifactInstall", stateExec) + doStandaloneFailureStates(device, standaloneData, stateExec, true, true, true) + return err + } + + rebootNeeded, err := determineRebootNeeded(installers) + if err != nil { + doStandaloneFailureStates(device, standaloneData, stateExec, true, true, true) + return err + } + + if rollbackSupport { + fmt.Println("Use 'commit' to update, or 'rollback' to roll back the update.") + } else { + fmt.Println("Artifact doesn't support rollback. Committing immediately.") + err = DoStandaloneCommit(device, stateExec) + if err != nil { + return err + } + } + + if rebootNeeded { + fmt.Println("At least one payload requested a reboot of the device it updated.") + if rebootExitCode { + return ErrorManualRebootRequired + } + } + + return nil +} + func doStandaloneInstallStates(art io.ReadCloser, device *dev.DeviceManager, stateExec statescript.Executor, rebootExitCode bool) error { diff --git a/cli/cli.go b/cli/cli.go index 6cede77ec..998cee524 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -191,6 +191,26 @@ func SetupCLI(args []string) error { }, }, }, + { + Name: "download", + Usage: "Mender Artifact to download - " + + "local file or a `URL`.", + ArgsUsage: "", + Action: func(ctx *cli.Context) error { + runOptions.imageFile = ctx.Args().First() + if len(runOptions.imageFile) == 0 { + cli.ShowAppHelpAndExit(ctx, 1) + } + return runOptions.handleCLIOptions(ctx) + }, + }, + { + Name: "apply", + Usage: "Apply the downloaded artifact", + Action: func(ctx *cli.Context) error { + return runOptions.handleCLIOptions(ctx) + }, + }, { Name: "rollback", Usage: "Rollback current Artifact. Returns (2) " + @@ -447,7 +467,7 @@ func SetupCLI(args []string) error { func (runOptions *runOptionsType) commonCLIHandler( ctx *cli.Context) (*conf.MenderConfig, error) { - if ctx.Command.Name != "install" && ctx.Args().Len() > 0 { + if (ctx.Command.Name != "install" && ctx.Command.Name != "download") && ctx.Args().Len() > 0 { return nil, errors.Errorf( errMsgAmbiguousArgumentsGivenF, ctx.Args().First()) @@ -525,6 +545,8 @@ func (runOptions *runOptionsType) handleCLIOptions(ctx *cli.Context) error { case "show-artifact", "show-provides", "install", + "download", + "apply", "commit", "rollback": return handleArtifactOperations(ctx, *runOptions, config) diff --git a/cli/commands.go b/cli/commands.go index fcb62b364..1e101d54a 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -269,6 +269,13 @@ func handleArtifactOperations(ctx *cli.Context, runOptions runOptionsType, return app.DoStandaloneInstall(deviceManager, runOptions.imageFile, runOptions.Config, stateExec, runOptions.rebootExitCode) + case "download": + return app.DoStandaloneDownload(deviceManager, runOptions.imageFile, + runOptions.Config, stateExec) + + case "apply": + return app.DoStandaloneApply(deviceManager, stateExec, runOptions.rebootExitCode) + case "commit": return app.DoStandaloneCommit(deviceManager, stateExec)