Chroot represents a chroot instance, which can be used to run commands
inside a chroot environment
Close unmounts all the bind mounts and closes the chroot environment
{
PrintVerboseInfo("Chroot.Close", "running...")
err := syscall.Unmount(filepath.Join(c.root, "/dev/pts"), 0)
if err != nil {
PrintVerboseErr("Chroot.Close", 0, err)
return err
}
mountList := ReservedMounts
if c.etcMounted {
mountList = append(mountList, "/etc")
}
mountList = append(mountList, "")
for _, mount := range mountList {
if mount == "/dev/pts" {
continue
}
mountDir := filepath.Join(c.root, mount)
PrintVerboseInfo("Chroot.Close", "unmounting", mountDir)
err := syscall.Unmount(mountDir, 0)
if err != nil {
PrintVerboseErr("Chroot.Close", 1, err)
return err
}
}
PrintVerboseInfo("Chroot.Close", "successfully closed.")
return nil
}
Execute runs a command in the chroot environment, the command is
a string and the arguments are a list of strings. If an error occurs
it is returned.
{
PrintVerboseInfo("Chroot.Execute", "running...")
PrintVerboseInfo("Chroot.Execute", "running command:", cmd)
e := exec.Command("chroot", c.root, "/bin/sh", "-c", cmd)
e.Stdout = os.Stdout
e.Stderr = os.Stderr
e.Stdin = os.Stdin
err := e.Run()
if err != nil {
PrintVerboseErr("Chroot.Execute", 0, err)
return err
}
PrintVerboseInfo("Chroot.Execute", "successfully ran.")
return nil
}
ExecuteCmds runs a list of commands in the chroot environment,
stops at the first error
{
PrintVerboseInfo("Chroot.ExecuteCmds", "running...")
for _, cmd := range cmds {
err := c.Execute(cmd)
if err != nil {
PrintVerboseErr("Chroot.ExecuteCmds", 0, err)
return err
}
}
PrintVerboseInfo("Chroot.ExecuteCmds", "successfully ran.")
return nil
}
NewChroot creates a new chroot environment from the given root path and
returns its Chroot instance or an error if something went wrong
{
PrintVerboseInfo("NewChroot", "running...")
root = strings.ReplaceAll(root, "//", "/")
if _, err := os.Stat(root); os.IsNotExist(err) {
PrintVerboseErr("NewChroot", 0, err)
return nil, err
}
chroot := &Chroot{
root: root,
rootUuid: rootUuid,
rootDevice: rootDevice,
etcMounted: mountUserEtc,
}
// workaround for grub-mkconfig, not able to find the device
// inside a chroot environment
err := chroot.Execute("mount --bind / /")
if err != nil {
PrintVerboseErr("NewChroot", 1, err)
return nil, err
}
for _, mount := range ReservedMounts {
PrintVerboseInfo("NewChroot", "mounting", mount)
err := syscall.Mount(mount, filepath.Join(root, mount), "", syscall.MS_BIND, "")
if err != nil {
PrintVerboseErr("NewChroot", 2, err)
return nil, err
}
}
if mountUserEtc {
err = syscall.Mount("overlay", filepath.Join(root, "etc"), "overlay", syscall.MS_RDONLY, "lowerdir="+userEtcPath+":"+filepath.Join(root, "/etc"))
if err != nil {
PrintVerboseErr("NewChroot", 3, "failed to mount user etc:", err)
return nil, err
}
}
PrintVerboseInfo("NewChroot", "successfully created.")
return chroot, nil
}
{
if os.Getenv("ABROOT_KARGS_PATH") != "" {
KargsPath = os.Getenv("ABROOT_KARGS_PATH")
}
}
kargsCreateIfMissing creates the kargs file if it doesn't exist
{
PrintVerboseInfo("kargsCreateIfMissing", "running...")
if _, err := os.Stat(KargsPath); os.IsNotExist(err) {
PrintVerboseInfo("kargsCreateIfMissing", "creating kargs file...")
err = os.WriteFile(KargsPath, []byte(DefaultKargs), 0644)
if err != nil {
PrintVerboseErr("kargsCreateIfMissing", 0, err)
return err
}
}
PrintVerboseInfo("kargsCreateIfMissing", "done")
return nil
}
KargsWrite makes a backup of the current kargs file and then
writes the new content to it
{
PrintVerboseInfo("KargsWrite", "running...")
err := kargsCreateIfMissing()
if err != nil {
PrintVerboseErr("KargsWrite", 0, err)
return err
}
validated, err := KargsFormat(content)
if err != nil {
PrintVerboseErr("KargsWrite", 1, err)
return err
}
err = KargsBackup()
if err != nil {
PrintVerboseErr("KargsWrite", 2, err)
return err
}
err = os.WriteFile(KargsPath, []byte(validated), 0644)
if err != nil {
PrintVerboseErr("KargsWrite", 3, err)
return err
}
PrintVerboseInfo("KargsWrite", "done")
return nil
}
KargsBackup makes a backup of the current kargs file
{
PrintVerboseInfo("KargsBackup", "running...")
content, err := KargsRead()
if err != nil {
PrintVerboseErr("KargsBackup", 0, err)
return err
}
err = os.WriteFile(KargsPath+".bak", []byte(content), 0644)
if err != nil {
PrintVerboseErr("KargsBackup", 1, err)
return err
}
PrintVerboseInfo("KargsBackup", "done")
return nil
}
KargsRead reads the content of the kargs file
{
PrintVerboseInfo("KargsRead", "running...")
err := kargsCreateIfMissing()
if err != nil {
PrintVerboseErr("KargsRead", 0, err)
return "", err
}
content, err := os.ReadFile(KargsPath)
if err != nil {
PrintVerboseErr("KargsRead", 1, err)
return "", err
}
PrintVerboseInfo("KargsRead", "done")
return string(content), nil
}
KargsFormat formats the contents of the kargs file, ensuring that
there are no duplicate entries, multiple spaces or trailing newline
{
PrintVerboseInfo("KargsValidate", "running...")
kargs := []string{}
lines := strings.Split(content, "\n")
for _, line := range lines {
if line == "" {
continue
}
lineArgs := strings.Split(line, " ")
for _, larg := range lineArgs {
// Check for duplicates
isDuplicate := false
for _, ka := range kargs {
if ka == larg {
isDuplicate = true
break
}
}
if !isDuplicate {
kargs = append(kargs, larg)
}
}
}
PrintVerboseInfo("KargsValidate", "done")
return strings.Join(kargs, " "), nil
}
KargsEdit copies the kargs file to a temporary file and opens it in the
user's preferred editor by querying the $EDITOR environment variable.
Once closed, its contents are written back to the main kargs file.
This function returns a boolean parameter indicating whether any changes
were made to the kargs file.
{
PrintVerboseInfo("KargsEdit", "running...")
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "nano"
}
err := kargsCreateIfMissing()
if err != nil {
PrintVerboseErr("KargsEdit", 0, err)
return false, err
}
// Open a temporary file, so editors installed via apx can also be used
PrintVerboseInfo("KargsEdit", "Copying kargs file to /tmp")
err = CopyFile(KargsPath, KargsTmpFile)
if err != nil {
PrintVerboseErr("KargsEdit", 1, err)
return false, err
}
// Call $EDITOR on temp file
PrintVerboseInfo("KargsEdit", "Opening", KargsTmpFile, "in", editor)
cmd := exec.Command(editor, KargsTmpFile)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
PrintVerboseErr("KargsEdit", 2, err)
return false, err
}
content, err := os.ReadFile(KargsTmpFile)
if err != nil {
PrintVerboseErr("KargsEdit", 3, err)
return false, err
}
// Check whether there were any changes
ogContent, err := os.ReadFile(KargsPath)
if err != nil {
PrintVerboseErr("KargsEdit", 4, err)
return false, err
}
if string(ogContent) == string(content) {
PrintVerboseInfo("KargsEdit", "No changes were made to kargs, skipping save.")
return false, nil
}
PrintVerboseInfo("KargsEdit", "Writing contents of", KargsTmpFile, "to the original location")
err = KargsWrite(string(content))
if err != nil {
PrintVerboseErr("KargsEdit", 5, err)
return false, err
}
PrintVerboseInfo("KargsEdit", "Done")
return true, nil
}
init initializes the log file and sets up logging
{
PrintVerboseInfo("NewLogFile", "running...")
// Incremental value to append to log file name
incremental := 0
// Check for existing log files
logFiles, err := filepath.Glob("/var/log/abroot.log.*")
if err != nil {
// If there are no log files, start with incremental 1
incremental = 1
} else {
allIncrementals := []int{}
// Extract incremental values from existing log file names
for _, logFile := range logFiles {
_, err := fmt.Sscanf(logFile, "/var/log/abroot.log.%d", &incremental)
if err != nil {
continue
}
allIncrementals = append(allIncrementals, incremental)
}
// Set incremental to the next available value
if len(allIncrementals) == 0 {
incremental = 1
} else {
incremental = allIncrementals[len(allIncrementals)-1] + 1
}
}
// Open or create the log file
logFile, err = os.OpenFile(
fmt.Sprintf("/var/log/abroot.log.%d", incremental),
os.O_RDWR|os.O_CREATE|os.O_APPEND,
0666,
)
if err != nil {
PrintVerboseErrNoLog("NewLogFile", 0, "failed to open log file", err)
}
}
IsVerbose checks if verbose mode is enabled
{
flag := cmdr.FlagValBool("verbose")
_, arg := os.LookupEnv("ABROOT_VERBOSE")
return flag || arg
}
formatMessage formats log messages based on prefix, level, and depth
{
if prefix == "" && level == "" && depth == -1 {
return fmt.Sprint(args...)
}
if depth > -1 {
level = fmt.Sprintf("%s(%f)", level, depth)
}
return fmt.Sprintf("%s:%s:%s", prefix, level, fmt.Sprint(args...))
}
printFormattedMessage prints formatted log messages to Stdout
{
printLog.Printf("%s\n", formattedMsg)
}
logToFileIfEnabled logs messages to the file if logging is enabled
{
if logFile != nil {
LogToFile(formattedMsg)
}
}
PrintVerboseNoLog prints verbose messages without logging to the file
{
if IsVerbose() {
formattedMsg := formatMessage(prefix, level, depth, args...)
printFormattedMessage(formattedMsg)
}
}
PrintVerbose prints verbose messages and logs to the file if enabled
{
PrintVerboseNoLog(prefix, level, depth, args...)
logToFileIfEnabled(formatMessage(prefix, level, depth, args...))
}
PrintVerboseSimpleNoLog prints simple verbose messages without logging to the file
{
PrintVerboseNoLog("", "", -1, args...)
}
PrintVerboseSimple prints simple verbose messages and logs to the file if enabled
{
PrintVerbose("", "", -1, args...)
}
PrintVerboseErrNoLog prints verbose error messages without logging to the file
{
PrintVerboseNoLog(prefix, "err", depth, args...)
}
PrintVerboseErr prints verbose error messages and logs to the file if enabled
{
PrintVerbose(prefix, "err", depth, args...)
}
PrintVerboseWarnNoLog prints verbose warning messages without logging to the file
{
PrintVerboseNoLog(prefix, "warn", depth, args...)
}
PrintVerboseWarn prints verbose warning messages and logs to the file if enabled
{
PrintVerbose(prefix, "warn", depth, args...)
}
PrintVerboseInfoNoLog prints verbose info messages without logging to the file
{
PrintVerboseNoLog(prefix, "info", -1, args...)
}
PrintVerboseInfo prints verbose info messages and logs to the file if enabled
{
PrintVerbose(prefix, "info", -1, args...)
}
LogToFile writes messages to the log file
{
if logFile != nil {
_, err := fmt.Fprintf(
logFile,
"%s: %s\n",
time.Now().Format("2006-01-02 1 15:04:05"),
fmt.Sprintf(msg, args...),
)
return err
}
return nil
}
GetLogFile returns the log file handle
{
return logFile
}
{
return "not enough space in disk"
}
{
if len(str) < size {
return str + strings.Repeat(" ", size-len(str))
} else {
return str
}
}
OciExportRootFs generates a rootfs from an image recipe file
{
PrintVerboseInfo("OciExportRootFs", "running...")
pt, err := prometheus.NewPrometheus(
"/var/lib/abroot/storage",
"overlay",
settings.Cnf.MaxParallelDownloads,
)
if err != nil {
PrintVerboseErr("OciExportRootFs", 0, err)
return err
}
imageRecipePath := filepath.Join(transDir, "imageRecipe")
if transDir == dest {
err := errors.New("transDir and dest cannot be the same")
PrintVerboseErr("OciExportRootFs", 1, err)
return err
}
// create dest if it doesn't exist
err = os.MkdirAll(dest, 0o755)
if err != nil {
PrintVerboseErr("OciExportRootFs", 3, err)
return err
}
// cleanup transDir
err = os.RemoveAll(transDir)
if err != nil {
PrintVerboseErr("OciExportRootFs", 4, err)
return err
}
err = os.MkdirAll(transDir, 0o755)
if err != nil {
PrintVerboseErr("OciExportRootFs", 5, err)
return err
}
// write imageRecipe
err = imageRecipe.Write(imageRecipePath)
if err != nil {
PrintVerboseErr("OciExportRootFs", 6, err)
return err
}
pulledImage := false
// pull image
if !strings.HasPrefix(imageRecipe.From, "localhost/") {
err = pullImageWithProgressbar(pt, buildImageName, imageRecipe)
if err != nil {
PrintVerboseErr("OciExportRootFs", 6.1, err)
return err
}
pulledImage = true
}
// build image
imageBuild, err := pt.BuildContainerFile(imageRecipePath, buildImageName)
if err != nil {
PrintVerboseErr("OciExportRootFs", 7, err)
return err
}
if pulledImage {
// This is safe because BuildContainerFile layers on top of the base image
// So this won't delete the actual layers, only the image reference
_, err = pt.Store.DeleteImage(imageRecipe.From, true)
if err != nil {
PrintVerboseWarn("OciExportRootFs", 7.5, "could not delete downloaded image", err)
}
}
// mount image
mountDir, err := pt.MountImage(imageBuild.TopLayer)
if err != nil {
PrintVerboseErr("OciExportRootFs", 8, err)
return err
}
err = checkImageSize(mountDir, dest)
if err != nil {
PrintVerboseErr("OciExportRootFs", 8.5, err)
return err
}
// copy mount dir contents to dest
err = rsyncCmd(mountDir+"/", dest, []string{"--delete", "--checksum"}, false)
if err != nil {
PrintVerboseErr("OciExportRootFs", 9, err)
return err
}
// unmount image
_, err = pt.UnMountImage(imageBuild.TopLayer, true)
if err != nil {
PrintVerboseErr("OciExportRootFs", 10, err)
return err
}
return nil
}
returns nil if there's enough space in the filesystem for the image
returns NotEnoughSpaceError if there is not enough space
returns other error if the sizes were not calculated correctly
{
imageDirStat, err := os.Stat(imageDir)
if err != nil {
PrintVerboseErr("OciExportRootFs", 8.1, err)
return err
}
var imageDirSize int64
if imageDirStat.IsDir() {
imageDirSize, err = getDirSize(imageDir)
if err != nil {
PrintVerboseErr("OciExportRootFs", 8.2, err)
return err
}
} else {
imageDirSize = imageDirStat.Size()
}
var stat syscall.Statfs_t
err = syscall.Statfs(filesystemMount, &stat)
if err != nil {
PrintVerboseErr("OciExportRootFs", 8.3, err)
return err
}
availableSpace := stat.Blocks * uint64(stat.Bsize)
if settings.Cnf.ThinProvisioning {
availableSpace /= 2
}
if uint64(imageDirSize) > availableSpace {
err := &NotEnoughSpaceError{}
PrintVerboseErr("OciExportRootFs", 8.4, err)
return err
}
return nil
}
pullImageWithProgressbar pulls the image specified in the provided recipe
and reports the download progress using pterm progressbars. Each blob has
its own bar, similar to how docker and podman report downloads in their
respective CLIs
{
PrintVerboseInfo("pullImageWithProgressbar", "running...")
progressCh := make(chan types.ProgressProperties)
manifestCh := make(chan prometheus.OciManifest)
defer close(progressCh)
defer close(manifestCh)
err := pt.PullImageAsync(image.From, name, progressCh, manifestCh)
if err != nil {
PrintVerboseErr("pullImageWithProgressbar", 0, err)
return err
}
multi := pterm.DefaultMultiPrinter
bars := map[string]*pterm.ProgressbarPrinter{}
multi.Start()
barFmt := "%s [%s/%s]"
for {
select {
case report := <-progressCh:
digest := report.Artifact.Digest.Encoded()
if pb, ok := bars[digest]; ok {
progressBytes := humanize.Bytes(uint64(report.Offset))
totalBytes := humanize.Bytes(uint64(report.Artifact.Size))
pb.Add(int(report.Offset) - pb.Current)
title := fmt.Sprintf(barFmt, digest[:12], progressBytes, totalBytes)
pb.UpdateTitle(padString(title, 28))
} else {
totalBytes := humanize.Bytes(uint64(report.Artifact.Size))
title := fmt.Sprintf(barFmt, digest[:12], "0", totalBytes)
newPb, err := Progressbar.WithTotal(int(report.Artifact.Size)).WithWriter(multi.NewWriter()).Start(padString(title, 28))
if err != nil {
PrintVerboseErr("pullImageWithProgressbar", 1, err)
return err
}
bars[digest] = newPb
}
case <-manifestCh:
multi.Stop()
return nil
}
}
}
FindImageWithLabel returns the name of the first image containinig the provided key-value pair
or an empty string if none was found
FindImageWithLabel returns the name of the first image containing the
provided key-value pair or an empty string if none was found
{
PrintVerboseInfo("FindImageWithLabel", "running...")
pt, err := prometheus.NewPrometheus(
"/var/lib/abroot/storage",
"overlay",
settings.Cnf.MaxParallelDownloads,
)
if err != nil {
PrintVerboseErr("FindImageWithLabel", 0, err)
return "", err
}
images, err := pt.Store.Images()
if err != nil {
PrintVerboseErr("FindImageWithLabel", 1, err)
return "", err
}
for _, img := range images {
// This is the only way I could find to get the labels form an image
builder, err := buildah.ImportBuilderFromImage(context.Background(), pt.Store, buildah.ImportFromImageOptions{Image: img.ID})
if err != nil {
PrintVerboseErr("FindImageWithLabel", 2, err)
return "", err
}
val, ok := builder.Labels()[key]
if ok && val == value {
return img.Names[0], nil
}
}
return "", nil
}
RetrieveImageForRoot retrieves the image created for the provided root
based on the label. Note for distro maintainers: labels must follow those
defined in the ABRoot config file
{
PrintVerboseInfo("RetrieveImageForRoot", "running...")
image, err := FindImageWithLabel("ABRoot.root", root)
if err != nil {
PrintVerboseErr("RetrieveImageForRoot", 0, err)
return "", err
}
return image, nil
}
DeleteAllButLatestImage deletes all images
{
pt, err := prometheus.NewPrometheus(
"/var/lib/abroot/storage",
"overlay",
settings.Cnf.MaxParallelDownloads,
)
if err != nil {
PrintVerboseErr("DeleteAllImagesButLatest", 1, err)
return err
}
allImages, err := pt.Store.Images()
if err != nil {
PrintVerboseErr("DeleteAllImagesButLatest", 2, err)
return fmt.Errorf("could not retrieve all images: %w", err)
}
if len(allImages) == 0 {
return nil
}
var latestImage *storage.Image
for _, image := range allImages {
if latestImage == nil || image.Created.After(latestImage.Created) {
latestImage = &image
}
}
for _, image := range allImages {
if image.ID != latestImage.ID {
_, err := pt.Store.DeleteImage(image.ID, true)
if err != nil {
PrintVerboseErr("DeleteAllImagesButLatest", 3, "failed to remove image: ", err)
}
}
}
return nil
}
BaseImagePackageDiff retrieves the added, removed, upgraded and downgraded
base packages (the ones bundled with the image).
{
PrintVerboseInfo("PackageDiff.BaseImagePackageDiff", "running...")
imageComponents := strings.Split(settings.Cnf.Name, "/")
imageName := imageComponents[len(imageComponents)-1]
reqUrl := fmt.Sprintf("%s/images/%s/diff", settings.Cnf.DifferURL, imageName)
body := fmt.Sprintf("{\"old_digest\": \"%s\", \"new_digest\": \"%s\"}", currentDigest, newDigest)
PrintVerboseInfo("PackageDiff.BaseImagePackageDiff", "Requesting base image diff to", reqUrl, "with body", body)
request, err := http.NewRequest(http.MethodGet, reqUrl, strings.NewReader(body))
if err != nil {
PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 0, err)
return
}
defer request.Body.Close()
resp, err := http.DefaultClient.Do(request)
if err != nil {
PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 1, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 2, fmt.Errorf("received non-OK status %s", resp.Status))
return
}
contents, err := io.ReadAll(resp.Body)
if err != nil {
PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 3, err)
return
}
pkgDiff := struct {
Added, Upgraded, Downgraded, Removed []diff.PackageDiff
}{}
err = json.Unmarshal(contents, &pkgDiff)
if err != nil {
PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 4, err)
return
}
added = pkgDiff.Added
upgraded = pkgDiff.Upgraded
downgraded = pkgDiff.Downgraded
removed = pkgDiff.Removed
return
}
OverlayPackageDiff retrieves the added, removed, upgraded and downgraded
overlay packages (the ones added manually via `abroot pkg add`).
{
PrintVerboseInfo("OverlayPackageDiff", "running...")
pkgM, err := NewPackageManager(false)
if err != nil {
PrintVerboseErr("OverlayPackageDiff", 0, err)
return
}
addedPkgs, err := pkgM.GetAddPackages()
if err != nil {
PrintVerboseErr("PackageDiff.OverlayPackageDiff", 0, err)
return
}
localAddedVersions := dpkg.DpkgBatchGetPackageVersion(addedPkgs)
localAdded := map[string]string{}
for i := 0; i < len(addedPkgs); i++ {
if localAddedVersions[i] != "" {
localAdded[addedPkgs[i]] = localAddedVersions[i]
}
}
remoteAdded := map[string]string{}
var pkgInfo map[string]interface{}
for pkgName := range localAdded {
pkgInfo, err = GetRepoContentsForPkg(pkgName)
if err != nil {
PrintVerboseErr("PackageDiff.OverlayPackageDiff", 1, err)
return
}
version, ok := pkgInfo["version"].(string)
if !ok {
err = fmt.Errorf("unexpected value when retrieving upstream version of '%s'", pkgName)
return
}
remoteAdded[pkgName] = version
}
added, upgraded, downgraded, removed = diff.DiffPackages(localAdded, remoteAdded)
return
}
atomicSwap allows swapping 2 files or directories in-place and atomically,
using the renameat2 syscall. This should be used instead of os.Rename,
which is not atomic at all
{
PrintVerboseInfo("AtomicSwap", "running...")
orig, err := os.Open(src)
if err != nil {
PrintVerboseErr("AtomicSwap", 0, err)
return err
}
newfile, err := os.Open(dst)
if err != nil {
PrintVerboseErr("AtomicSwap", 1, err)
return err
}
err = unix.Renameat2(int(orig.Fd()), src, int(newfile.Fd()), dst, unix.RENAME_EXCHANGE)
if err != nil {
PrintVerboseErr("AtomicSwap", 2, err)
return err
}
PrintVerboseInfo("AtomicSwap", "done")
return nil
}
Represents a Checks struct which contains all the checks which can
be performed one by one or all at once using PerformAllChecks()
PerformAllChecks performs all checks
{
err := c.CheckCompatibilityFS()
if err != nil {
return err
}
err = c.CheckConnectivity()
if err != nil {
return err
}
err = c.CheckRoot()
if err != nil {
return err
}
return nil
}
CheckCompatibilityFS checks if the filesystem is compatible with ABRoot v2
if not, it returns an error. Note that currently only ext4, btrfs and xfs
are supported/tested. Here we assume some utilities are installed, such as
findmnt and lsblk
{
PrintVerboseInfo("Checks.CheckCompatibilityFS", "running...")
var fs []string
if runtime.GOOS == "linux" {
fs = []string{"ext4", "btrfs", "xfs"}
} else {
err := fmt.Errorf("your OS (%s) is not supported", runtime.GOOS)
PrintVerboseErr("Checks.CheckCompatibilityFS", 0, err)
return err
}
cmd, err := exec.Command("findmnt", "-n", "-o", "source", "/").Output()
if err != nil {
PrintVerboseErr("Checks.CheckCompatibilityFS", 1, err)
return err
}
device := string([]byte(cmd[:len(cmd)-1]))
cmd, err = exec.Command("lsblk", "-o", "fstype", "-n", device).Output()
if err != nil {
PrintVerboseErr("Checks.CheckCompatibilityFS", 2, err)
return err
}
fsType := string([]byte(cmd[:len(cmd)-1]))
for _, f := range fs {
if f == string(fsType) {
PrintVerboseInfo("CheckCompatibilityFS", fsType, "is supported")
return nil
}
}
err = fmt.Errorf("the filesystem (%s) is not supported", fsType)
PrintVerboseErr("Checks.CheckCompatibilityFS", 3, err)
return err
}
CheckConnectivity checks if the system is connected to the internet
{
PrintVerboseInfo("Checks.CheckConnectivity", "running...")
timeout := 5 * time.Second
_, err := net.DialTimeout("tcp", "vanillaos.org:80", timeout)
if err != nil {
PrintVerboseErr("Checks.CheckConnectivity", 1, err)
return err
}
return nil
}
CheckRoot checks if the user is root and returns an error if not
{
PrintVerboseInfo("Checks.CheckRoot", "running...")
if os.Geteuid() == 0 {
PrintVerboseInfo("Checks.CheckRoot", "you are root")
return nil
}
err := errors.New("not root")
PrintVerboseErr("Checks.CheckRoot", 1, err)
return err
}
NewChecks returns a new Checks struct
{
return &Checks{}
}
{
info, err := cpu.Info()
if err != nil {
return "", err
}
if len(info) == 0 {
return "", fmt.Errorf("CPU information not found")
}
return info[0].ModelName, nil
}
{
parts := strings.SplitN(line, " ", 3)
if len(parts) < 3 {
return "", fmt.Errorf("GPU information not found")
}
parts = strings.SplitN(parts[2], ":", 2)
if len(parts) < 2 {
return "", fmt.Errorf("GPU information not found")
}
return strings.TrimSpace(parts[1]), nil
}
{
cmd := exec.Command("lspci")
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("Error getting GPU info:", err)
return nil, err
}
lines := strings.Split(string(output), "\n")
var gpus []string
for _, line := range lines {
if strings.Contains(line, "VGA compatible controller") {
gpu, err := parseGPUInfo(line)
if err != nil {
continue
}
gpus = append(gpus, gpu)
}
}
return gpus, nil
}
{
vm, err := mem.VirtualMemory()
if err != nil {
return "", err
}
return fmt.Sprintf("%d MB", vm.Total/1024/1024), nil
}
{
cpu, _ := getCPUInfo()
gpu, _ := getGPUInfo()
memory, _ := getMemoryInfo()
return PCSpecs{
CPU: cpu,
GPU: gpu,
Memory: memory,
}
}
An ABSystem allows to perform system operations such as upgrades,
package changes and rollback on an ABRoot-compliant system.
CheckAll performs all checks from the Checks struct
{
PrintVerboseInfo("ABSystem.CheckAll", "running...")
err := s.Checks.PerformAllChecks()
if err != nil {
PrintVerboseErr("ABSystem.CheckAll", 0, err)
return err
}
PrintVerboseInfo("ABSystem.CheckAll", "all checks passed")
return nil
}
CheckUpdate checks if there is an update available
{
PrintVerboseInfo("ABSystem.CheckUpdate", "running...")
return s.Registry.HasUpdate(s.CurImage.Digest)
}
{
PrintVerboseInfo("ABSystem.CreateRootSymlinks", "creating symlinks")
links := []string{"mnt", "proc", "run", "dev", "media", "root", "sys", "tmp", "var"}
for _, link := range links {
linkName := filepath.Join(systemNewPath, link)
err := os.RemoveAll(linkName)
if err != nil {
PrintVerboseErr("ABSystem.CreateRootSymlinks", 1, err)
return err
}
targetName := filepath.Join("/", link)
err = os.Symlink(targetName, linkName)
if err != nil {
PrintVerboseErr("ABSystem.CreateRootSymlinks", 2, err)
return err
}
}
return nil
}
{
if name == "" {
return fmt.Errorf("no image provided")
}
if strings.Contains(name, ".") {
registrySplit := strings.SplitN(name, "/", 2)
settings.Cnf.Registry = registrySplit[0]
name = registrySplit[1]
}
nameTagSplit := strings.Split(name, ":")
name = nameTagSplit[0]
if len(nameTagSplit) < 1 {
fmt.Errorf("No tag provided")
}
settings.Cnf.Tag = nameTagSplit[1]
if name != "" {
settings.Cnf.Name = name
}
_, _, err := s.CheckUpdate()
if errors.Is(err, ErrImageNotFound) {
return fmt.Errorf("provided image cannot be found")
}
if !dryRun {
err := settings.WriteConfigToFile(settings.CnfPathAdmin)
if err != nil {
return err
}
}
return nil
}
RunOperation executes a root-switching operation from the options below:
UPGRADE:
Upgrades to a new image, if available,
FORCE_UPGRADE:
Forces the upgrade operation, even if no new image is available,
APPLY:
Applies package changes, and updates the system if an update is available.
INITRAMFS:
Updates the initramfs for the future root, but doesn't update the system.
{
PrintVerboseInfo("ABSystem.RunOperation", "starting", operation)
cq := goodies.NewCleanupQueue()
defer cq.Run()
// Stage 0: Check if upgrade is possible
// -------------------------------------
PrintVerboseSimple("[Stage 0] -------- ABSystemRunOperation")
if s.finishedFileExists() {
PrintVerboseWarn("ABSystemRunOperation", 0, "reboot required")
return errors.New("another operation finished successfully, a reboot is required")
}
err := s.LockOperation()
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 0.1, "could not create lock file:", err)
return fmt.Errorf("could not create lock file: %w", err)
}
cq.Add(func(args ...interface{}) error {
return s.UnlockOperation()
}, nil, 100, &goodies.NoErrorHandler{}, false)
// removes the finalizing file if it exists
err = s.removeFinalizingFile()
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 0.2, err)
return err
}
// Stage 1: Check if there is an update available
// ------------------------------------------------
PrintVerboseSimple("[Stage 1] -------- ABSystemRunOperation")
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystemRunOperation", 2, err)
return err
}
var imageDigest string
if operation != INITRAMFS {
var res bool
imageDigest, res, err = s.CheckUpdate()
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 1, err)
return err
}
if !res {
if operation != FORCE_UPGRADE && operation != APPLY && operation != DRY_RUN_APPLY {
PrintVerboseErr("ABSystemRunOperation", 1.1, err)
return ErrNoUpdate
}
imageDigest = s.CurImage.Digest
if operation == FORCE_UPGRADE {
PrintVerboseWarn("ABSystemRunOperation", 1.2, "No update available but --force is set. Proceeding...")
}
}
} else {
imageDigest = s.CurImage.Digest
}
// Stage 2: Get the future root and boot partitions,
// mount future to /part-future and clean up
// old .system_new and abimage-new.abr (it is
// possible that last transaction was interrupted
// before the clean up was done). Finally run
// the IntegrityCheck on the future root.
// ------------------------------------------------
PrintVerboseSimple("[Stage 2] -------- ABSystemRunOperation")
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystemRunOperation", 2, err)
return err
}
partFuture, err := s.RootM.GetFuture()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 2.1, err)
return err
}
partBoot, err := s.RootM.GetBoot()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 2.2, err)
return err
}
partFuture.Partition.Unmount() // just in case
partBoot.Unmount()
err = partFuture.Partition.Mount("/part-future/")
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 2.3, err)
return err
}
os.RemoveAll("/part-future/.system_new")
os.RemoveAll("/part-future/abimage-new.abr") // errors are safe to ignore
cq.Add(func(args ...interface{}) error {
return partFuture.Partition.Unmount()
}, nil, 90, &goodies.NoErrorHandler{}, false)
err = RepairRootIntegrity(partFuture.Partition.MountPoint)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 2.4, err)
return err
}
// Stage 3: Make a imageRecipe with user packages
// ------------------------------------------------
PrintVerboseSimple("[Stage 3] -------- ABSystemRunOperation")
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystemRunOperation", 2, err)
return err
}
// Stage 3.1: Delete old images
switch operation {
case DRY_RUN_UPGRADE, DRY_RUN_APPLY, DRY_RUN_INITRAMFS:
default:
err = DeleteAllButLatestImage()
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 3.05, err)
return err
}
}
futurePartition, err := s.RootM.GetFuture()
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 3.1, err)
return err
}
labels := map[string]string{
"maintainer": "'Generated by ABRoot'",
"ABRoot.root": futurePartition.Label,
}
args := map[string]string{}
pkgM, err := NewPackageManager(false)
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 3.2, err)
return err
}
pkgsFinal, err := pkgM.GetFinalCmd()
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 3.25, err)
}
if pkgsFinal == "" {
pkgsFinal = "true"
}
content := `RUN ` + pkgsFinal
var imageName string
switch operation {
case INITRAMFS, DRY_RUN_INITRAMFS:
presentPartition, err := s.RootM.GetPresent()
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 3.3, err)
return err
}
imageName, err = RetrieveImageForRoot(presentPartition.Label)
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 3.4, err)
return err
}
// Handle case where an image for the current root may not exist
// in storage
if imageName == "" {
imageName = settings.GetFullImageNameWithTag()
}
default:
imageName = settings.GetFullImageName()
imageName += "@" + imageDigest
labels["ABRoot.BaseImageDigest"] = imageDigest
}
imageRecipe := NewImageRecipe(
imageName,
labels,
args,
content,
)
// Stage 4: Extract the rootfs
// ------------------------------------------------
PrintVerboseSimple("[Stage 4] -------- ABSystemRunOperation")
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystemRunOperation", 2, err)
return err
}
abrootTrans := filepath.Join(partFuture.Partition.MountPoint, "abroot-trans")
systemOld := filepath.Join(partFuture.Partition.MountPoint, ".system")
systemNew := filepath.Join(partFuture.Partition.MountPoint, ".system.new")
if freeSpace || os.Getenv("ABROOT_FREE_SPACE") != "" {
PrintVerboseInfo("ABSystemRunOperation", "Deleting future system to free space, this will render the future root temporarily unavailable")
err := os.RemoveAll(systemOld)
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 4, err)
return err
}
err = os.MkdirAll(systemOld, 0o755)
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 4.1, err)
return err
}
} else {
PrintVerboseInfo("ABSystemRunOperation", "Creating a reflink clone of the old system to copy into")
err := os.RemoveAll(systemNew)
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 4.11, "could not cleanup old systemNew folder", err)
return err
}
err = exec.Command("cp", "--reflink", "-a", systemOld, systemNew).Run()
if err != nil {
PrintVerboseWarn("ABSystemRunOperation", 4.12, "reflink copy of system failed, falling back to slow copy because:", err)
// can be safely ignored
// file system doesn't support CoW
}
}
err = OciExportRootFs(
"abroot-"+uuid.New().String(),
imageRecipe,
abrootTrans,
systemNew,
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 4.2, err)
return err
}
// Stage 4.1: Delete old images
switch operation {
case DRY_RUN_UPGRADE, DRY_RUN_APPLY, DRY_RUN_INITRAMFS:
default:
err = DeleteAllButLatestImage()
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 3.05, err)
return err
}
}
// Stage 5: Write abimage.abr.new and config to future/
// ------------------------------------------------
PrintVerboseSimple("[Stage 5] -------- ABSystemRunOperation")
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystemRunOperation", 2, err)
return err
}
abimage, err := NewABImage(imageDigest, settings.GetFullImageNameWithTag())
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 5.1, err)
return err
}
err = abimage.WriteTo(partFuture.Partition.MountPoint, "new")
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 5.2, err)
return err
}
varParent := s.RootM.VarPartition.Parent
if varParent != nil && varParent.IsEncrypted() {
device := varParent.Device
if varParent.IsDevMapper() {
device = "/dev/mapper/" + device
} else {
device = "/dev/" + device
}
settings.Cnf.PartCryptVar = device
}
err = settings.WriteConfigToFile(filepath.Join(systemNew, "/usr/share/abroot/abroot.json"))
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 5.25, err)
return err
}
err = pkgM.WriteSummaryToRoot(systemNew)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 5.26, err)
return err
}
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystemRunOperation", 2, err)
return err
}
// from this point on, it is not possible to stop the upgrade
// so we create the finalizing file. Note that interrupting the upgrade
// from this point on will not leave the system in an inconsistent
// state, but it could leave the future partition in a dirty state
// preventing it from booting.
err = s.createFinalizingFile()
if err != nil {
PrintVerboseErr("ABSystemRunOperation", 5.3, err)
return err
}
// Stage (dry): If dry-run, exit here before writing to disk
// ------------------------------------------------
switch operation {
case DRY_RUN_UPGRADE, DRY_RUN_APPLY, DRY_RUN_INITRAMFS:
PrintVerboseInfo("ABSystem.RunOperation", "dry-run completed")
return nil
}
// Stage 6: Update the bootloader
// ------------------------------------------------
PrintVerboseSimple("[Stage 6] -------- ABSystemRunOperation")
partPresent, err := s.RootM.GetPresent()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.01, "failed to get present partition:", err)
}
chroot, err := NewChroot(
systemNew,
partFuture.Partition.Uuid,
partFuture.Partition.Device,
true,
filepath.Join("/var/lib/abroot/etc", partPresent.Label),
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.02, err)
return err
}
generatedGrubConfigPath := "/boot/grub/grub.cfg"
grubCommand := fmt.Sprintf(settings.Cnf.UpdateGrubCmd, generatedGrubConfigPath)
err = chroot.Execute(grubCommand)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.1, err)
return err
}
err = chroot.Execute(settings.Cnf.UpdateInitramfsCmd) // ensure initramfs is updated
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.2, err)
return err
}
err = chroot.Close()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.25, err)
return err
}
newKernelVer := getKernelVersion(filepath.Join(systemNew, "boot"))
if newKernelVer == "" {
err := errors.New("could not get kernel version")
PrintVerboseErr("ABSystem.RunOperation", 7.26, err)
return err
}
var rootUuid string
// If Thin-Provisioning set, mount init partition and move linux and initrd
// images to it.
var initMountpoint string
if settings.Cnf.ThinProvisioning {
initPartition, err := s.RootM.GetInit()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.3, err)
return err
}
initMountpoint = filepath.Join(systemNew, "boot", "init")
err = initPartition.Mount(initMountpoint)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.4, err)
return err
}
cq.Add(func(args ...interface{}) error {
return initPartition.Unmount()
}, nil, 80, &goodies.NoErrorHandler{}, false)
futureInitDir := filepath.Join(initMountpoint, partFuture.Label)
err = os.RemoveAll(futureInitDir)
if err != nil {
PrintVerboseWarn("ABSystem.RunOperation", 7.44)
}
err = os.MkdirAll(futureInitDir, 0o755)
if err != nil {
PrintVerboseWarn("ABSystem.RunOperation", 7.47, err)
}
err = CopyFile(
filepath.Join(systemNew, "boot", "vmlinuz-"+newKernelVer),
filepath.Join(futureInitDir, "vmlinuz-"+newKernelVer),
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.5, err)
return err
}
err = CopyFile(
filepath.Join(systemNew, "boot", "initrd.img-"+newKernelVer),
filepath.Join(futureInitDir, "initrd.img-"+newKernelVer),
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.6, err)
return err
}
os.Remove(filepath.Join(systemNew, "boot", "vmlinuz-"+newKernelVer))
os.Remove(filepath.Join(systemNew, "boot", "initrd.img-"+newKernelVer))
rootUuid = initPartition.Uuid
} else {
rootUuid = partFuture.Partition.Uuid
}
err = generateABGrubConf(
newKernelVer,
systemNew,
rootUuid,
partFuture.Label,
generatedGrubConfigPath,
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.7, err)
return err
}
// Create links back to the root system
err = s.CreateRootSymlinks(systemNew)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.8, err)
return err
}
// Stage 7: Sync /etc
// ------------------------------------------------
PrintVerboseSimple("[Stage 7] -------- ABSystemRunOperation")
oldEtc := "/.system/sysconf" // The current etc WITHOUT anything overlayed
presentEtc, err := s.RootM.GetPresent()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 8, err)
return err
}
futureEtc, err := s.RootM.GetFuture()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 8.1, err)
return err
}
oldUpperEtc := fmt.Sprintf("/var/lib/abroot/etc/%s", presentEtc.Label)
newUpperEtc := fmt.Sprintf("/var/lib/abroot/etc/%s", futureEtc.Label)
// make sure the future etc directories exist, ignoring errors
newWorkEtc := fmt.Sprintf("/var/lib/abroot/etc/%s-work", futureEtc.Label)
os.MkdirAll(newUpperEtc, 0o755)
os.MkdirAll(newWorkEtc, 0o755)
err = EtcBuilder.ExtBuildCommand(oldEtc, systemNew+"/sysconf", oldUpperEtc, newUpperEtc)
if err != nil {
PrintVerboseErr("AbSystem.RunOperation", 8.2, err)
return err
}
// Stage 8: Mount boot partition
// ------------------------------------------------
PrintVerboseSimple("[Stage 8] -------- ABSystemRunOperation")
tmpBootMount := "/run/abroot/tmp-boot-mount-1/"
err = os.MkdirAll(tmpBootMount, 0o755)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 9, err)
return err
}
err = partBoot.Mount(tmpBootMount)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 9.1, err)
return err
}
cq.Add(func(args ...interface{}) error {
return partBoot.Unmount()
}, nil, 100, &goodies.NoErrorHandler{}, false)
// Stage 9: Atomic swap the rootfs and abimage.abr
// ------------------------------------------------
PrintVerboseSimple("[Stage 9] -------- ABSystemRunOperation")
err = AtomicSwap(systemOld, systemNew)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 10, err)
return err
}
cq.Add(func(args ...interface{}) error {
return os.RemoveAll(systemNew)
}, nil, 20, &goodies.NoErrorHandler{}, false)
oldABImage := filepath.Join(partFuture.Partition.MountPoint, "abimage.abr")
newABImage := filepath.Join(partFuture.Partition.MountPoint, "abimage-new.abr")
// PartFuture may not have /abimage.abr if it got corrupted or was wiped.
// In these cases, create a dummy file for the atomic swap.
if _, err = os.Stat(oldABImage); os.IsNotExist(err) {
PrintVerboseInfo("ABSystem.RunOperation", "Creating dummy /part-future/abimage.abr")
os.Create(oldABImage)
}
err = AtomicSwap(oldABImage, newABImage)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 10.1, err)
return err
}
cq.Add(func(args ...interface{}) error {
return os.RemoveAll(newABImage)
}, nil, 30, &goodies.NoErrorHandler{}, false)
// Stage 10: Atomic swap the bootloader
// ------------------------------------------------
PrintVerboseSimple("[Stage 10] -------- ABSystemRunOperation")
grub, err := NewGrub(partBoot)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 11, err)
return err
}
// Only swap grub entries if we're booted into the present partition
isPresent, err := grub.IsBootedIntoPresentRoot()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 11.1, err)
return err
}
if isPresent {
grubCfgCurrent := filepath.Join(tmpBootMount, "grub/grub.cfg")
grubCfgFuture := filepath.Join(tmpBootMount, "grub/grub.cfg.future")
// Just like in Stage 9, tmpBootMount/grub/grub.cfg.future may not exist.
if _, err = os.Stat(grubCfgFuture); os.IsNotExist(err) {
PrintVerboseInfo("ABSystem.RunOperation", "Creating grub.cfg.future")
grubCfgContents, err := os.ReadFile(grubCfgCurrent)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 11.2, err)
}
var replacerPairs []string
if grub.FutureRoot == "a" {
replacerPairs = []string{
"default=1", "default=0",
"Previous State (A)", "Current State (A)",
"Current State (B)", "Previous State (B)",
}
} else {
replacerPairs = []string{
"default=0", "default=1",
"Current State (A)", "Previous State (A)",
"Previous State (B)", "Current State (B)",
}
}
replacer := strings.NewReplacer(replacerPairs...)
os.WriteFile(grubCfgFuture, []byte(replacer.Replace(string(grubCfgContents))), 0o644)
}
err = AtomicSwap(grubCfgCurrent, grubCfgFuture)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 11.3, err)
return err
}
}
err = s.createFinishedFile()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 11.4, err)
return fmt.Errorf("could not write finished file: %w", err)
}
PrintVerboseInfo("ABSystem.RunOperation", "upgrade completed")
return nil
}
Rollback swaps the master grub files if the current root is not the default
{
PrintVerboseInfo("ABSystem.Rollback", "starting")
cq := goodies.NewCleanupQueue()
defer cq.Run()
if s.finishedFileExists() {
if checkOnly {
return ROLLBACK_RES_NO, nil
}
return ROLLBACK_FAILED, errors.New("an operation finished successfully, can't roll back until reboot")
}
// we won't allow upgrades while rolling back
if !checkOnly {
err = s.LockOperation()
if err != nil {
PrintVerboseErr("ABSystem.Rollback", 0, err)
return ROLLBACK_FAILED, fmt.Errorf("can't lock operation: %w", err)
}
}
partBoot, err := s.RootM.GetBoot()
if err != nil {
PrintVerboseErr("ABSystem.Rollback", 1, err)
return ROLLBACK_FAILED, err
}
tmpBootMount := "/run/abroot/tmp-boot-mount-2/"
err = os.MkdirAll(tmpBootMount, 0o755)
if err != nil {
PrintVerboseErr("ABSystem.Rollback", 2, err)
return ROLLBACK_FAILED, err
}
err = partBoot.Mount(tmpBootMount)
if err != nil {
PrintVerboseErr("ABSystem.Rollback", 3, err)
return ROLLBACK_FAILED, err
}
cq.Add(func(args ...interface{}) error {
return partBoot.Unmount()
}, nil, 100, &goodies.NoErrorHandler{}, false)
grub, err := NewGrub(partBoot)
if err != nil {
PrintVerboseErr("ABSystem.Rollback", 4, err)
return ROLLBACK_FAILED, err
}
// Only swap grub entries if we're booted into the present partition
isPresent, err := grub.IsBootedIntoPresentRoot()
if err != nil {
PrintVerboseErr("ABSystem.Rollback", 5, err)
return ROLLBACK_FAILED, err
}
// If checkOnly is true, we stop here and return the appropriate response
if checkOnly {
response = ROLLBACK_RES_YES
if isPresent {
response = ROLLBACK_RES_NO
}
return response, nil
}
if isPresent {
PrintVerboseInfo("ABSystem.Rollback", "current root is the default, nothing to do")
return ROLLBACK_UNNECESSARY, nil
}
grubCfgCurrent := filepath.Join(tmpBootMount, "grub/grub.cfg")
grubCfgFuture := filepath.Join(tmpBootMount, "grub/grub.cfg.future")
// Just like in Stage 9, tmpBootMount/grub/grub.cfg.future may not exist.
if _, err = os.Stat(grubCfgFuture); os.IsNotExist(err) {
PrintVerboseInfo("ABSystem.Rollback", "Creating grub.cfg.future")
grubCfgContents, err := os.ReadFile(grubCfgCurrent)
if err != nil {
PrintVerboseErr("ABSystem.Rollback", 6, err)
}
var replacerPairs []string
if grub.FutureRoot == "a" {
replacerPairs = []string{
"default=1", "default=0",
"A (previous)", "A (current)",
"B (current)", "B (previous)",
}
} else {
replacerPairs = []string{
"default=0", "default=1",
"A (current)", "A (previous)",
"B (previous)", "B (current)",
}
}
replacer := strings.NewReplacer(replacerPairs...)
os.WriteFile(grubCfgFuture, []byte(replacer.Replace(string(grubCfgContents))), 0o644)
}
err = AtomicSwap(grubCfgCurrent, grubCfgFuture)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7, err)
return ROLLBACK_FAILED, err
}
// allow upgrades after rolling back
err = s.UnlockOperation()
if err != nil {
PrintVerboseErr("ABSystem.Rollback", 9, err)
PrintVerboseInfo("ABSystem.Rollback", "rollback completed with unlock failure")
}
PrintVerboseInfo("ABSystem.Rollback", "rollback completed")
return ROLLBACK_SUCCESS, nil
}
LockOperation creates a lock file, preventing upgrades from proceeding
Returns ErrOperationLocked if the operation is already locked by a running abroot instance
{
pid := os.Getpid()
pidData := []byte(strconv.Itoa(pid))
err := os.MkdirAll(filepath.Dir(operationLockFile), 0o755)
if err != nil {
PrintVerboseWarn("ABSystem.LockOperation", 1, err)
}
if _, err = os.Stat(operationLockFile); err == nil {
if s.isLockfilePidActive() {
return ErrOperationLocked
}
}
err = os.WriteFile(operationLockFile, pidData, 0o644)
if err != nil {
os.Remove(operationLockFile)
return fmt.Errorf("can't write lockfile: %w", err)
}
PrintVerboseInfo("ABSystem.LockOperation", "lock file created")
return nil
}
UnlockOperation removes the lock file, allowing upgrades to proceed
{
err := os.Remove(operationLockFile)
if err != nil {
PrintVerboseErr("ABSystem.UnlockOperation", 0, err)
return err
}
PrintVerboseInfo("ABSystem.UnlockOperation", "lock file removed")
return nil
}
{
_, err := os.Stat(finishedOperationFile)
return !errors.Is(err, os.ErrNotExist)
}
{
os.MkdirAll(filepath.Dir(finishedOperationFile), 0o755)
_, err := os.Create(finishedOperationFile)
if err != nil {
return err
}
return nil
}
{
runningPid, err := os.ReadFile(operationLockFile)
if errors.Is(err, os.ErrNotExist) {
return false
}
if err != nil {
PrintVerboseErr("ABSystem.RemoveStageFile", 0, err)
return true
}
if string(runningPid) == "" {
PrintVerboseWarn("ABSystem.isLockfilePidActive", 1, "lock file does not contain PID")
return true
}
if _, err := os.Stat(filepath.Join("/proc", string(runningPid))); errors.Is(err, os.ErrNotExist) {
return false
}
return true
}
{
os.MkdirAll(filepath.Dir(finalizingFile), 0o755)
_, err := os.Create(finalizingFile)
if err != nil {
return err
}
return nil
}
{
err := os.Remove(finalizingFile)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
ABSystemOperation represents a system operation to be performed by the
ABSystem, must be given as a parameter to the RunOperation function.
string
ABRollbackResponse represents the response of a rollback operation
string
NewABSystem initializes a new ABSystem, which contains all the functions
to perform system operations such as upgrades, package changes and rollback.
It returns a pointer to the initialized ABSystem and an error, if any.
{
PrintVerboseInfo("NewABSystem: running...")
i, err := NewABImageFromRoot()
if err != nil {
PrintVerboseErr("NewABSystem", 0, err)
return nil, err
}
c := NewChecks()
r := NewRegistry()
rm := NewABRootManager()
return &ABSystem{
Checks: c,
RootM: rm,
Registry: r,
CurImage: i,
}, nil
}
UserStopRequested checks if the user lock file exists and returns a boolean
note that if the user lock file exists, it means that the user explicitly
requested the upgrade to be locked (using an update manager for example)
{
if _, err := os.Stat(userStopFile); os.IsNotExist(err) {
return false
}
PrintVerboseInfo("ABSystem.UserStopRequested", "lock file exists")
return true
}
MakeStopRequest requests all other abroot operations to stop
It also prevents any new operations from running.
{
os.MkdirAll(filepath.Dir(userStopFile), 0o755)
err := os.WriteFile(userStopFile, []byte{}, 0o644)
if err != nil {
return fmt.Errorf("could not write stop file: %w", err)
}
return nil
}
CancelStopRequest removes the stop request
{
if !UserStopRequested() {
return nil
}
err := os.Remove(userStopFile)
if err != nil {
return fmt.Errorf("could remove stop file: %w", err)
}
return nil
}
ConfEditResult is the result of the ConfEdit function
int
ConfEdit opens the configuration file in the default editor
{
editor := os.Getenv("EDITOR")
if editor == "" {
nanoBin, err := exec.LookPath("nano")
if err == nil {
editor = nanoBin
}
viBin, err := exec.LookPath("vi")
if err == nil {
editor = viBin
}
editorBin, err := exec.LookPath("editor")
if err == nil {
editor = editorBin
}
if editor == "" {
return CONF_FAILED, fmt.Errorf("no editor found in $EDITOR, nano or vi")
}
}
// getting the configuration content so as we can compare it later
// to see if it has been changed
cnfContent, err := os.ReadFile(settings.CnfPathAdmin)
if err != nil {
return CONF_FAILED, err
}
tmpFilePath := settings.CnfPathAdmin + ".tmp.json"
err = os.WriteFile(tmpFilePath, cnfContent, 0o755)
if err != nil {
return CONF_FAILED, err
}
defer os.Remove(tmpFilePath)
// open the editor
cmd := exec.Command(editor, tmpFilePath)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return CONF_FAILED, err
}
// getting the new content
newCnfContent, err := os.ReadFile(tmpFilePath)
if err != nil {
return CONF_FAILED, err
}
// we compare the old and new content to return the proper result
if string(cnfContent) == string(newCnfContent) {
return CONF_UNCHANGED, nil
}
viper.SetConfigFile(tmpFilePath)
err = viper.ReadInConfig()
if err != nil {
return CONF_FAILED, err
}
err = AtomicSwap(settings.CnfPathAdmin, tmpFilePath)
if err != nil {
return CONF_FAILED, err
}
return CONF_CHANGED, nil
}
A Registry instance exposes functions to interact with the configured
Docker registry
HasUpdate checks if the image/tag from the registry has a different digest
it returns the new digest and a boolean indicating if an update is available
{
PrintVerboseInfo("Registry.HasUpdate", "Checking for updates ...")
token, err := GetToken()
if err != nil {
PrintVerboseErr("Registry.HasUpdate", 0, err)
return "", false, err
}
manifest, err := r.GetManifest(token)
if err != nil {
PrintVerboseErr("Registry.HasUpdate", 1, err)
return "", false, err
}
if manifest.Digest == digest {
PrintVerboseInfo("Registry.HasUpdate", "no update available")
return "", false, nil
}
PrintVerboseInfo("Registry.HasUpdate", "update available. Old digest", digest, "new digest", manifest.Digest)
return manifest.Digest, true, nil
}
GetManifest returns the manifest of the image, a token is required
to perform the request and is generated using GetToken()
{
PrintVerboseInfo("Registry.GetManifest", "running...")
manifestAPIUrl := fmt.Sprintf("%s/%s/manifests/%s", r.API, settings.Cnf.Name, settings.Cnf.Tag)
PrintVerboseInfo("Registry.GetManifest", "call URI is", manifestAPIUrl)
req, err := http.NewRequest("GET", manifestAPIUrl, nil)
if err != nil {
PrintVerboseErr("Registry.GetManifest", 0, err)
return nil, err
}
req.Header.Set("User-Agent", "abroot")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Accept", "application/vnd.oci.image.manifest.v1+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
PrintVerboseErr("Registry.GetManifest", 1, err)
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
PrintVerboseErr("Registry.GetManifest", 2, err)
return nil, err
}
m := make(map[string]interface{})
err = json.Unmarshal(body, &m)
if err != nil {
PrintVerboseErr("Registry.GetManifest", 3, err)
return nil, err
}
// If the manifest contains an errors property, it means that the
// request failed. Ref: https://github.com/Vanilla-OS/ABRoot/issues/285
if m["errors"] != nil {
errors := m["errors"].([]interface{})
for _, e := range errors {
err := e.(map[string]interface{})
PrintVerboseErr("Registry.GetManifest", 3.5, err)
return nil, fmt.Errorf("Registry error: %s", err["code"])
}
}
// digest is stored in the header
digest := resp.Header.Get("Docker-Content-Digest")
// we need to parse the layers to get the digests
var layerDigests []string
if m["layers"] == nil && m["fsLayers"] == nil {
PrintVerboseErr("Registry.GetManifest", 4, err)
return nil, fmt.Errorf("Manifest does not contain layer property")
} else if m["layers"] == nil && m["fsLayers"] != nil {
PrintVerboseWarn("Registry.GetManifest", 4, "layers property not found, using fsLayers")
layers := m["fsLayers"].([]interface{})
for _, layer := range layers {
layerDigests = append(layerDigests, layer.(map[string]interface{})["blobSum"].(string))
}
} else {
layers := m["layers"].([]interface{})
var layerDigests []string
for _, layer := range layers {
layerDigests = append(layerDigests, layer.(map[string]interface{})["digest"].(string))
}
}
PrintVerboseInfo("Registry.GetManifest", "success")
manifest := &Manifest{
Manifest: body,
Digest: digest,
Layers: layerDigests,
}
return manifest, nil
}
Manifest is the struct used to parse the manifest response from the registry
it contains the manifest itself, the digest and the list of layers. This
should be compatible with most registries, but it's not guaranteed
NewRegistry returns a new Registry instance, exposing functions to
interact with the configured Docker registry
{
PrintVerboseInfo("NewRegistry", "running...")
return &Registry{
API: fmt.Sprintf("https://%s/%s", settings.Cnf.Registry, settings.Cnf.RegistryAPIVersion),
}
}
{
requestUrl := fmt.Sprintf(
"https://%s/%s/",
settings.Cnf.Registry,
settings.Cnf.RegistryAPIVersion,
)
resp, err := http.Get(requestUrl)
if err != nil {
return "", "", err
}
if resp.StatusCode == 401 {
authUrl := resp.Header["www-authenticate"]
if len(authUrl) == 0 {
authUrl = resp.Header["Www-Authenticate"]
if len(authUrl) == 0 {
return "", "", fmt.Errorf("unable to find authentication url for registry")
}
}
return strings.Split(strings.Split(authUrl[0], "realm=\"")[1], "\",")[0], strings.Split(strings.Split(authUrl[0], "service=\"")[1], "\"")[0], nil
} else {
PrintVerboseInfo("Registry.getRegistryAuthUrl", "registry does not require authentication")
return fmt.Sprintf("https://%s/", settings.Cnf.Registry), settings.Cnf.RegistryService, nil
}
}
GetToken generates a token using the provided tokenURL and returns it
{
authUrl, serviceUrl, err := getRegistryAuthUrl()
if err != nil {
return "", err
}
requestUrl := fmt.Sprintf(
"%s?scope=repository:%s:pull&service=%s",
authUrl,
settings.Cnf.Name,
serviceUrl,
)
PrintVerboseInfo("Registry.GetToken", "call URI is", requestUrl)
resp, err := http.Get(requestUrl)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusForbidden {
return "", ErrImageNotFound
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("token request failed with status code: %d", resp.StatusCode)
}
tokenBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// Parse token from response
var tokenResponse struct {
Token string `json:"token"`
}
err = json.Unmarshal(tokenBytes, &tokenResponse)
if err != nil {
return "", err
}
token := tokenResponse.Token
return token, nil
}
rsyncCmd executes the rsync command with the requested options.
If silent is true, rsync progress will not appear in stdout.
{
args := []string{"-avxHAX"}
args = append(args, opts...)
args = append(args, src)
args = append(args, dst)
cmd := exec.Command("rsync", args...)
stdout, _ := cmd.StdoutPipe()
var totalFiles int
if !silent {
countCmdOut, _ := exec.Command(
"/bin/sh",
"-c",
fmt.Sprintf("echo -n $(($(rsync --dry-run %s | wc -l) - 4))", strings.Join(args, " ")),
).Output()
totalFiles, _ = strconv.Atoi(string(countCmdOut))
}
reader := bufio.NewReader(stdout)
err := cmd.Start()
if err != nil {
return err
}
if !silent {
verbose := IsVerbose()
p, _ := cmdr.ProgressBar.WithTotal(totalFiles).WithTitle("Sync in progress").WithMaxWidth(120).Start()
maxLineLen := cmdr.TerminalWidth() / 4
for i := 0; i < p.Total; i++ {
line, _ := reader.ReadString('\n')
line = strings.TrimSpace(line)
if verbose {
cmdr.Info.Println(line + " synced")
}
if len(line) > maxLineLen {
startingLen := len(line) - maxLineLen + 1
line = "<" + line[startingLen:]
} else {
padding := maxLineLen - len(line)
line += strings.Repeat(" ", padding)
}
p.UpdateTitle("Syncing " + line)
p.Increment()
}
} else {
stdout.Close()
}
err = cmd.Wait()
if err != nil {
// exit status 24 is a warning, not an error, we don't care about it
// since rsync is going to be removed in the OCI version
if !strings.Contains(err.Error(), "exit status 24") {
return err
}
}
return nil
}
rsyncDryRun executes the rsync command with the --dry-run option.
{
opts := []string{"--dry-run"}
if len(excluded) > 0 {
for _, exclude := range excluded {
opts = append(opts, "--exclude="+exclude)
}
}
return rsyncCmd(src, dst, opts, false)
}
AtomicRsync executes the rsync command in an atomic-like manner.
It does so by dry-running the rsync, and if it succeeds, it runs
the rsync again performing changes.
If the keepUnwanted option
is set to true, it will omit the --delete option, so that the already
existing and unwanted files will not be deleted.
To ensure the changes are applied atomically, we rsync on a _new directory first,
and use atomicSwap to replace the _new with the dst directory.
{
PrintVerboseInfo("AtomicRsync", "Running...")
if _, err := os.Stat(transitionalPath); os.IsNotExist(err) {
err = os.Mkdir(transitionalPath, 0755)
if err != nil {
PrintVerboseErr("AtomicRsync", 0, err)
return err
}
}
PrintVerboseInfo("AtomicRsync", "Starting dry run process...")
err := rsyncDryRun(src, transitionalPath, excluded)
if err != nil {
return err
}
opts := []string{"--link-dest", dst, "--exclude", finalPath, "--exclude", transitionalPath}
if len(excluded) > 0 {
for _, exclude := range excluded {
opts = append(opts, "--exclude", exclude)
}
}
if !keepUnwanted {
opts = append(opts, "--delete")
}
PrintVerboseInfo("AtomicRsync", "Starting rsync process...")
err = rsyncCmd(src, transitionalPath, opts, true)
if err != nil {
return err
}
PrintVerboseInfo("AtomicRsync", "Starting atomic swap process...")
err = AtomicSwap(transitionalPath, finalPath)
if err != nil {
return err
}
PrintVerboseInfo("AtomicRsync", "Removing transitional path...")
return os.RemoveAll(transitionalPath)
}
{
if !RootCheck(false) {
return
}
if _, err := os.Stat(abrootDir); os.IsNotExist(err) {
err := os.Mkdir(abrootDir, 0755)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
}
{
if os.Geteuid() != 0 {
if display {
fmt.Println("You must be root to run this command")
}
return false
}
return true
}
fileExists checks if a file exists
{
if _, err := os.Stat(path); err == nil {
PrintVerboseInfo("fileExists", "File exists:", path)
return true
}
PrintVerboseInfo("fileExists", "File does not exist:", path)
return false
}
isLink checks if a path is a link
{
if fileInfo, err := os.Lstat(path); err == nil && fileInfo.Mode().Type() == os.ModeSymlink {
PrintVerboseInfo("isLink", "Path is a link:", path)
return true
}
PrintVerboseInfo("isLink", "Path is not a link:", path)
return false
}
CopyFile copies a file from source to dest
{
PrintVerboseInfo("CopyFile", "Running...")
PrintVerboseInfo("CopyFile", "Opening source file")
srcFile, err := os.Open(source)
if err != nil {
PrintVerboseErr("CopyFile", 0, err)
return err
}
defer srcFile.Close()
PrintVerboseInfo("CopyFile", "Opening destination file")
destFile, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
PrintVerboseErr("CopyFile", 1, err)
return err
}
defer destFile.Close()
PrintVerboseInfo("CopyFile", "Performing copy operation")
if _, err := io.Copy(destFile, srcFile); err != nil {
PrintVerboseErr("CopyFile", 2, err)
return err
}
return nil
}
isDeviceLUKSEncrypted checks whether a device specified by devicePath is a LUKS-encrypted device
{
PrintVerboseInfo("isDeviceLUKSEncrypted", "Verifying if", devicePath, "is encrypted")
isLuksCmd := "cryptsetup isLuks %s"
cmd := exec.Command("sh", "-c", fmt.Sprintf(isLuksCmd, devicePath))
err := cmd.Run()
if err != nil {
// We expect the command to return exit status 1 if partition isn't
// LUKS-encrypted
if exitError, ok := err.(*exec.ExitError); ok {
if exitError.ExitCode() == 1 {
return false, nil
}
}
err = fmt.Errorf("failed to check if %s is LUKS-encrypted: %s", devicePath, err)
PrintVerboseErr("isDeviceLUKSEncrypted", 0, err)
return false, err
}
return true, nil
}
getDirSize calculates the total size of a directory recursively.
{
ds, err := os.Stat(path)
if err != nil {
return 0, err
}
if !ds.IsDir() {
return 0, fmt.Errorf("%s is not a directory", path)
}
inodes := map[uint64]bool{}
var totalSize int64 = 0
dfs := os.DirFS(path)
err = fs.WalkDir(dfs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
fileinfo, err := d.Info()
if err != nil {
return err
}
fileinfoSys := fileinfo.Sys().(*syscall.Stat_t)
if fileinfoSys.Nlink > 1 {
if _, ok := inodes[fileinfoSys.Ino]; !ok {
totalSize += fileinfo.Size()
inodes[fileinfoSys.Ino] = true
}
} else {
totalSize += fileinfo.Size()
inodes[fileinfoSys.Ino] = true
}
}
return nil
})
if err != nil {
return 0, err
}
return totalSize, nil
}
DiskManager exposes functions to interact with the system's disks
and partitions (e.g. mount, unmount, get partitions, etc.)
GetPartitionByLabel finds a partition by searching for its label.
If no partition can be found with the given label, returns error.
{
PrintVerboseInfo("DiskManager.GetPartitionByLabel", "retrieving partitions")
partitions, err := d.GetPartitions("")
if err != nil {
PrintVerboseErr("DiskManager.GetPartitionByLabel", 0, err)
return Partition{}, err
}
for _, part := range partitions {
if part.Label == label {
PrintVerboseInfo("DiskManager.GetPartitionByLabel", "Partition with UUID", part.Uuid, "has label", label)
return part, nil
}
}
errMsg := fmt.Errorf("could not find partition with label %s", label)
PrintVerboseErr("DiskManager.GetPartitionByLabel", 1, errMsg)
return Partition{}, errMsg
}
getPartitions gets a disk's partitions. If device is an empty string, gets
all partitions from all disks
{
PrintVerboseInfo("DiskManager.getPartitions", "running...")
output, err := exec.Command("lsblk", "-J", "-o", "NAME,FSTYPE,LABEL,MOUNTPOINT,UUID").Output()
if err != nil {
PrintVerboseErr("DiskManager.getPartitions", 0, err)
return nil, err
}
var partitions struct {
BlockDevices []struct {
Name string `json:"name"`
Type string `json:"type"`
Children []Children `json:"children"`
} `json:"blockdevices"`
}
if err := json.Unmarshal(output, &partitions); err != nil {
PrintVerboseErr("DiskManager.getPartitions", 1, err)
return nil, err
}
var result []Partition
for _, blockDevice := range partitions.BlockDevices {
if device != "" && blockDevice.Name != device {
continue
}
iterChildren(&blockDevice.Children, &result)
}
PrintVerboseInfo("DiskManager.getPartitions", "successfully got partitions for disk", device)
return result, nil
}
Partition represents either a standard partition or a device-mapper
partition, such as an LVM volume
Mount mounts a partition to a directory, returning an error if any occurs
{
PrintVerboseInfo("Partition.Mount", "running...")
if _, err := os.Stat(destination); os.IsNotExist(err) {
if err := os.MkdirAll(destination, 0755); err != nil {
PrintVerboseErr("Partition.Mount", 0, err)
return err
}
}
devicePath := "/dev/"
if p.IsDevMapper() {
devicePath += "mapper/"
}
devicePath += p.Device
err := syscall.Mount(devicePath, destination, p.FsType, 0, "")
if err != nil {
PrintVerboseErr("Partition.Mount", 1, err)
return err
}
p.MountPoint = destination
PrintVerboseInfo("Partition.Mount", "successfully mounted", devicePath, "to", destination)
return nil
}
Unmount unmounts a partition
{
PrintVerboseInfo("Partition.Unmount", "running...")
if p.MountPoint == "" {
PrintVerboseErr("Partition.Unmount", 0, errors.New("no mount point"))
return errors.New("no mount point")
}
err := syscall.Unmount(p.MountPoint, 0)
if err != nil {
PrintVerboseErr("Partition.Unmount", 1, err)
return err
}
PrintVerboseInfo("Partition.Unmount", "successfully unmounted", p.MountPoint)
p.MountPoint = ""
return nil
}
Returns whether the partition is a device-mapper virtual partition
{
return p.Parent != nil
}
IsEncrypted returns whether the partition is encrypted
{
return strings.HasPrefix(p.FsType, "crypto_")
}
The children a block device or partition may have
NewDiskManager creates and returns a pointer to a new DiskManager instance
from which you can interact with the system's disks and partitions
{
return &DiskManager{}
}
iterChildren iterates through the children of a device or partition
recursively
{
for _, child := range *childs {
*result = append(*result, Partition{
Label: child.Label,
MountPoint: child.MountPoint,
MountOptions: child.MountOptions,
Uuid: child.Uuid,
FsType: child.FsType,
Device: child.LogicalName,
})
currentPartitions := len(*result)
iterChildren(&child.Children, result)
detectedPartitions := len(*result) - currentPartitions
// Populate children's reference to parent
for i := currentPartitions; i < len(*result); i++ {
if (*result)[i].Parent == nil {
(*result)[i].Parent = &(*result)[len(*result)-detectedPartitions-1]
}
}
}
}
getKernelVersion returns the latest kernel version available in the root
{
PrintVerboseInfo("getKernelVersion", "running...")
kernelDir := filepath.Join(bootPath, "vmlinuz-*")
files, err := filepath.Glob(kernelDir)
if err != nil {
PrintVerboseErr("getKernelVersion", 0, err)
return ""
}
if len(files) == 0 {
PrintVerboseErr("getKernelVersion", 1, errors.New("no kernel found"))
return ""
}
var maxVer *version.Version
for _, file := range files {
verStr := filepath.Base(file)[8:]
ver, err := version.NewVersion(verStr)
if err != nil {
continue
}
if maxVer == nil || ver.GreaterThan(maxVer) {
maxVer = ver
}
}
if maxVer != nil {
return maxVer.String()
}
return ""
}
An ImageRecipe represents a Dockerfile/Containerfile-like recipe
Write writes a ImageRecipe to the given path, returning an error if any
{
PrintVerboseInfo("ImageRecipe.Write", "running...")
// create file
file, err := os.Create(path)
if err != nil {
PrintVerboseErr("ImageRecipe.Write", 0, err)
return err
}
defer file.Close()
// write from
_, err = file.WriteString(fmt.Sprintf("FROM %s\n", c.From))
if err != nil {
PrintVerboseErr("ImageRecipe.Write", 1, err)
return err
}
// write labels
for key, value := range c.Labels {
_, err = file.WriteString(fmt.Sprintf("LABEL %s=%s\n", key, value))
if err != nil {
PrintVerboseErr("ImageRecipe.Write", 2, err)
return err
}
}
// write args
for key, value := range c.Args {
_, err = file.WriteString(fmt.Sprintf("ARG %s=%s\n", key, value))
if err != nil {
PrintVerboseErr("ImageRecipe.Write", 3, err)
return err
}
}
// write content
_, err = file.WriteString(c.Content)
if err != nil {
PrintVerboseErr("ImageRecipe.Write", 4, err)
return err
}
PrintVerboseInfo("ImageRecipe.Write", "done")
return nil
}
NewImageRecipe creates a new ImageRecipe instance and returns a pointer to it
{
PrintVerboseInfo("NewImageRecipe", "running...")
return &ImageRecipe{
From: image,
Labels: labels,
Args: args,
Content: content,
}
}
The ABImage is the representation of an OCI image used by ABRoot, it
contains the digest, the timestamp and the image name. If you need to
investigate the current ABImage on an ABRoot system, you can find it
at /abimage.abr
WriteTo writes the json to a destination path, if the suffix is not empty,
it will be appended to the filename
{
PrintVerboseInfo("ABImage.WriteTo", "running...")
if _, err := os.Stat(dest); os.IsNotExist(err) {
err = os.MkdirAll(dest, 0755)
if err != nil {
PrintVerboseErr("ABImage.WriteTo", 0, err)
return err
}
}
if suffix != "" {
suffix = "-" + suffix
}
imageName := "abimage" + suffix + ".abr"
imagePath := filepath.Join(dest, imageName)
abimage, err := json.Marshal(a)
if err != nil {
PrintVerboseErr("ABImage.WriteTo", 1, err)
return err
}
err = os.WriteFile(imagePath, abimage, 0644)
if err != nil {
PrintVerboseErr("ABImage.WriteTo", 2, err)
return err
}
PrintVerboseInfo("ABImage.WriteTo", "successfully wrote abimage.abr to "+imagePath)
return nil
}
NewABImage creates a new ABImage instance and returns a pointer to it,
if the digest is empty, it returns an error
{
if digest == "" {
return nil, fmt.Errorf("NewABImage: digest is empty")
}
return &ABImage{
Digest: digest,
Timestamp: time.Now(),
Image: image,
}, nil
}
NewABImageFromRoot returns the current ABImage by parsing /abimage.abr, if
it fails, it returns an error (e.g. if the file doesn't exist).
Note for distro maintainers: if the /abimage.abr is not present, it could
mean that the user is running an older version of ABRoot (pre v2) or the
root state is corrupted. In the latter case, generating a new ABImage should
fix the issue, Digest and Timestamp can be random, but Image should reflect
an existing image on the configured Docker registry. Anyway, support on this
is not guaranteed, so please don't open issues about this.
{
PrintVerboseInfo("NewABImageFromRoot", "running...")
abimage, err := os.ReadFile("/abimage.abr")
if err != nil {
PrintVerboseErr("NewABImageFromRoot", 0, err)
return nil, err
}
var a ABImage
err = json.Unmarshal(abimage, &a)
if err != nil {
PrintVerboseErr("NewABImageFromRoot", 1, err)
return nil, err
}
PrintVerboseInfo("NewABImageFromRoot", "found abimage.abr: "+a.Digest)
return &a, nil
}
{
fixupOlderSystems(rootPath)
err = repairLinks(rootPath)
if err != nil {
return err
}
err = repairPaths(rootPath)
if err != nil {
return err
}
return nil
}
{
for _, link := range linksToRepair {
sourceAbs := filepath.Join(rootPath, link[0])
targetAbs := filepath.Join(rootPath, link[1])
err = repairLink(sourceAbs, targetAbs)
if err != nil {
return err
}
}
return nil
}
{
target := targetAbs
source, err := filepath.Rel(filepath.Dir(target), sourceAbs)
if err != nil {
PrintVerboseErr("repairLink", 1, "Can't make ", source, " relative to ", target, " : ", err)
return err
}
if !isLink(target) {
err = os.RemoveAll(target)
if err != nil && !os.IsNotExist(err) {
PrintVerboseErr("repairLink", 2, "Can't remove ", target, " : ", err)
return err
}
PrintVerboseInfo("repairLink", "Repairing ", target, " -> ", source)
err = os.Symlink(source, target)
if err != nil {
return err
}
}
return nil
}
{
for _, path := range pathsToRepair {
err = repairPath(filepath.Join(rootPath, path))
if err != nil {
return err
}
}
return nil
}
{
if info, err := os.Lstat(path); err == nil && info.IsDir() {
return nil
}
err = os.Remove(path)
if err != nil && !os.IsNotExist(err) {
PrintVerboseErr("repairPath", 1, "Can't remove ", path, " : ", err)
return err
}
PrintVerboseInfo("repairPath", "Repairing ", path)
err = os.MkdirAll(path, 0o755)
if err != nil {
PrintVerboseErr("repairPath", 2, "Can't create ", path, " : ", err)
return err
}
return nil
}
this is here to keep compatibility with older systems
e.g. /media was a folder instead of a mountpoint for /var/media
{
paths := []string{
"media",
"mnt",
"root",
}
for _, path := range paths {
legacyPath := filepath.Join(rootPath, path)
newPath := filepath.Join("/var", path)
if _, err := os.Lstat(newPath); errors.Is(err, os.ErrNotExist) {
err = exec.Command("mv", legacyPath, newPath).Run()
if err != nil {
PrintVerboseErr("fixupOlderSystems", 1, "could not move ", legacyPath, " to ", newPath, " : ", err)
// if moving failed it probably means that it migrated successfully in the past
// so it's safe to ignore errors
}
}
}
}
PackageManager struct
Add adds a package to the packages.add file
{
PrintVerboseInfo("PackageManager.Add", "running...")
// Check for package manager status and user agreement
err := p.CheckStatus()
if err != nil {
PrintVerboseErr("PackageManager.Add", 0, err)
return err
}
// Check if package was removed before
packageWasRemoved := false
removedIndex := -1
pkgsRemove, err := p.GetRemovePackages()
if err != nil {
PrintVerboseErr("PackageManager.Add", 2.1, err)
return err
}
for i, rp := range pkgsRemove {
if rp == pkg {
packageWasRemoved = true
removedIndex = i
break
}
}
// packages that have been removed by the user aren't always in the repo
if !packageWasRemoved {
// Check if package exists in repo
for _, _pkg := range strings.Split(pkg, " ") {
err := p.ExistsInRepo(_pkg)
if err != nil {
PrintVerboseErr("PackageManager.Add", 0, err)
return err
}
}
}
// If package was removed by the user, simply remove it from packages.remove
if packageWasRemoved {
pkgsRemove = append(pkgsRemove[:removedIndex], pkgsRemove[removedIndex+1:]...)
PrintVerboseInfo("PackageManager.Add", "unsetting manually removed package")
return p.writeRemovePackages(pkgsRemove)
}
// Abort if package is already added
pkgsAdd, err := p.GetAddPackages()
if err != nil {
PrintVerboseErr("PackageManager.Add", 3, err)
return err
}
for _, p := range pkgsAdd {
if p == pkg {
PrintVerboseInfo("PackageManager.Add", "package already added")
return nil
}
}
pkgsAdd = append(pkgsAdd, pkg)
PrintVerboseInfo("PackageManager.Add", "writing packages.add")
return p.writeAddPackages(pkgsAdd)
}
Remove either removes a manually added package from packages.add or adds
a package to be deleted into packages.remove
{
PrintVerboseInfo("PackageManager.Remove", "running...")
// Check for package manager status and user agreement
err := p.CheckStatus()
if err != nil {
PrintVerboseErr("PackageManager.Remove", 0, err)
return err
}
// Check if package exists in packages.add
pkgsAddList, err := p.GetAddPackagesString(" ")
if err != nil {
PrintVerboseErr("PackageManager.Remove", 1, err)
return err
}
if !strings.Contains(pkgsAddList, pkg) {
// Check if package exists in repo
// FIXME: this should also check if the package is installed in
// different systems, not just debian-based ditros.. Since this is a
// distro specific feature, I'm leaving it as is for now.
err = p.ExistsOnSystem(pkg)
if err != nil {
PrintVerboseErr("PackageManager.Remove", 1, err)
return err
}
}
// If package was added by the user, simply remove it from packages.add
pkgsAdd, err := p.GetAddPackages()
if err != nil {
PrintVerboseErr("PackageManager.Remove", 4, err)
return err
}
for i, ap := range pkgsAdd {
if ap == pkg {
pkgsAdd = append(pkgsAdd[:i], pkgsAdd[i+1:]...)
PrintVerboseInfo("PackageManager.Remove", "removing manually added package")
return p.writeAddPackages(pkgsAdd)
}
}
// Abort if package is already removed
pkgsRemove, err := p.GetRemovePackages()
if err != nil {
PrintVerboseErr("PackageManager.Remove", 5, err)
return err
}
for _, p := range pkgsRemove {
if p == pkg {
PrintVerboseInfo("PackageManager.Remove", "package already removed")
return nil
}
}
pkgsRemove = append(pkgsRemove, pkg)
// Otherwise, add package to packages.remove
PrintVerboseInfo("PackageManager.Remove", "writing packages.remove")
return p.writeRemovePackages(pkgsRemove)
}
GetAddPackages returns the packages in the packages.add file
{
PrintVerboseInfo("PackageManager.GetAddPackages", "running...")
return p.getPackages(PackagesAddFile)
}
GetRemovePackages returns the packages in the packages.remove file
{
PrintVerboseInfo("PackageManager.GetRemovePackages", "running...")
return p.getPackages(PackagesRemoveFile)
}
GetUnstagedPackages returns the package changes that are yet to be applied
{
PrintVerboseInfo("PackageManager.GetUnstagedPackages", "running...")
alreadyAdded, alreadyRemoved, err := p.GetCurrentlyInstalledPackages(rootPath)
if err != nil {
PrintVerboseErr("PackageManager.GetUnstagedPackages", 1, err)
return toBeAdded, toBeRemoved, err
}
shouldBeAdded, err := p.GetAddPackages()
if err != nil {
PrintVerboseErr("PackageManager.GetUnstagedPackages", 2, err)
return toBeAdded, toBeRemoved, err
}
shouldBeRemoved, err := p.GetRemovePackages()
if err != nil {
PrintVerboseErr("PackageManager.GetUnstagedPackages", 2, err)
return toBeAdded, toBeRemoved, err
}
addNew, removeAdded := sliceDifference(shouldBeAdded, alreadyAdded)
removeNew, addRemoved := sliceDifference(shouldBeRemoved, alreadyRemoved)
toBeAdded = append(addNew, addRemoved...)
toBeRemoved = append(removeNew, removeAdded...)
return toBeAdded, toBeRemoved, nil
}
GetAddPackagesString returns the packages in the packages.add file as a string
{
PrintVerboseInfo("PackageManager.GetAddPackagesString", "running...")
pkgs, err := p.GetAddPackages()
if err != nil {
PrintVerboseErr("PackageManager.GetAddPackagesString", 0, err)
return "", err
}
PrintVerboseInfo("PackageManager.GetAddPackagesString", "done")
return strings.Join(pkgs, sep), nil
}
GetRemovePackagesString returns the packages in the packages.remove file as a string
{
PrintVerboseInfo("PackageManager.GetRemovePackagesString", "running...")
pkgs, err := p.GetRemovePackages()
if err != nil {
PrintVerboseErr("PackageManager.GetRemovePackagesString", 0, err)
return "", err
}
PrintVerboseInfo("PackageManager.GetRemovePackagesString", "done")
return strings.Join(pkgs, sep), nil
}
{
PrintVerboseInfo("PackageManager.getPackages", "running...")
pkgs := []string{}
f, err := os.Open(filepath.Join(p.baseDir, file))
if err != nil {
PrintVerboseErr("PackageManager.getPackages", 0, err)
return pkgs, err
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
PrintVerboseErr("PackageManager.getPackages", 1, err)
return pkgs, err
}
for _, pkg := range strings.Split(string(b), "\n") {
pkgTrimmed := strings.TrimSpace(pkg)
if pkgTrimmed != "" {
pkgs = append(pkgs, pkgTrimmed)
}
}
PrintVerboseInfo("PackageManager.getPackages", "returning packages")
return pkgs, nil
}
{
PrintVerboseInfo("PackageManager.writeAddPackages", "running...")
return p.writePackages(PackagesAddFile, pkgs)
}
{
PrintVerboseInfo("PackageManager.writeRemovePackages", "running...")
return p.writePackages(PackagesRemoveFile, pkgs)
}
{
PrintVerboseInfo("PackageManager.writePackages", "running...")
f, err := os.Create(filepath.Join(p.baseDir, file))
if err != nil {
PrintVerboseErr("PackageManager.writePackages", 0, err)
return err
}
defer f.Close()
for _, pkg := range pkgs {
if pkg == "" {
continue
}
_, err = fmt.Fprintf(f, "%s\n", pkg)
if err != nil {
PrintVerboseErr("PackageManager.writePackages", 1, err)
return err
}
}
PrintVerboseInfo("PackageManager.writePackages", "packages written")
return nil
}
{
addPkgs, err := p.GetAddPackagesString(" ")
if err != nil {
PrintVerboseErr("PackageManager.processUpgradePackages", 0, err)
return
}
removePkgs, err := p.GetRemovePackagesString(" ")
if err != nil {
PrintVerboseErr("PackageManager.processUpgradePackages", 1, err)
return
}
if len(addPkgs) == 0 && len(removePkgs) == 0 {
PrintVerboseInfo("PackageManager.processUpgradePackages", "no packages to install or remove")
return []string{}, nil
}
if removePkgs != "" {
finalRemovePkgs := fmt.Sprintf("%s %s", settings.Cnf.IPkgMngRm, removePkgs)
commands = append(commands, finalRemovePkgs)
}
if addPkgs != "" {
finalAddPkgs := fmt.Sprintf("%s %s", settings.Cnf.IPkgMngAdd, addPkgs)
commands = append(commands, finalAddPkgs)
}
return commands, nil
}
{
PrintVerboseInfo("PackageManager.GetFinalCmd", "running...")
commands, err := p.createPackageCommands()
if err != nil {
return "", err
}
if len(commands) == 0 {
return "", nil
}
preExec := settings.Cnf.IPkgMngPre
postExec := settings.Cnf.IPkgMngPost
if preExec != "" {
commands = append([]string{preExec}, commands...)
}
if postExec != "" {
commands = append(commands, postExec)
}
cmd := strings.Join(commands, " && ")
PrintVerboseInfo("PackageManager.GetFinalCmd", "returning cmd: "+cmd)
return cmd, nil
}
{
if p.CheckStatus() != nil {
return "", nil
}
addPkgs, err := p.GetAddPackages()
if err != nil {
if errors.Is(err, &os.PathError{}) {
addPkgs = []string{}
} else {
return "", err
}
}
removePkgs, err := p.GetRemovePackages()
if err != nil {
if errors.Is(err, &os.PathError{}) {
removePkgs = []string{}
} else {
return "", err
}
}
// GetPackages returns slices with one empty element if there are no packages
if len(addPkgs) == 1 && addPkgs[0] == "" {
addPkgs = []string{}
}
if len(removePkgs) == 1 && removePkgs[0] == "" {
removePkgs = []string{}
}
summary := ""
for _, pkg := range addPkgs {
summary += "+ " + pkg + "\n"
}
for _, pkg := range removePkgs {
summary += "- " + pkg + "\n"
}
return summary, nil
}
WriteSummaryToFile writes added and removed packages to the root specified by rootPath
added packages get the + prefix, while removed packages get the - prefix
{
summaryFilePath := filepath.Join(rootPath, summaryFileLocation)
summary, err := p.getSummary()
if err != nil {
return err
}
if summary == "" {
return nil
}
summaryFile, err := os.Create(summaryFilePath)
if err != nil {
return err
}
defer summaryFile.Close()
err = summaryFile.Chmod(0o644)
if err != nil {
return err
}
_, err = summaryFile.WriteString(summary)
if err != nil {
return err
}
return nil
}
GetCurrentlyInstalledPackages returns the currently installed packages
{
summaryFilePath := filepath.Join(rootPath, summaryFileLocation)
if _, err := os.Stat(summaryFilePath); errors.Is(err, os.ErrNotExist) {
return added, removed, nil
}
content, err := os.ReadFile(summaryFilePath)
if err != nil {
PrintVerboseErr("PackageManager.GetCurrentlyInstalledPackages", 1, err)
return added, removed, err
}
for _, line := range strings.Split(string(content), "\n") {
addedPkg, isAdded := strings.CutPrefix(line, "+ ")
if isAdded {
added = append(added, addedPkg)
continue
}
removedPkg, isRemoved := strings.CutPrefix(line, "- ")
if isRemoved {
removed = append(removed, removedPkg)
continue
}
PrintVerboseWarn("PackageManager.GetCurrentlyInstalledPackages", 1, "line "+line+" is not a valid package string")
}
return added, removed, nil
}
{
PrintVerboseInfo("PackageManager.ExistsInRepo", "running...")
ok, err := assertPkgMngApiSetUp()
if err != nil {
return err
}
if !ok {
return nil
}
url := strings.Replace(settings.Cnf.IPkgMngApi, "{packageName}", pkg, 1)
PrintVerboseInfo("PackageManager.ExistsInRepo", "checking if package exists in repo: "+url)
resp, err := http.Get(url)
if err != nil {
PrintVerboseErr("PackageManager.ExistsInRepo", 0, err)
return err
}
if resp.StatusCode != 200 {
PrintVerboseInfo("PackageManager.ExistsInRepo", "package does not exist in repo")
return fmt.Errorf("package does not exist in repo: %s", pkg)
}
PrintVerboseInfo("PackageManager.ExistsInRepo", "package exists in repo")
return nil
}
{
PrintVerboseInfo("PackageManager.ExistsOnSystem", "running...")
PrintVerboseInfo("PackageManager.ExistsInRepo", "checking if package "+pkg+" exists on system: ")
packageListFile, err := os.ReadFile("/var/lib/dpkg/status")
if err != nil {
PrintVerboseErr("PackageManager.ExistsOnSystem", 0, err)
return p.ExistsInRepo(pkg)
}
if !strings.Contains(string(packageListFile), "Package: "+pkg) {
PrintVerboseInfo("PackageManager.ExistsInRepo", "package does not exist on system")
return fmt.Errorf("package does not exist on system: %s", pkg)
}
PrintVerboseInfo("PackageManager.ExistsInRepo", "package exists on system")
return nil
}
AcceptUserAgreement sets the package manager status to enabled
{
PrintVerboseInfo("PackageManager.AcceptUserAgreement", "running...")
if p.Status != PKG_MNG_REQ_AGREEMENT {
PrintVerboseInfo("PackageManager.AcceptUserAgreement", "package manager is not in agreement mode")
return nil
}
err := os.WriteFile(
PkgManagerUserAgreementFile,
[]byte(time.Now().String()),
0o644,
)
if err != nil {
PrintVerboseErr("PackageManager.AcceptUserAgreement", 0, err)
return err
}
return nil
}
GetUserAgreementStatus returns if the user has accepted the package manager
agreement or not
{
PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "running...")
if p.Status != PKG_MNG_REQ_AGREEMENT {
PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "package manager is not in agreement mode")
return true
}
_, err := os.Stat(PkgManagerUserAgreementFile)
if err != nil {
PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "user has not accepted the agreement")
return false
}
PrintVerboseInfo("PackageManager.GetUserAgreementStatus", "user has accepted the agreement")
return true
}
CheckStatus checks if the package manager is enabled or not
{
PrintVerboseInfo("PackageManager.CheckStatus", "running...")
// Check if package manager is enabled
if p.Status == PKG_MNG_DISABLED {
PrintVerboseInfo("PackageManager.CheckStatus", "package manager is disabled")
return nil
}
// Check if user has accepted the package manager agreement
if p.Status == PKG_MNG_REQ_AGREEMENT {
if !p.GetUserAgreementStatus() {
PrintVerboseInfo("PackageManager.CheckStatus", "package manager agreement not accepted")
err := errors.New("package manager agreement not accepted")
return err
}
}
PrintVerboseInfo("PackageManager.CheckStatus", "package manager is enabled")
return nil
}
ABRootPkgManagerStatus represents the status of the package manager
in the ABRoot configuration file
int
NewPackageManager returns a new PackageManager struct
{
PrintVerboseInfo("PackageManager.NewPackageManager", "running...")
baseDir := PackagesBaseDir
if dryRun {
baseDir = DryRunPackagesBaseDir
}
err := os.MkdirAll(baseDir, 0o755)
if err != nil {
PrintVerboseErr("PackageManager.NewPackageManager", 0, err)
return nil, err
}
_, err = os.Stat(filepath.Join(baseDir, PackagesAddFile))
if err != nil {
err = os.WriteFile(
filepath.Join(baseDir, PackagesAddFile),
[]byte(""),
0o644,
)
if err != nil {
PrintVerboseErr("PackageManager.NewPackageManager", 1, err)
return nil, err
}
}
_, err = os.Stat(filepath.Join(baseDir, PackagesRemoveFile))
if err != nil {
err = os.WriteFile(
filepath.Join(baseDir, PackagesRemoveFile),
[]byte(""),
0o644,
)
if err != nil {
PrintVerboseErr("PackageManager.NewPackageManager", 2, err)
return nil, err
}
}
// here we convert settings.Cnf.IPkgMngStatus to an ABRootPkgManagerStatus
// for easier understanding in the code
var status ABRootPkgManagerStatus
switch settings.Cnf.IPkgMngStatus {
case PKG_MNG_REQ_AGREEMENT:
status = PKG_MNG_REQ_AGREEMENT
case PKG_MNG_ENABLED:
status = PKG_MNG_ENABLED
default:
status = PKG_MNG_DISABLED
}
return &PackageManager{dryRun, baseDir, status}, nil
}
assertPkgMngApiSetUp checks whether the repo API is properly configured.
If a configuration exists but is malformed, returns an error.
{
if settings.Cnf.IPkgMngApi == "" {
PrintVerboseInfo("PackageManager.assertPkgMngApiSetUp", "no API url set, will not check if package exists. This could lead to errors")
return false, nil
}
_, err := url.ParseRequestURI(settings.Cnf.IPkgMngApi)
if err != nil {
return false, fmt.Errorf("PackageManager.assertPkgMngApiSetUp: Value set as API url (%s) is not a valid URL", settings.Cnf.IPkgMngApi)
}
if !strings.Contains(settings.Cnf.IPkgMngApi, "{packageName}") {
return false, fmt.Errorf("PackageManager.assertPkgMngApiSetUp: API url does not contain {packageName} placeholder. ABRoot is probably misconfigured, please report the issue to the maintainers of the distribution")
}
PrintVerboseInfo("PackageManager.assertPkgMngApiSetUp", "Repo is set up properly")
return true, nil
}
GetRepoContentsForPkg retrieves package information from the repository API
{
PrintVerboseInfo("PackageManager.GetRepoContentsForPkg", "running...")
ok, err := assertPkgMngApiSetUp()
if err != nil {
return map[string]interface{}{}, err
}
if !ok {
return map[string]interface{}{}, errors.New("PackageManager.GetRepoContentsForPkg: no API url set, cannot query package information")
}
url := strings.Replace(settings.Cnf.IPkgMngApi, "{packageName}", pkg, 1)
PrintVerboseInfo("PackageManager.GetRepoContentsForPkg", "fetching package information in: "+url)
resp, err := http.Get(url)
if err != nil {
PrintVerboseErr("PackageManager.GetRepoContentsForPkg", 0, err)
return map[string]interface{}{}, err
}
contents, err := io.ReadAll(resp.Body)
if err != nil {
PrintVerboseErr("PackageManager.GetRepoContentsForPkg", 1, err)
return map[string]interface{}{}, err
}
pkgInfo := map[string]interface{}{}
err = json.Unmarshal(contents, &pkgInfo)
if err != nil {
PrintVerboseErr("PackageManager.GetRepoContentsForPkg", 2, err)
return map[string]interface{}{}, err
}
return pkgInfo, nil
}
{
inA := make(map[string]struct{})
inB := make(map[string]struct{})
for _, item := range a {
inA[item] = struct{}{}
}
for _, item := range b {
inB[item] = struct{}{}
}
for _, item := range a {
if _, found := inB[item]; !found {
onlyInA = append(onlyInA, item)
}
}
for _, item := range b {
if _, found := inA[item]; !found {
onlyInB = append(onlyInB, item)
}
}
return onlyInA, onlyInB
}
ABRootManager exposes methods to manage ABRoot partitions, this includes
getting the present and future partitions, the boot partition, the init
volume (when using LVM Thin-Provisioning), and the other partition. If you
need to operate on an ABRoot partition, you should use this struct, each
partition is a pointer to a Partition struct, which contains methods to
operate on the partition itself
GetPartitions gets the root partitions from the current device
{
PrintVerboseInfo("ABRootManager.GetRootPartitions", "running...")
diskM := NewDiskManager()
rootLabels := []string{settings.Cnf.PartLabelA, settings.Cnf.PartLabelB}
for _, label := range rootLabels {
partition, err := diskM.GetPartitionByLabel(label)
if err != nil {
PrintVerboseErr("ABRootManager.GetRootPartitions", 0, err)
return err
}
identifier, err := a.IdentifyPartition(partition)
if err != nil {
PrintVerboseErr("ABRootManager.GetRootPartitions", 1, err)
return err
}
isCurrent := a.IsCurrent(partition)
a.Partitions = append(a.Partitions, ABRootPartition{
Label: partition.Label,
IdentifiedAs: identifier,
Partition: partition,
MountPoint: partition.MountPoint,
MountOptions: partition.MountOptions,
Uuid: partition.Uuid,
FsType: partition.FsType,
Current: isCurrent,
})
}
partition, err := diskM.GetPartitionByLabel(settings.Cnf.PartLabelVar)
if err != nil {
PrintVerboseErr("ABRootManager.GetRootPartitions", 2, err)
return err
}
a.VarPartition = partition
PrintVerboseInfo("ABRootManager.GetRootPartitions", "successfully got root partitions")
return nil
}
IsCurrent checks if a partition is the current one
{
PrintVerboseInfo("ABRootManager.IsCurrent", "running...")
if partition.MountPoint == "/" {
PrintVerboseInfo("ABRootManager.IsCurrent", "partition is current")
return true
}
PrintVerboseInfo("ABRootManager.IsCurrent", "partition is not current")
return false
}
IdentifyPartition identifies a partition
{
PrintVerboseInfo("ABRootManager.IdentifyPartition", "running...")
if partition.Label == settings.Cnf.PartLabelA || partition.Label == settings.Cnf.PartLabelB {
if partition.MountPoint == "/" {
PrintVerboseInfo("ABRootManager.IdentifyPartition", "partition is present")
return "present", nil
}
PrintVerboseInfo("ABRootManager.IdentifyPartition", "partition is future")
return "future", nil
}
err = errors.New("partition is not managed by ABRoot")
PrintVerboseErr("ABRootManager.IdentifyPartition", 0, err)
return "", err
}
GetPresent gets the present partition
{
PrintVerboseInfo("ABRootManager.GetPresent", "running...")
for _, partition := range a.Partitions {
if partition.IdentifiedAs == "present" {
PrintVerboseInfo("ABRootManager.GetPresent", "successfully got present partition")
return partition, nil
}
}
err = errors.New("present partition not found")
PrintVerboseErr("ABRootManager.GetPresent", 0, err)
return ABRootPartition{}, err
}
GetFuture gets the future partition
{
PrintVerboseInfo("ABRootManager.GetFuture", "running...")
for _, partition := range a.Partitions {
if partition.IdentifiedAs == "future" {
PrintVerboseInfo("ABRootManager.GetFuture", "successfully got future partition")
return partition, nil
}
}
err = errors.New("future partition not found")
PrintVerboseErr("ABRootManager.GetFuture", 0, err)
return ABRootPartition{}, err
}
GetOther gets the other partition
{
PrintVerboseInfo("ABRootManager.GetOther", "running...")
present, err := a.GetPresent()
if err != nil {
PrintVerboseErr("ABRootManager.GetOther", 0, err)
return ABRootPartition{}, err
}
if present.Label == settings.Cnf.PartLabelA {
PrintVerboseInfo("ABRootManager.GetOther", "successfully got other partition")
return a.GetPartition(settings.Cnf.PartLabelB)
}
PrintVerboseInfo("ABRootManager.GetOther", "successfully got other partition")
return a.GetPartition(settings.Cnf.PartLabelA)
}
GetPartition gets a partition by label
{
PrintVerboseInfo("ABRootManager.GetPartition", "running...")
for _, partition := range a.Partitions {
if partition.Label == label {
PrintVerboseInfo("ABRootManager.GetPartition", "successfully got partition")
return partition, nil
}
}
err = errors.New("partition not found")
PrintVerboseErr("ABRootManager.GetPartition", 0, err)
return ABRootPartition{}, err
}
GetBoot gets the boot partition from the current device
{
PrintVerboseInfo("ABRootManager.GetBoot", "running...")
diskM := NewDiskManager()
part, err := diskM.GetPartitionByLabel(settings.Cnf.PartLabelBoot)
if err != nil {
err = errors.New("boot partition not found")
PrintVerboseErr("ABRootManager.GetBoot", 0, err)
return Partition{}, err
}
PrintVerboseInfo("ABRootManager.GetBoot", "successfully got boot partition")
return part, nil
}
GetInit gets the init volume when using LVM Thin-Provisioning
{
PrintVerboseInfo("ABRootManager.GetInit", "running...")
// Make sure Thin-Provisioning is properly configured
if !settings.Cnf.ThinProvisioning || settings.Cnf.ThinInitVolume == "" {
return Partition{}, errors.New("ABRootManager.GetInit: error: system is not configured for thin-provisioning")
}
diskM := NewDiskManager()
part, err := diskM.GetPartitionByLabel(settings.Cnf.ThinInitVolume)
if err != nil {
err = errors.New("init volume not found")
PrintVerboseErr("ABRootManager.GetInit", 0, err)
return Partition{}, err
}
PrintVerboseInfo("ABRootManager.GetInit", "successfully got init volume")
return part, nil
}
ABRootPartition represents a partition managed by ABRoot
NewABRootManager creates a new ABRootManager
{
PrintVerboseInfo("NewABRootManager", "running...")
a := &ABRootManager{}
a.GetPartitions()
return a
}
MergeDiff merges the diff lines between the first and second files into
the destination file. If any errors occur, they are returned.
{
PrintVerboseInfo("MergeDiff", "merging", firstFile, "+", secondFile, "->", destination)
// get the diff lines
diffLines, err := DiffFiles(firstFile, secondFile)
if err != nil {
PrintVerboseErr("MergeDiff", 0, err)
return err
}
// copy second file to destination to apply patch
secondFileContents, err := os.ReadFile(secondFile)
if err != nil {
PrintVerboseErr("MergeDiff", 1, err)
return err
}
err = os.WriteFile(destination, secondFileContents, 0644)
if err != nil {
PrintVerboseErr("MergeDiff", 2, err)
return err
}
// write the diff to the destination
err = WriteDiff(destination, diffLines)
if err != nil {
PrintVerboseErr("MergeDiff", 3, err)
return err
}
PrintVerboseInfo("MergeDiff", "merge completed")
return nil
}
DiffFiles returns the diff lines between source and dest files using the
diff command (assuming it is installed). If no diff is found, nil is
returned. If any errors occur, they are returned.
{
PrintVerboseInfo("DiffFiles", "diffing", sourceFile, "and", destFile)
cmd := exec.Command("diff", "-u", sourceFile, destFile)
var out bytes.Buffer
cmd.Stdout = &out
errCode := 0
err := cmd.Run()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
errCode = exitError.ExitCode()
}
}
// diff returns 1 if there are differences
if errCode == 1 {
PrintVerboseInfo("DiffFiles", "diff found")
return out.Bytes(), nil
}
PrintVerboseInfo("DiffFiles", "no diff found")
return nil, nil
}
WriteDiff applies the diff lines to the destination file using the patch
command (assuming it is installed). If any errors occur, they are returned.
{
PrintVerboseInfo("WriteDiff", "applying diff to", destFile)
if len(diffLines) == 0 {
PrintVerboseInfo("WriteDiff", "no changes to apply")
return nil // no changes to apply
}
cmd := exec.Command("patch", "-R", destFile)
cmd.Stdin = bytes.NewReader(diffLines)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
PrintVerboseErr("WriteDiff", 0, err)
return err
}
PrintVerboseInfo("WriteDiff", "diff applied")
return nil
}
Grub represents a grub instance, it exposes methods to generate a new grub
config compatible with ABRoot, and to check if the system is booted into
the present root or the future root
{
PrintVerboseInfo("Grub.IsBootedIntoPresentRoot", "running...")
a := NewABRootManager()
future, err := a.GetFuture()
if err != nil {
return false, err
}
if g.FutureRoot == "a" {
PrintVerboseInfo("Grub.IsBootedIntoPresentRoot", "done")
return future.Label == settings.Cnf.PartLabelA, nil
} else {
PrintVerboseInfo("Grub.IsBootedIntoPresentRoot", "done")
return future.Label == settings.Cnf.PartLabelB, nil
}
}
generateABGrubConf generates a new grub config with the given details
{
PrintVerboseInfo("generateABGrubConf", "generating grub config for ABRoot")
kargs, err := KargsRead()
if err != nil {
PrintVerboseErr("generateABGrubConf", 0, err)
return err
}
var grubPath, bootPrefix, systemRoot string
if settings.Cnf.ThinProvisioning {
grubPath = filepath.Join(rootPath, "boot", "init", rootLabel)
bootPrefix = "/" + rootLabel
diskM := NewDiskManager()
sysRootPart, err := diskM.GetPartitionByLabel(rootLabel)
if err != nil {
PrintVerboseErr("generateABGrubConf", 3, err)
return err
}
systemRoot = "/dev/mapper/" + sysRootPart.Device
} else {
grubPath = filepath.Join(rootPath, "boot", "grub")
bootPrefix = "/.system/boot"
systemRoot = "UUID=" + rootUuid
}
confPath := filepath.Join(grubPath, "abroot.cfg")
template := ` search --no-floppy --fs-uuid --set=root %s
linux %s/vmlinuz-%s root=%s %s
initrd %s/initrd.img-%s
`
err = os.MkdirAll(grubPath, 0755)
if err != nil {
PrintVerboseErr("generateABGrubConf", 2, err)
return err
}
abrootBootConfig := fmt.Sprintf(template, rootUuid, bootPrefix, kernelVersion, systemRoot, kargs, bootPrefix, kernelVersion)
generatedGrubConfigContents, err := os.ReadFile(filepath.Join(rootPath, generatedGrubConfigPath))
if err != nil {
PrintVerboseErr("generateABGrubConf", 3, "could not read grub config", err)
return err
}
generatedGrubConfig := string(generatedGrubConfigContents)
replacementString := "REPLACED_BY_ABROOT"
if !strings.Contains(generatedGrubConfig, replacementString) {
err := errors.New("could not find replacement string \"" + replacementString + "\", check /etc/grub.d configuration")
PrintVerboseErr("generateABGrubConf", 3.1, err)
return err
}
grubConfigWithBootEntry := strings.Replace(generatedGrubConfig, "REPLACED_BY_ABROOT", abrootBootConfig, 1)
err = os.WriteFile(confPath, []byte(grubConfigWithBootEntry), 0644)
if err != nil {
PrintVerboseErr("generateABGrubConf", 4, "could not read grub config", err)
return err
}
PrintVerboseInfo("generateABGrubConf", "done")
return nil
}
NewGrub creates a new Grub instance
{
PrintVerboseInfo("NewGrub", "running...")
grubPath := filepath.Join(bootPart.MountPoint, "grub")
confPath := filepath.Join(grubPath, "grub.cfg")
cfg, err := os.ReadFile(confPath)
if err != nil {
PrintVerboseErr("NewGrub", 0, err)
return nil, err
}
var presentRoot, futureRoot string
for _, entry := range strings.Split(string(cfg), "\n") {
if strings.Contains(entry, "abroot-a") {
if strings.Contains(entry, "Current State") {
presentRoot = "a"
} else if strings.Contains(entry, "Previous State") {
futureRoot = "a"
}
} else if strings.Contains(entry, "abroot-b") {
if strings.Contains(entry, "Current State") {
presentRoot = "b"
} else if strings.Contains(entry, "Previous State") {
futureRoot = "b"
}
}
}
if presentRoot == "" || futureRoot == "" {
err := errors.New("could not find root partitions")
PrintVerboseErr("NewGrub", 1, err)
return nil, err
}
PrintVerboseInfo("NewGrub", "done")
return &Grub{
PresentRoot: presentRoot,
FutureRoot: futureRoot,
}, nil
}
import "os"
import "os/exec"
import "path/filepath"
import "strings"
import "syscall"
import "os"
import "os/exec"
import "strings"
import "fmt"
import "log"
import "os"
import "path/filepath"
import "time"
import "github.com/vanilla-os/orchid/cmdr"
import "context"
import "errors"
import "fmt"
import "os"
import "path/filepath"
import "strings"
import "syscall"
import "time"
import "github.com/containers/buildah"
import "github.com/containers/image/v5/types"
import "github.com/containers/storage"
import "github.com/dustin/go-humanize"
humanize
import "github.com/pterm/pterm"
import "github.com/vanilla-os/abroot/settings"
import "github.com/vanilla-os/prometheus"
import "encoding/json"
import "fmt"
import "io"
import "net/http"
import "strings"
import "github.com/vanilla-os/abroot/extras/dpkg"
import "github.com/vanilla-os/abroot/settings"
import "github.com/vanilla-os/differ/diff"
import "os"
import "golang.org/x/sys/unix"
import "errors"
import "fmt"
import "net"
import "os"
import "os/exec"
import "runtime"
import "time"
import "fmt"
import "os/exec"
import "strings"
import "github.com/shirou/gopsutil/cpu"
import "github.com/shirou/gopsutil/mem"
import "errors"
import "fmt"
import "os"
import "os/exec"
import "path/filepath"
import "strconv"
import "strings"
import "github.com/google/uuid"
import "github.com/linux-immutability-tools/EtcBuilder/cmd"
EtcBuilder
import "github.com/vanilla-os/abroot/settings"
import "github.com/vanilla-os/sdk/pkg/v1/goodies"
import "fmt"
import "os"
import "os/exec"
import "github.com/spf13/viper"
import "github.com/vanilla-os/abroot/settings"
import "encoding/json"
import "errors"
import "fmt"
import "io"
import "net/http"
import "strings"
import "github.com/vanilla-os/abroot/settings"
import "bufio"
import "fmt"
import "os"
import "os/exec"
import "strconv"
import "strings"
import "github.com/vanilla-os/orchid/cmdr"
import "fmt"
import "io"
import "io/fs"
import "os"
import "os/exec"
import "syscall"
import "encoding/json"
import "errors"
import "fmt"
import "os"
import "os/exec"
import "strings"
import "syscall"
import "errors"
import "path/filepath"
import "github.com/hashicorp/go-version"
import "fmt"
import "os"
import "encoding/json"
import "fmt"
import "os"
import "path/filepath"
import "time"
import "errors"
import "os"
import "os/exec"
import "path/filepath"
import "encoding/json"
import "errors"
import "fmt"
import "io"
import "net/http"
import "net/url"
import "os"
import "path/filepath"
import "strings"
import "time"
import "github.com/vanilla-os/abroot/settings"
import "errors"
import "github.com/vanilla-os/abroot/settings"
import "bytes"
import "os"
import "os/exec"
import "errors"
import "fmt"
import "os"
import "path/filepath"
import "strings"
import "github.com/vanilla-os/abroot/settings"