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