core
packageAPI reference for the core
package.
Imports
(40)encoding/json
STD
errors
STD
fmt
STD
os
STD
os/exec
STD
strings
STD
syscall
STD
path/filepath
STD
time
PKG
github.com/opencontainers/go-digest
STD
context
PKG
github.com/containers/buildah
PKG
github.com/dustin/go-humanize
PKG
github.com/pterm/pterm
INT
github.com/vanilla-os/abroot/settings
PKG
github.com/vanilla-os/prometheus
PKG
go.podman.io/image/v5/types
PKG
go.podman.io/storage
STD
log
PKG
github.com/vanilla-os/orchid/cmdr
STD
io
STD
net/http
INT
github.com/vanilla-os/abroot/extras/dpkg
PKG
github.com/vanilla-os/differ/diff
STD
bufio
STD
strconv
PKG
github.com/shirou/gopsutil/cpu
PKG
github.com/shirou/gopsutil/mem
STD
net/url
PKG
golang.org/x/sys/unix
STD
bytes
PKG
github.com/hashicorp/go-version
PKG
github.com/google/uuid
PKG
github.com/linux-immutability-tools/EtcBuilder/cmd
PKG
github.com/vanilla-os/sdk/pkg/v1/goodies
STD
io/fs
STD
slices
STD
net
STD
runtime
PKG
github.com/spf13/viper
DiskManager
DiskManager exposes functions to interact with the system’s disks
and partitions (e.g. mount, unmount, get partitions, etc.)
type DiskManager struct
Methods
GetPartitionByLabel finds a partition by searching for its label. If no partition can be found with the given label, returns error.
Parameters
Returns
func (*DiskManager) GetPartitionByLabel(label string) (Partition, 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
Parameters
Returns
func (*DiskManager) GetPartitions(device string) ([]Partition, error)
{
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
Partition represents either a standard partition or a device-mapper
partition, such as an LVM volume
type Partition struct
Methods
Mount mounts a partition to a directory, returning an error if any occurs
Parameters
Returns
func (*Partition) Mount(destination string) error
{
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
Returns
func (*Partition) Unmount() error
{
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
Returns
func (*Partition) IsDevMapper() bool
{
return p.Parent != nil
}
IsEncrypted returns whether the partition is encrypted
Returns
func (*Partition) IsEncrypted() bool
{
return strings.HasPrefix(p.FsType, "crypto_")
}
Fields
| Name | Type | Description |
|---|---|---|
| Label | string | |
| MountPoint | string | |
| MountOptions | string | |
| Uuid | string | |
| FsType | string | |
| Device | string | |
| Parent | *Partition |
Children
The children a block device or partition may have
type Children struct
Fields
| Name | Type | Description |
|---|---|---|
| MountPoint | string | json:"mountpoint" |
| FsType | string | json:"fstype" |
| Label | string | json:"label" |
| Uuid | string | json:"uuid" |
| LogicalName | string | json:"name" |
| Size | string | json:"size" |
| MountOptions | string | json:"mountopts" |
| Children | []Children | json:"children" |
NewDiskManager
NewDiskManager creates and returns a pointer to a new DiskManager instance
from which you can interact with the system’s disks and partitions
Returns
func NewDiskManager() *DiskManager
{
return &DiskManager{}
}
iterChildren
iterChildren iterates through the children of a device or partition
recursively
func iterChildren(childs *[]Children, result *[]Partition)
{
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]
}
}
}
}
ABImage
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
type ABImage struct
Methods
WriteTo writes the json to a destination path, if the suffix is not empty, it will be appended to the filename
Parameters
Returns
func (*ABImage) WriteTo(dest string) error
{
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
}
}
imagePath := filepath.Join(dest, "abimage.abr")
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
}
Fields
| Name | Type | Description |
|---|---|---|
| Digest | digest.Digest | json:"digest" |
| Timestamp | time.Time | json:"timestamp" |
| Image | string | json:"image" |
NewABImage
NewABImage creates a new ABImage instance and returns a pointer to it,
if the digest is empty, it returns an error
Parameters
Returns
func NewABImage(digest digest.Digest, image string) (*ABImage, error)
{
if digest == "" {
return nil, fmt.Errorf("NewABImage: digest is empty")
}
return &ABImage{
Digest: digest,
Timestamp: time.Now(),
Image: image,
}, nil
}
NewABImageFromRoot
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.
Returns
func NewABImageFromRoot() (*ABImage, error)
{
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
}
NotEnoughSpaceError
type NotEnoughSpaceError struct
Methods
Returns
func (*NotEnoughSpaceError) Error() string
{
return "not enough space in disk"
}
padString
Parameters
Returns
func padString(str string, size int) string
{
if len(str) < size {
return str + strings.Repeat(" ", size-len(str))
} else {
return str
}
}
OciPullImage
OciExportRootFs pulls an image from a registry
Parameters
Returns
func OciPullImage(imageName string) error
{
pt, err := prometheus.NewPrometheus(
"/var/lib/abroot/storage",
"overlay",
settings.Cnf.MaxParallelDownloads,
)
if err != nil {
PrintVerboseErr("OciPullImage", 0, err)
return err
}
if strings.HasPrefix(imageName, "localhost/") {
return nil
}
err = pullImageWithProgressbar(pt, "remoteimage", imageName)
if err != nil {
PrintVerboseErr("OciPullImage", 1, err)
return err
}
return nil
}
OciExportRootFs
OciExportRootFs generates a rootfs from an image recipe file
Parameters
Returns
func OciExportRootFs(buildImageName string, imageRecipe *ImageRecipe, transDir string, dest string) error
{
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
}
// build image
imageBuild, err := pt.BuildContainerFile(imageRecipePath, buildImageName)
if err != nil {
PrintVerboseErr("OciExportRootFs", 7, err)
return err
}
// This is safe because BuildContainerFile layers on top of the base image
// So this won't delete the actual layers, only the image reference
_, _ = pt.Store.DeleteImage(imageRecipe.From, true)
// 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", "--delete-before", "--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
}
checkImageSize
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
Parameters
Returns
func checkImageSize(imageDir string, filesystemMount string) error
{
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
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
Parameters
Returns
func pullImageWithProgressbar(pt *prometheus.Prometheus, name string, imageName string) error
{
PrintVerboseInfo("pullImageWithProgressbar", "running...")
progressCh := make(chan types.ProgressProperties)
manifestCh := make(chan prometheus.OciManifest)
errorCh := make(chan error)
defer close(progressCh)
defer close(manifestCh)
defer close(errorCh)
err := pt.PullImageAsync(imageName, name, progressCh, manifestCh, errorCh)
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
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
Parameters
Returns
func FindImageWithLabel(key, value string) (string, error)
{
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
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
Parameters
Returns
func RetrieveImageForRoot(root string) (string, error)
{
PrintVerboseInfo("RetrieveImageForRoot", "running...")
image, err := FindImageWithLabel("ABRoot.root", root)
if err != nil {
PrintVerboseErr("RetrieveImageForRoot", 0, err)
return "", err
}
return image, nil
}
DeleteAllButLatestImage
DeleteAllButLatestImage deletes all images
Returns
func DeleteAllButLatestImage() error
{
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
}
HasUpdate
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
Parameters
Returns
func HasUpdate(oldDigest digest.Digest) (digest.Digest, bool, error)
{
PrintVerboseInfo("OCI.HasUpdate", "Checking for updates ...")
pt, err := prometheus.NewPrometheus(
"/var/lib/abroot/storage",
"overlay",
settings.Cnf.MaxParallelDownloads,
)
if err != nil {
PrintVerboseErr("OCI.HasUpdate", 0, err)
return "", false, err
}
imageName := fmt.Sprintf("%s/%s:%s", settings.Cnf.Registry, settings.Cnf.Name, settings.Cnf.Tag)
PrintVerboseInfo("OCI.HasUpdate", "checking image: ", imageName)
_, newDigest, err := pt.PullManifestOnly(imageName)
if err != nil {
PrintVerboseErr("OCI.HasUpdate", 1, err)
return "", false, err
}
if newDigest == oldDigest {
PrintVerboseInfo("OCI.HasUpdate", "no update available")
return "", false, nil
}
PrintVerboseInfo("OCI.HasUpdate", "update available. Old digest: ", oldDigest, ", new digest: ", newDigest)
return newDigest, true, nil
}
Chroot
Chroot represents a chroot instance, which can be used to run commands
inside a chroot environment
type Chroot struct
Methods
Close unmounts all the bind mounts and closes the chroot environment
Returns
func (*Chroot) Close() error
{
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.
Parameters
Returns
func (*Chroot) Execute(cmd string) error
{
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
Parameters
Returns
func (*Chroot) ExecuteCmds(cmds []string) 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
}
Fields
| Name | Type | Description |
|---|---|---|
| root | string | |
| rootUuid | string | |
| rootDevice | string | |
| etcMounted | bool |
NewChroot
NewChroot creates a new chroot environment from the given root path and
returns its Chroot instance or an error if something went wrong
Parameters
Returns
func NewChroot(root string, rootUuid string, rootDevice string, mountUserEtc bool, userEtcPath string) (*Chroot, error)
{
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
}
RepairRootIntegrity
Parameters
Returns
func RepairRootIntegrity(rootPath string) (err error)
{
fixupOlderSystems(rootPath)
err = repairLinks(rootPath)
if err != nil {
return err
}
err = repairPaths(rootPath)
if err != nil {
return err
}
return nil
}
repairLinks
Parameters
Returns
func repairLinks(rootPath string) (err error)
{
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
}
repairLink
Parameters
Returns
func repairLink(sourceAbs, targetAbs string) (err error)
{
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
}
dest, err := os.Readlink(target)
if err != nil || dest != source {
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
}
repairPaths
Parameters
Returns
func repairPaths(rootPath string) (err error)
{
for _, path := range pathsToRepair {
err = repairPath(filepath.Join(rootPath, path))
if err != nil {
return err
}
}
return nil
}
repairPath
Parameters
Returns
func repairPath(path string) (err error)
{
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
}
fixupOlderSystems
this is here to keep compatibility with older systems
Parameters
func fixupOlderSystems(rootPath string)
{
paths := []string{
"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
}
}
}
}
init
init initializes the log file and sets up logging
func init()
{
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
IsVerbose checks if verbose mode is enabled
Returns
func IsVerbose() bool
{
flag := cmdr.FlagValBool("verbose")
_, arg := os.LookupEnv("ABROOT_VERBOSE")
return flag || arg
}
formatMessage
formatMessage formats log messages based on prefix, level, and depth
Parameters
Returns
func formatMessage(prefix, level string, depth float32, args ...interface{}) string
{
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
printFormattedMessage prints formatted log messages to Stdout
Parameters
func printFormattedMessage(formattedMsg string)
{
printLog.Printf("%s\n", formattedMsg)
}
logToFileIfEnabled
logToFileIfEnabled logs messages to the file if logging is enabled
Parameters
func logToFileIfEnabled(formattedMsg string)
{
if logFile != nil {
LogToFile(formattedMsg)
}
}
PrintVerboseNoLog
PrintVerboseNoLog prints verbose messages without logging to the file
Parameters
func PrintVerboseNoLog(prefix, level string, depth float32, args ...interface{})
{
if IsVerbose() {
formattedMsg := formatMessage(prefix, level, depth, args...)
printFormattedMessage(formattedMsg)
}
}
PrintVerbose
PrintVerbose prints verbose messages and logs to the file if enabled
Parameters
func PrintVerbose(prefix, level string, depth float32, args ...interface{})
{
PrintVerboseNoLog(prefix, level, depth, args...)
logToFileIfEnabled(formatMessage(prefix, level, depth, args...))
}
PrintVerboseSimpleNoLog
PrintVerboseSimpleNoLog prints simple verbose messages without logging to the file
Parameters
func PrintVerboseSimpleNoLog(args ...interface{})
{
PrintVerboseNoLog("", "", -1, args...)
}
PrintVerboseSimple
PrintVerboseSimple prints simple verbose messages and logs to the file if enabled
Parameters
func PrintVerboseSimple(args ...interface{})
{
PrintVerbose("", "", -1, args...)
}
PrintVerboseErrNoLog
PrintVerboseErrNoLog prints verbose error messages without logging to the file
Parameters
func PrintVerboseErrNoLog(prefix string, depth float32, args ...interface{})
{
PrintVerboseNoLog(prefix, "err", depth, args...)
}
PrintVerboseErr
PrintVerboseErr prints verbose error messages and logs to the file if enabled
Parameters
func PrintVerboseErr(prefix string, depth float32, args ...interface{})
{
PrintVerbose(prefix, "err", depth, args...)
}
PrintVerboseWarnNoLog
PrintVerboseWarnNoLog prints verbose warning messages without logging to the file
Parameters
func PrintVerboseWarnNoLog(prefix string, depth float32, args ...interface{})
{
PrintVerboseNoLog(prefix, "warn", depth, args...)
}
PrintVerboseWarn
PrintVerboseWarn prints verbose warning messages and logs to the file if enabled
Parameters
func PrintVerboseWarn(prefix string, depth float32, args ...interface{})
{
PrintVerbose(prefix, "warn", depth, args...)
}
PrintVerboseInfoNoLog
PrintVerboseInfoNoLog prints verbose info messages without logging to the file
Parameters
func PrintVerboseInfoNoLog(prefix string, args ...interface{})
{
PrintVerboseNoLog(prefix, "info", -1, args...)
}
PrintVerboseInfo
PrintVerboseInfo prints verbose info messages and logs to the file if enabled
Parameters
func PrintVerboseInfo(prefix string, args ...interface{})
{
PrintVerbose(prefix, "info", -1, args...)
}
LogToFile
LogToFile writes messages to the log file
Parameters
Returns
func LogToFile(msg string, args ...interface{}) error
{
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
GetLogFile returns the log file handle
Returns
func GetLogFile() *os.File
{
return logFile
}
BaseImagePackageDiff
BaseImagePackageDiff retrieves the added, removed, upgraded and downgraded
base packages (the ones bundled with the image).
Parameters
Returns
func BaseImagePackageDiff(currentDigest, newDigest digest.Digest) (added, upgraded, downgraded, removed []diff.PackageDiff, err error)
{
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
OverlayPackageDiff retrieves the added, removed, upgraded and downgraded
overlay packages (the ones added manually via abroot pkg add).
Returns
func OverlayPackageDiff() (added, upgraded, downgraded, removed []diff.PackageDiff, err error)
{
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
}
ABRootManager
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
type ABRootManager struct
Methods
GetPartitions gets the root partitions from the current device
Returns
func (*ABRootManager) GetPartitions() error
{
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
Parameters
Returns
func (*ABRootManager) IsCurrent(partition Partition) bool
{
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
Parameters
Returns
func (*ABRootManager) IdentifyPartition(partition Partition) (identifiedAs string, err error)
{
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
Returns
func (*ABRootManager) GetPresent() (partition ABRootPartition, err error)
{
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
Returns
func (*ABRootManager) GetFuture() (partition ABRootPartition, err error)
{
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
Returns
func (*ABRootManager) GetOther() (partition ABRootPartition, err error)
{
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
Parameters
Returns
func (*ABRootManager) GetPartition(label string) (partition ABRootPartition, err error)
{
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
Returns
func (*ABRootManager) GetBoot() (partition Partition, err error)
{
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
Returns
func (*ABRootManager) GetInit() (partition Partition, err error)
{
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
}
Fields
| Name | Type | Description |
|---|---|---|
| Partitions | []ABRootPartition | |
| VarPartition | Partition |
Uses
ABRootPartition
ABRootPartition represents a partition managed by ABRoot
type ABRootPartition struct
Fields
| Name | Type | Description |
|---|---|---|
| Label | string | |
| IdentifiedAs | string | |
| Partition | Partition | |
| MountPoint | string | |
| MountOptions | string | |
| Uuid | string | |
| FsType | string | |
| Current | bool |
Uses
NewABRootManager
NewABRootManager creates a new ABRootManager
Returns
func NewABRootManager() *ABRootManager
{
PrintVerboseInfo("NewABRootManager", "running...")
a := &ABRootManager{}
a.GetPartitions()
return a
}
rsyncCmd
rsyncCmd executes the rsync command with the requested options.
If silent is true, rsync progress will not appear in stdout.
Parameters
Returns
func rsyncCmd(src, dst string, opts []string, silent bool) error
{
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
rsyncDryRun executes the rsync command with the –dry-run option.
Parameters
Returns
func rsyncDryRun(src, dst string, excluded []string) error
{
opts := []string{"--dry-run"}
if len(excluded) > 0 {
for _, exclude := range excluded {
opts = append(opts, "--exclude="+exclude)
}
}
return rsyncCmd(src, dst, opts, false)
}
AtomicRsync
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.
Parameters
Returns
func AtomicRsync(src, dst string, transitionalPath string, finalPath string, excluded []string, keepUnwanted bool) error
{
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)
}
PCSpecs
type PCSpecs struct
Fields
| Name | Type | Description |
|---|---|---|
| CPU | string | |
| GPU | []string | |
| Memory | string |
GPUInfo
type GPUInfo struct
Fields
| Name | Type | Description |
|---|---|---|
| Address | string | |
| Description | string |
getCPUInfo
Returns
func getCPUInfo() (string, error)
{
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
}
parseGPUInfo
Parameters
Returns
func parseGPUInfo(line string) (string, error)
{
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
}
getGPUInfo
Returns
func getGPUInfo() ([]string, error)
{
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
}
getMemoryInfo
Returns
func getMemoryInfo() (string, error)
{
vm, err := mem.VirtualMemory()
if err != nil {
return "", err
}
return fmt.Sprintf("%d MB", vm.Total/1024/1024), nil
}
GetPCSpecs
Returns
func GetPCSpecs() PCSpecs
{
cpu, _ := getCPUInfo()
gpu, _ := getGPUInfo()
memory, _ := getMemoryInfo()
return PCSpecs{
CPU: cpu,
GPU: gpu,
Memory: memory,
}
}
Uses
init
func init()
{
if os.Getenv("ABROOT_KARGS_PATH") != "" {
KargsPath = os.Getenv("ABROOT_KARGS_PATH")
}
}
kargsCreateIfMissing
kargsCreateIfMissing creates the kargs file if it doesn’t exist
Returns
func kargsCreateIfMissing() error
{
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
KargsWrite makes a backup of the current kargs file and then
writes the new content to it
Parameters
Returns
func KargsWrite(content string) error
{
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
KargsBackup makes a backup of the current kargs file
Returns
func KargsBackup() error
{
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
KargsRead reads the content of the kargs file
Returns
func KargsRead() (string, error)
{
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
KargsFormat formats the contents of the kargs file, ensuring that
there are no duplicate entries, multiple spaces or trailing newline
Parameters
Returns
func KargsFormat(content string) (string, error)
{
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
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.
Returns
func KargsEdit() (bool, error)
{
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
}
PackageManager
PackageManager struct
type PackageManager struct
Methods
Add adds a package to the packages.add file
Parameters
Returns
func (*PackageManager) Add(pkg string) error
{
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
Parameters
Returns
func (*PackageManager) Remove(pkg string) error
{
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
Returns
func (*PackageManager) GetAddPackages() ([]string, error)
{
PrintVerboseInfo("PackageManager.GetAddPackages", "running...")
return p.getPackages(PackagesAddFile)
}
GetRemovePackages returns the packages in the packages.remove file
Returns
func (*PackageManager) GetRemovePackages() ([]string, error)
{
PrintVerboseInfo("PackageManager.GetRemovePackages", "running...")
return p.getPackages(PackagesRemoveFile)
}
GetUnstagedPackages returns the package changes that are yet to be applied
Parameters
Returns
func (*PackageManager) GetUnstagedPackages(rootPath string) (toBeAdded, toBeRemoved []string, err error)
{
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
Parameters
Returns
func (*PackageManager) GetAddPackagesString(sep string) (string, error)
{
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
Parameters
Returns
func (*PackageManager) GetRemovePackagesString(sep string) (string, error)
{
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
}
Parameters
Returns
func (*PackageManager) getPackages(file string) ([]string, error)
{
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
}
Parameters
Returns
func (*PackageManager) writeAddPackages(pkgs []string) error
{
PrintVerboseInfo("PackageManager.writeAddPackages", "running...")
return p.writePackages(PackagesAddFile, pkgs)
}
Parameters
Returns
func (*PackageManager) writeRemovePackages(pkgs []string) error
{
PrintVerboseInfo("PackageManager.writeRemovePackages", "running...")
return p.writePackages(PackagesRemoveFile, pkgs)
}
Parameters
Returns
func (*PackageManager) writePackages(file string, pkgs []string) error
{
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
}
Returns
func (*PackageManager) createPackageCommands() (commands []string, err error)
{
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
}
Returns
func (*PackageManager) GetFinalCmd() (string, error)
{
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
}
Returns
func (*PackageManager) getSummary() (string, error)
{
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
Parameters
Returns
func (*PackageManager) WriteSummaryToRoot(rootPath string) error
{
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
Parameters
Returns
func (*PackageManager) GetCurrentlyInstalledPackages(rootPath string) (added []string, removed []string, err error)
{
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
}
Parameters
Returns
func (*PackageManager) ExistsInRepo(pkg string) error
{
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
}
Parameters
Returns
func (*PackageManager) ExistsOnSystem(pkg string) error
{
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
Returns
func (*PackageManager) AcceptUserAgreement() error
{
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
Returns
func (*PackageManager) GetUserAgreementStatus() bool
{
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
Returns
func (*PackageManager) CheckStatus() error
{
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
}
Fields
| Name | Type | Description |
|---|---|---|
| dryRun | bool | |
| baseDir | string | |
| Status | ABRootPkgManagerStatus |
ABRootPkgManagerStatus
ABRootPkgManagerStatus represents the status of the package manager
in the ABRoot configuration file
type ABRootPkgManagerStatus int
NewPackageManager
NewPackageManager returns a new PackageManager struct
Parameters
Returns
func NewPackageManager(dryRun bool) (*PackageManager, error)
{
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
assertPkgMngApiSetUp checks whether the repo API is properly configured.
If a configuration exists but is malformed, returns an error.
Returns
func assertPkgMngApiSetUp() (bool, 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
GetRepoContentsForPkg retrieves package information from the repository API
Parameters
Returns
func GetRepoContentsForPkg(pkg string) (map[string]interface{}, error)
{
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
}
sliceDifference
Parameters
Returns
func sliceDifference(a, b []string) (onlyInA, onlyInB []string)
{
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
}
AtomicSwap
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
Parameters
Returns
func AtomicSwap(src, dst string) error
{
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
}
MergeDiff
MergeDiff merges the diff lines between the first and second files into
the destination file. If any errors occur, they are returned.
Parameters
Returns
func MergeDiff(firstFile, secondFile, destination string) error
{
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
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.
Parameters
Returns
func DiffFiles(sourceFile, destFile string) ([]byte, error)
{
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
WriteDiff applies the diff lines to the destination file using the patch
command (assuming it is installed). If any errors occur, they are returned.
Parameters
Returns
func WriteDiff(destFile string, diffLines []byte) error
{
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
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
type Grub struct
Methods
Returns
func (*Grub) IsBootedIntoPresentRoot() (bool, error)
{
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
}
}
Fields
| Name | Type | Description |
|---|---|---|
| PresentRoot | string | |
| FutureRoot | string |
generateABGrubConf
generateABGrubConf generates a new grub config with the given details
Parameters
Returns
func generateABGrubConf(kernelVersion string, rootPath string, rootUuid string, rootLabel string, generatedGrubConfigPath string) error
{
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
NewGrub creates a new Grub instance
func NewGrub(bootPart Partition) (*Grub, error)
{
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
}
Uses
ImageRecipe
An ImageRecipe represents a Dockerfile/Containerfile-like recipe
type ImageRecipe struct
Methods
Write writes a ImageRecipe to the given path, returning an error if any
Parameters
Returns
func (*ImageRecipe) Write(path string) error
{
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
}
Fields
| Name | Type | Description |
|---|---|---|
| From | string | |
| Labels | map[string]string | |
| Args | map[string]string | |
| Content | string |
NewImageRecipe
NewImageRecipe creates a new ImageRecipe instance and returns a pointer to it
Parameters
Returns
func NewImageRecipe(image string, labels map[string]string, args map[string]string, content string) *ImageRecipe
{
PrintVerboseInfo("NewImageRecipe", "running...")
return &ImageRecipe{
From: image,
Labels: labels,
Args: args,
Content: content,
}
}
getKernelVersion
getKernelVersion returns the latest kernel version available in the root
Parameters
Returns
func getKernelVersion(bootPath string) string
{
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 ""
}
ABSystem
An ABSystem allows to perform system operations such as upgrades,
package changes and rollback on an ABRoot-compliant system.
type ABSystem struct
Methods
CheckAll performs all checks from the Checks struct
Returns
func (*ABSystem) CheckAll() error
{
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
Returns
func (*ABSystem) CheckUpdate() (digest.Digest, bool, error)
{
PrintVerboseInfo("ABSystem.CheckUpdate", "running...")
return HasUpdate(s.CurImage.Digest)
}
Parameters
Returns
func (*ABSystem) Rebase(name string, dryRun bool) error
{
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 err != nil {
return err
}
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.
Parameters
Returns
func (*ABSystem) RunOperation(operation ABSystemOperation, deleteBeforeCopy bool, dryRun bool) error
{
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("ABSystem.RunOperation", 0, "reboot required")
return errors.New("another operation finished successfully, a reboot is required")
}
err := s.LockOperation()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 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("ABSystem.RunOperation", 0.2, err)
return err
}
// Stage 1: Check if there is an update available
// ------------------------------------------------
PrintVerboseSimple("[Stage 1] -------- ABSystemRunOperation")
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystem.RunOperation", 2, err)
return err
}
var imageDigest digest.Digest
if operation != INITRAMFS {
var res bool
imageDigest, res, err = s.CheckUpdate()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 1, err)
return err
}
if !res {
if operation != FORCE_UPGRADE && operation != APPLY {
PrintVerboseErr("ABSystem.RunOperation", 1.1, err)
return ErrNoUpdate
}
imageDigest = s.CurImage.Digest
if operation == FORCE_UPGRADE {
PrintVerboseWarn("ABSystem.RunOperation", 1.2, "No update available but --force is set. Proceeding...")
}
}
} else {
imageDigest = s.CurImage.Digest
}
// Stage 2: Get the present root, future root and boot partitions,
// mount future to /part-future and clean up
// old /part-future/new directory (it is
// possible that last transaction was interrupted
// before the clean up was done).
// ------------------------------------------------
PrintVerboseSimple("[Stage 2] -------- ABSystemRunOperation")
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystem.RunOperation", 2, err)
return err
}
partPresent, err := s.RootM.GetPresent()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 2.05, 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()
futureRoot := "/part-future"
err = partFuture.Partition.Mount(futureRoot)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 2.3, err)
return err
}
cq.Add(func(args ...interface{}) error {
return partFuture.Partition.Unmount()
}, nil, 90, &goodies.NoErrorHandler{}, false)
// Stage 3: Make a imageRecipe with user packages
// then download the image
// ------------------------------------------------
PrintVerboseSimple("[Stage 3] -------- ABSystemRunOperation")
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystem.RunOperation", 2, err)
return err
}
// Stage 3.1: Delete old images
if !dryRun {
err = DeleteAllButLatestImage()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 3.1, err)
return err
}
}
labels := map[string]string{
"maintainer": "'Generated by ABRoot'",
"ABRoot.root": partFuture.Label,
}
args := map[string]string{}
pkgM, err := NewPackageManager(false)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 3.2, err)
return err
}
pkgsFinal, err := pkgM.GetFinalCmd()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 3.3, err)
}
if pkgsFinal == "" {
pkgsFinal = "true"
}
content := `RUN ` + pkgsFinal
var imageName string
switch operation {
case INITRAMFS:
imageName, err = RetrieveImageForRoot(partPresent.Label)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 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.String()
labels["ABRoot.BaseImageDigest"] = imageDigest.String()
}
imageRecipe := NewImageRecipe(
imageName,
labels,
args,
content,
)
// Stage 3.2: Download image
if !dryRun {
err = OciPullImage(imageName)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 3.5, err)
return err
}
}
// Stage 4: Extract the rootfs
// ------------------------------------------------
PrintVerboseSimple("[Stage 4] -------- ABSystemRunOperation")
if UserStopRequested() {
err = ErrUserStopped
PrintVerboseErr("ABSystem.RunOperation", 2, err)
return err
}
err = s.createFinalizingFile()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 5.3, err)
return err
}
if deleteBeforeCopy || os.Getenv("ABROOT_DELETE_BEFORE_COPY") != "" {
PrintVerboseInfo("ABSystemRunOperation", "Deleting future system, this will render the future root temporarily unavailable")
if !dryRun {
err := ClearDirectory(partFuture.Partition.MountPoint, nil)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 4, err)
return err
}
}
}
abrootTrans := filepath.Join(futureRoot, "abroot-trans")
if !dryRun {
err = OciExportRootFs(
"abroot-"+uuid.New().String(),
imageRecipe,
abrootTrans,
futureRoot,
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 4.2, err)
return err
}
}
// Stage 4.1: Delete old images
if !dryRun {
err = DeleteAllButLatestImage()
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 3.1, err)
return err
}
}
// Stage 4.2: Repair root integrity
if !dryRun {
err = RepairRootIntegrity(futureRoot)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 2.4, err)
return err
}
}
// Stage 5: Write new abimage.abr and config to future/
// ------------------------------------------------
PrintVerboseSimple("[Stage 5] -------- ABSystemRunOperation")
abimage, err := NewABImage(imageDigest, settings.GetFullImageNameWithTag())
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 5.1, err)
return err
}
if !dryRun {
err = abimage.WriteTo(futureRoot)
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
}
if !dryRun {
err = settings.WriteConfigToFile(filepath.Join(futureRoot, "/usr/share/abroot/abroot.json"))
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 5.25, err)
return err
}
}
if !dryRun {
err = pkgM.WriteSummaryToRoot(futureRoot)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 5.26, err)
return err
}
}
// Stage 6: Update the bootloader
// ------------------------------------------------
PrintVerboseSimple("[Stage 6] -------- ABSystemRunOperation")
generatedGrubConfigPath := "/boot/grub/grub.cfg"
if !dryRun {
chroot, err := NewChroot(
futureRoot,
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
}
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
}
}
var newKernelVer string
if !dryRun {
newKernelVer = getKernelVersion(filepath.Join(futureRoot, "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(futureRoot, "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)
if !dryRun {
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 = MoveFile(
filepath.Join(futureRoot, "boot", "vmlinuz-"+newKernelVer),
filepath.Join(futureInitDir, "vmlinuz-"+newKernelVer),
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.5, err)
return err
}
err = MoveFile(
filepath.Join(futureRoot, "boot", "initrd.img-"+newKernelVer),
filepath.Join(futureInitDir, "initrd.img-"+newKernelVer),
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.6, err)
return err
}
err = MoveFile(
filepath.Join(futureRoot, "boot", "config-"+newKernelVer),
filepath.Join(futureInitDir, "config-"+newKernelVer),
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.7, err)
return err
}
err = MoveFile(
filepath.Join(futureRoot, "boot", "System.map-"+newKernelVer),
filepath.Join(futureInitDir, "System.map-"+newKernelVer),
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.8, err)
return err
}
}
rootUuid = initPartition.Uuid
} else {
rootUuid = partFuture.Partition.Uuid
}
if !dryRun {
err = generateABGrubConf(
newKernelVer,
futureRoot,
rootUuid,
partFuture.Label,
generatedGrubConfigPath,
)
if err != nil {
PrintVerboseErr("ABSystem.RunOperation", 7.9, err)
return err
}
}
// Stage 7: Sync /etc
// ------------------------------------------------
PrintVerboseSimple("[Stage 7] -------- ABSystemRunOperation")
oldEtc := "/sysconf" // The current etc WITHOUT anything overlayed
oldUpperEtc := fmt.Sprintf("/var/lib/abroot/etc/%s", partPresent.Label)
newUpperEtc := fmt.Sprintf("/var/lib/abroot/etc/%s", partFuture.Label)
// make sure the future etc directories exist, ignoring errors
newWorkEtc := fmt.Sprintf("/var/lib/abroot/etc/%s-work", partFuture.Label)
if !dryRun {
os.MkdirAll(newUpperEtc, 0o755)
os.MkdirAll(newWorkEtc, 0o755)
err = EtcBuilder.ExtBuildCommand(oldEtc, futureRoot+"/sysconf", oldUpperEtc, newUpperEtc)
if err != nil {
PrintVerboseErr("AbSystem.RunOperation", 8, 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 bootloader
// ------------------------------------------------
PrintVerboseSimple("[Stage 9] -------- 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 !dryRun && 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
}
}
if !dryRun {
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
Parameters
Returns
func (*ABSystem) Rollback(checkOnly bool) (response ABRollbackResponse, err error)
{
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
Returns
func (*ABSystem) LockOperation() error
{
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
Returns
func (*ABSystem) UnlockOperation() error
{
err := os.Remove(operationLockFile)
if err != nil {
PrintVerboseErr("ABSystem.UnlockOperation", 0, err)
return err
}
PrintVerboseInfo("ABSystem.UnlockOperation", "lock file removed")
return nil
}
Returns
func (*ABSystem) finishedFileExists() bool
{
_, err := os.Stat(finishedOperationFile)
return !errors.Is(err, os.ErrNotExist)
}
Returns
func (*ABSystem) createFinishedFile() error
{
os.MkdirAll(filepath.Dir(finishedOperationFile), 0o755)
_, err := os.Create(finishedOperationFile)
if err != nil {
return err
}
return nil
}
Returns
func (*ABSystem) isLockfilePidActive() bool
{
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
}
Returns
func (*ABSystem) createFinalizingFile() error
{
os.MkdirAll(filepath.Dir(finalizingFile), 0o755)
_, err := os.Create(finalizingFile)
if err != nil {
return err
}
return nil
}
Returns
func (*ABSystem) removeFinalizingFile() error
{
err := os.Remove(finalizingFile)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
Fields
| Name | Type | Description |
|---|---|---|
| Checks | *Checks | |
| RootM | *ABRootManager | |
| CurImage | *ABImage |
ABSystemOperation
ABSystemOperation represents a system operation to be performed by the
ABSystem, must be given as a parameter to the RunOperation function.
type ABSystemOperation string
ABRollbackResponse
ABRollbackResponse represents the response of a rollback operation
type ABRollbackResponse string
NewABSystem
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.
Returns
func NewABSystem() (*ABSystem, error)
{
PrintVerboseInfo("NewABSystem: running...")
i, err := NewABImageFromRoot()
if err != nil {
PrintVerboseErr("NewABSystem", 0, err)
return nil, err
}
c := NewChecks()
rm := NewABRootManager()
return &ABSystem{
Checks: c,
RootM: rm,
CurImage: i,
}, nil
}
UserStopRequested
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)
Returns
func UserStopRequested() bool
{
if _, err := os.Stat(userStopFile); os.IsNotExist(err) {
return false
}
PrintVerboseInfo("ABSystem.UserStopRequested", "lock file exists")
return true
}
MakeStopRequest
MakeStopRequest requests all other abroot operations to stop
It also prevents any new operations from running.
Returns
func MakeStopRequest() error
{
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
CancelStopRequest removes the stop request
Returns
func CancelStopRequest() error
{
if !UserStopRequested() {
return nil
}
err := os.Remove(userStopFile)
if err != nil {
return fmt.Errorf("could remove stop file: %w", err)
}
return nil
}
init
func init()
{
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)
}
}
}
RootCheck
Parameters
Returns
func RootCheck(display bool) bool
{
if os.Geteuid() != 0 {
if display {
fmt.Println("You must be root to run this command")
}
return false
}
return true
}
fileExists
fileExists checks if a file exists
Parameters
Returns
func fileExists(path string) bool
{
if _, err := os.Stat(path); err == nil {
PrintVerboseInfo("fileExists", "File exists:", path)
return true
}
PrintVerboseInfo("fileExists", "File does not exist:", path)
return false
}
CopyFile
CopyFile copies a file from source to dest
Parameters
Returns
func CopyFile(source, dest string) error
{
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
}
MoveFile
MoveFile copies a file from source to dest, then remove the source file
Parameters
Returns
func MoveFile(source, dest string) error
{
err := CopyFile(source, dest)
if err != nil {
return err
}
PrintVerboseInfo("MoveFile", "Removing source file")
err = os.Remove(source)
if err != nil {
PrintVerboseErr("MoveFile", 0, err)
return err
}
return nil
}
ClearDirectory
ClearDirectory deletes all contents of a directory without removing the directory itself.
Skips files/directories specified in the skip list.
Parameters
Returns
func ClearDirectory(path string, skipList []string) error
{
files, err := os.ReadDir(path)
if err != nil {
return err
}
for _, file := range files {
if slices.Contains(skipList, file.Name()) {
continue
}
err = os.RemoveAll(filepath.Join(path, file.Name()))
if err != nil {
return err
}
}
return nil
}
isDeviceLUKSEncrypted
isDeviceLUKSEncrypted checks whether a device specified by devicePath is a LUKS-encrypted device
Parameters
Returns
func isDeviceLUKSEncrypted(devicePath string) (bool, error)
{
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
getDirSize calculates the total size of a directory recursively.
Parameters
Returns
func getDirSize(path string) (int64, error)
{
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
}
Checks
Represents a Checks struct which contains all the checks which can
be performed one by one or all at once using PerformAllChecks()
type Checks struct
Methods
PerformAllChecks performs all checks
Returns
func (*Checks) PerformAllChecks() error
{
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
Returns
func (*Checks) CheckCompatibilityFS() error
{
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
Returns
func (*Checks) CheckConnectivity() error
{
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
Returns
func (*Checks) CheckRoot() error
{
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
NewChecks returns a new Checks struct
Returns
func NewChecks() *Checks
{
return &Checks{}
}
ConfEditResult
ConfEditResult is the result of the ConfEdit function
type ConfEditResult int
ConfEdit
ConfEdit opens the configuration file in the default editor
Returns
func ConfEdit() (ConfEditResult, error)
{
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
}