-
Notifications
You must be signed in to change notification settings - Fork 465
Import pivot code #859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
openshift-merge-robot
merged 1 commit into
openshift:master
from
cgwalters:mcd-pivot-merge
Jun 17, 2019
Merged
Import pivot code #859
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,376 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "encoding/json" | ||
| "flag" | ||
| "fmt" | ||
| "io/ioutil" | ||
| "os" | ||
| "strings" | ||
|
|
||
| // Enable sha256 in container image references | ||
| _ "crypto/sha256" | ||
|
|
||
| "github.com/golang/glog" | ||
| daemon "github.com/openshift/machine-config-operator/pkg/daemon" | ||
| "github.com/openshift/machine-config-operator/pkg/daemon/pivot/types" | ||
| "github.com/openshift/machine-config-operator/pkg/daemon/pivot/utils" | ||
| "github.com/spf13/cobra" | ||
| "github.com/spf13/pflag" | ||
| ) | ||
|
|
||
| // flag storage | ||
| var keep bool | ||
| var reboot bool | ||
| var exit77 bool | ||
|
|
||
| const ( | ||
| // the number of times to retry commands that pull data from the network | ||
| numRetriesNetCommands = 5 | ||
| etcPivotFile = "/etc/pivot/image-pullspec" | ||
| runPivotRebootFile = "/run/pivot/reboot-needed" | ||
| // Pull secret. Written by the machine-config-operator | ||
| kubeletAuthFile = "/var/lib/kubelet/config.json" | ||
| // File containing kernel arg changes for tuning | ||
| kernelTuningFile = "/etc/pivot/kernel-args" | ||
| cmdLineFile = "/proc/cmdline" | ||
| ) | ||
|
|
||
| // TODO: fill out the whitelist | ||
| // tuneableArgsWhitelist contains allowed keys for tunable arguments | ||
| var tuneableArgsWhitelist = map[string]bool{ | ||
| "nosmt": true, | ||
| } | ||
|
|
||
| var pivotCmd = &cobra.Command{ | ||
| Use: "pivot", | ||
| DisableFlagsInUseLine: true, | ||
| Short: "Allows moving from one OSTree deployment to another", | ||
| Args: cobra.MaximumNArgs(1), | ||
| Run: Execute, | ||
| } | ||
|
|
||
| // init executes upon import | ||
| func init() { | ||
| rootCmd.AddCommand(pivotCmd) | ||
| pivotCmd.PersistentFlags().BoolVarP(&keep, "keep", "k", false, "Do not remove container image") | ||
| pivotCmd.PersistentFlags().BoolVarP(&reboot, "reboot", "r", false, "Reboot if changed") | ||
| pivotCmd.PersistentFlags().BoolVar(&exit77, "unchanged-exit-77", false, "If unchanged, exit 77") | ||
| pflag.CommandLine.AddGoFlagSet(flag.CommandLine) | ||
| } | ||
|
|
||
| // isArgTuneable returns if the argument provided is allowed to be modified | ||
| func isArgTunable(arg string) bool { | ||
| return tuneableArgsWhitelist[arg] | ||
| } | ||
|
|
||
| // isArgInUse checks to see if the argument is already in use by the system currently | ||
| func isArgInUse(arg, cmdLinePath string) (bool, error) { | ||
| if cmdLinePath == "" { | ||
| cmdLinePath = cmdLineFile | ||
| } | ||
| content, err := ioutil.ReadFile(cmdLinePath) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
|
|
||
| checkable := string(content) | ||
| if strings.Contains(checkable, arg) { | ||
| return true, nil | ||
| } | ||
| return false, nil | ||
| } | ||
|
|
||
| // parseTuningFile parses the kernel argument tuning file | ||
| func parseTuningFile(tuningFilePath, cmdLinePath string) ([]types.TuneArgument, []types.TuneArgument, error) { | ||
| addArguments := []types.TuneArgument{} | ||
| deleteArguments := []types.TuneArgument{} | ||
| if tuningFilePath == "" { | ||
| tuningFilePath = kernelTuningFile | ||
| } | ||
| if cmdLinePath == "" { | ||
| cmdLinePath = cmdLineFile | ||
| } | ||
| // Return fast if the file does not exist | ||
| if _, err := os.Stat(tuningFilePath); os.IsNotExist(err) { | ||
| glog.V(2).Infof("no kernel tuning needed as %s does not exist", tuningFilePath) | ||
| // This isn't an error. Return out. | ||
| return addArguments, deleteArguments, err | ||
| } | ||
| // Read and parse the file | ||
| file, err := os.Open(tuningFilePath) | ||
| if err != nil { | ||
| // If we have an issue reading return an error | ||
| glog.Infof("Unable to open %s for reading: %v", tuningFilePath, err) | ||
| return addArguments, deleteArguments, err | ||
| } | ||
| // Clean up | ||
| defer file.Close() | ||
|
|
||
| // Parse the tuning lines | ||
| scanner := bufio.NewScanner(file) | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| if strings.HasPrefix(line, "ADD ") { | ||
| // NOTE: Today only specific bare kernel arguments are allowed so | ||
| // there is not a need to split on =. | ||
| key := strings.TrimSpace(line[len("ADD "):]) | ||
| if isArgTunable(key) { | ||
| // Find out if the argument is in use | ||
| inUse, err := isArgInUse(key, cmdLinePath) | ||
| if err != nil { | ||
| return addArguments, deleteArguments, err | ||
| } | ||
| if !inUse { | ||
| addArguments = append(addArguments, types.TuneArgument{Key: key, Bare: true}) | ||
| } else { | ||
| glog.Infof(`skipping "%s" as it is already in use`, key) | ||
| } | ||
| } else { | ||
| glog.Infof("%s not a whitelisted kernel argument", key) | ||
| } | ||
| } else if strings.HasPrefix(line, "DELETE ") { | ||
| // NOTE: Today only specific bare kernel arguments are allowed so | ||
| // there is not a need to split on =. | ||
| key := strings.TrimSpace(line[len("DELETE "):]) | ||
| if isArgTunable(key) { | ||
| inUse, err := isArgInUse(key, cmdLinePath) | ||
| if err != nil { | ||
| return addArguments, deleteArguments, err | ||
| } | ||
| if inUse { | ||
| deleteArguments = append(deleteArguments, types.TuneArgument{Key: key, Bare: true}) | ||
| } else { | ||
| glog.Infof(`skipping "%s" as it is not present in the current argument list`, key) | ||
| } | ||
| } else { | ||
| glog.Infof("%s not a whitelisted kernel argument", key) | ||
| } | ||
| } else { | ||
| glog.V(2).Infof(`skipping malformed line in %s: "%s"`, tuningFilePath, line) | ||
| } | ||
| } | ||
| return addArguments, deleteArguments, nil | ||
| } | ||
|
|
||
| // updateTuningArgs executes additions and removals of kernel tuning arguments | ||
| func updateTuningArgs(tuningFilePath, cmdLinePath string) (bool, error) { | ||
| if cmdLinePath == "" { | ||
| cmdLinePath = cmdLineFile | ||
| } | ||
| changed := false | ||
| additions, deletions, err := parseTuningFile(tuningFilePath, cmdLinePath) | ||
| if err != nil { | ||
| return changed, err | ||
| } | ||
|
|
||
| // Execute additions | ||
| for _, toAdd := range additions { | ||
| if toAdd.Bare { | ||
| changed = true | ||
| utils.Run("rpm-ostree", "kargs", fmt.Sprintf("--append=%s", toAdd.Key)) | ||
| } else { | ||
| panic("Not supported") | ||
| } | ||
| } | ||
| // Execute deletions | ||
| for _, toDelete := range deletions { | ||
| if toDelete.Bare { | ||
| changed = true | ||
| utils.Run("rpm-ostree", "kargs", fmt.Sprintf("--delete=%s", toDelete.Key)) | ||
| } else { | ||
| panic("Not supported") | ||
| } | ||
| } | ||
| return changed, nil | ||
| } | ||
|
|
||
| // podmanRemove kills and removes a container | ||
| func podmanRemove(cid string) { | ||
| utils.RunIgnoreErr("podman", "kill", cid) | ||
| utils.RunIgnoreErr("podman", "rm", "-f", cid) | ||
| } | ||
|
|
||
| // getDefaultDeployment uses rpm-ostree status --json to get the current deployment | ||
| func getDefaultDeployment() types.RpmOstreeDeployment { | ||
| // use --status for now, we can switch to D-Bus if we need more info | ||
| var rosState types.RpmOstreeState | ||
| output := utils.RunGetOut("rpm-ostree", "status", "--json") | ||
| if err := json.Unmarshal([]byte(output), &rosState); err != nil { | ||
| glog.Fatalf("Failed to parse `rpm-ostree status --json` output: %v", err) | ||
| } | ||
|
|
||
| // just make it a hard error if we somehow don't have any deployments | ||
| if len(rosState.Deployments) == 0 { | ||
| glog.Fatalf("Not currently booted in a deployment") | ||
| } | ||
|
|
||
| return rosState.Deployments[0] | ||
| } | ||
|
|
||
| // pullAndRebase potentially rebases system if not already rebased. | ||
| func pullAndRebase(container string) (imgid string, changed bool) { | ||
| defaultDeployment := getDefaultDeployment() | ||
|
|
||
| previousPivot := "" | ||
| if len(defaultDeployment.CustomOrigin) > 0 { | ||
| if strings.HasPrefix(defaultDeployment.CustomOrigin[0], "pivot://") { | ||
| previousPivot = defaultDeployment.CustomOrigin[0][len("pivot://"):] | ||
| glog.Infof("Previous pivot: %s", previousPivot) | ||
| } | ||
| } | ||
|
|
||
| var authArgs []string | ||
| if utils.FileExists(kubeletAuthFile) { | ||
| authArgs = append(authArgs, "--authfile", kubeletAuthFile) | ||
| } | ||
|
|
||
| // If we're passed a non-canonical image, resolve it to its sha256 now | ||
| isCanonicalForm := true | ||
| if _, err := daemon.GetRefDigest(container); err != nil { | ||
| isCanonicalForm = false | ||
| // In non-canonical form, we pull unconditionally right now | ||
| args := []string{"pull", "-q"} | ||
| args = append(args, authArgs...) | ||
| args = append(args, container) | ||
| utils.RunExt(false, numRetriesNetCommands, "podman", args...) | ||
| } else { | ||
| targetMatched, err := daemon.CompareOSImageURL(previousPivot, container) | ||
| if err != nil { | ||
| glog.Fatalf("%v", err) | ||
| } | ||
| if targetMatched { | ||
| changed = false | ||
| return | ||
| } | ||
|
|
||
| // Pull the image | ||
| args := []string{"pull", "-q"} | ||
| args = append(args, authArgs...) | ||
| args = append(args, container) | ||
| utils.RunExt(false, numRetriesNetCommands, "podman", args...) | ||
| } | ||
|
|
||
| inspectArgs := []string{"inspect", "--type=image"} | ||
| inspectArgs = append(inspectArgs, fmt.Sprintf("%s", container)) | ||
| output := utils.RunExt(true, 1, "podman", inspectArgs...) | ||
| var imagedataArray []types.ImageInspection | ||
| json.Unmarshal([]byte(output), &imagedataArray) | ||
| imagedata := imagedataArray[0] | ||
| if !isCanonicalForm { | ||
| imgid = imagedata.RepoDigests[0] | ||
| glog.Infof("Resolved to: %s", imgid) | ||
| } else { | ||
| imgid = container | ||
| } | ||
|
|
||
| // Clean up a previous container | ||
| podmanRemove(types.PivotName) | ||
|
|
||
| // `podman mount` wants a container, so let's make create a dummy one, but not run it | ||
| cid := utils.RunGetOut("podman", "create", "--net=none", "--name", types.PivotName, imgid) | ||
| // Use the container ID to find its mount point | ||
| mnt := utils.RunGetOut("podman", "mount", cid) | ||
| repo := fmt.Sprintf("%s/srv/repo", mnt) | ||
|
|
||
| // Now we need to figure out the commit to rebase to | ||
|
|
||
| // Commit label takes priority | ||
| ostreeCsum, ok := imagedata.Labels["com.coreos.ostree-commit"] | ||
| if ok { | ||
| if ostreeVersion, ok := imagedata.Labels["version"]; ok { | ||
| glog.Infof("Pivoting to: %s (%s)", ostreeVersion, ostreeCsum) | ||
| } else { | ||
| glog.Infof("Pivoting to: %s", ostreeCsum) | ||
| } | ||
| } else { | ||
| glog.Infof("No com.coreos.ostree-commit label found in metadata! Inspecting...") | ||
| refs := strings.Split(utils.RunGetOut("ostree", "refs", "--repo", repo), "\n") | ||
| if len(refs) == 1 { | ||
| glog.Infof("Using ref %s", refs[0]) | ||
| ostreeCsum = utils.RunGetOut("ostree", "rev-parse", "--repo", repo, refs[0]) | ||
| } else if len(refs) > 1 { | ||
| glog.Fatalf("Multiple refs found in repo!") | ||
| } else { | ||
| // XXX: in the future, possibly scan the repo to find a unique .commit object | ||
| glog.Fatalf("No refs found in repo!") | ||
| } | ||
| } | ||
|
|
||
| // This will be what will be displayed in `rpm-ostree status` as the "origin spec" | ||
| customURL := fmt.Sprintf("pivot://%s", imgid) | ||
|
|
||
| // RPM-OSTree can now directly slurp from the mounted container! | ||
| // https://github.com/projectatomic/rpm-ostree/pull/1732 | ||
| utils.Run("rpm-ostree", "rebase", "--experimental", | ||
| fmt.Sprintf("%s:%s", repo, ostreeCsum), | ||
| "--custom-origin-url", customURL, | ||
| "--custom-origin-description", "Managed by pivot tool") | ||
|
|
||
| // Kill our dummy container | ||
| podmanRemove(types.PivotName) | ||
|
|
||
| changed = true | ||
| return | ||
| } | ||
|
|
||
| // Execute runs the command | ||
| func Execute(cmd *cobra.Command, args []string) { | ||
| var fromFile bool | ||
| var container string | ||
| if len(args) > 0 { | ||
| container = args[0] | ||
| fromFile = false | ||
| } else { | ||
| glog.Infof("Using image pullspec from %s", etcPivotFile) | ||
| data, err := ioutil.ReadFile(etcPivotFile) | ||
| if err != nil { | ||
| glog.Fatalf("Failed to read from %s: %v", etcPivotFile, err) | ||
| } | ||
| container = strings.TrimSpace(string(data)) | ||
| fromFile = true | ||
| } | ||
|
|
||
| imgid, changed := pullAndRebase(container) | ||
|
|
||
| // Delete the file now that we successfully rebased | ||
| if fromFile { | ||
| if err := os.Remove(etcPivotFile); err != nil { | ||
| if !os.IsNotExist(err) { | ||
| glog.Fatalf("Failed to delete %s: %v", etcPivotFile, err) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // By default, delete the image. | ||
| if !keep { | ||
| // Related: https://github.com/containers/libpod/issues/2234 | ||
| utils.RunIgnoreErr("podman", "rmi", imgid) | ||
| } | ||
|
|
||
| // Check to see if we need to tune kernel arguments | ||
| tuningChanged, err := updateTuningArgs(kernelTuningFile, cmdLineFile) | ||
| if err != nil { | ||
| glog.Infof("unable to parse tuning file %s: %s", kernelTuningFile, err) | ||
| } | ||
| // If tuning changes but the oscontainer didn't we still denote we changed | ||
| // for the reboot | ||
| if tuningChanged { | ||
| changed = true | ||
| if err != nil { | ||
| glog.Infof(`Unable to remove kernel tuning file %s: "%s"`, kernelTuningFile, err) | ||
| } | ||
|
|
||
| } | ||
|
|
||
| if !changed { | ||
| glog.Info("Already at target pivot; exiting...") | ||
| if exit77 { | ||
| os.Exit(77) | ||
| } | ||
| } else if reboot || utils.FileExists(runPivotRebootFile) { | ||
| // Reboot the machine if asked to do so | ||
| utils.Run("systemctl", "reboot") | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be a json err check here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops i commented too late 😂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably got imported as is from openshift/pivot and yeah, we should have an error check here - let's follow up all the things here :)