PackageManager struct

PackageManager struct

Fields:

  • dryRun (bool)
  • baseDir (string)
  • Status (ABRootPkgManagerStatus)

Methods:

Add

Add adds a package to the packages.add file


Parameters:
  • pkg string

Returns:
  • error

Show/Hide Method Body
{
	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

Remove either removes a manually added package from packages.add or adds

a package to be deleted into packages.remove


Parameters:
  • pkg string

Returns:
  • error

Show/Hide Method Body
{
	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

GetAddPackages returns the packages in the packages.add file


Returns:
  • []string
  • error

Show/Hide Method Body
{
	PrintVerboseInfo("PackageManager.GetAddPackages", "running...")
	return p.getPackages(PackagesAddFile)
}

GetRemovePackages

GetRemovePackages returns the packages in the packages.remove file


Returns:
  • []string
  • error

Show/Hide Method Body
{
	PrintVerboseInfo("PackageManager.GetRemovePackages", "running...")
	return p.getPackages(PackagesRemoveFile)
}

GetUnstagedPackages

GetUnstagedPackages returns the package changes that are yet to be applied


Returns:
  • []UnstagedPackage
  • error

Show/Hide Method Body
{
	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

GetUnstagedPackagesPlain returns the package changes that are yet to be applied

as strings


Returns:
  • []string
  • error

Show/Hide Method Body
{
	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

ClearUnstagedPackages removes all packages from the unstaged list


Returns:
  • error

Show/Hide Method Body
{
	PrintVerboseInfo("PackageManager.ClearUnstagedPackages", "running...")
	return p.writeUnstagedPackages([]UnstagedPackage{})
}

GetAddPackagesString

GetAddPackagesString returns the packages in the packages.add file as a string


Parameters:
  • sep string

Returns:
  • string
  • error

Show/Hide Method Body
{
	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

GetRemovePackagesString returns the packages in the packages.remove file as a string


Parameters:
  • sep string

Returns:
  • string
  • error

Show/Hide Method Body
{
	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
}

getPackages


Parameters:
  • file string

Returns:
  • []string
  • error

Show/Hide Method Body
{
	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
}

writeAddPackages


Parameters:
  • pkgs []string

Returns:
  • error

Show/Hide Method Body
{
	PrintVerboseInfo("PackageManager.writeAddPackages", "running...")
	return p.writePackages(PackagesAddFile, pkgs)
}

writeRemovePackages


Parameters:
  • pkgs []string

Returns:
  • error

Show/Hide Method Body
{
	PrintVerboseInfo("PackageManager.writeRemovePackages", "running...")
	return p.writePackages(PackagesRemoveFile, pkgs)
}

writeUnstagedPackages


Parameters:
  • pkgs []UnstagedPackage

Returns:
  • error

Show/Hide Method Body
{
	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)
}

writePackages


Parameters:
  • file string
  • pkgs []string

Returns:
  • error

Show/Hide Method Body
{
	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
}

processApplyPackages


Returns:
  • string
  • string

Show/Hide Method Body
{
	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
}

processUpgradePackages


Returns:
  • string
  • string

Show/Hide Method Body
{
	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
}

GetFinalCmd


Parameters:
  • operation ABSystemOperation

Returns:
  • string

References:


Show/Hide Method Body
{
	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
}

getSummary


Returns:
  • string
  • error

Show/Hide Method Body
{
	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

WriteSummaryToFile writes added and removed packages to summaryFilePath

added packages get the + prefix, while removed packages get the - prefix


Parameters:
  • summaryFilePath string

Returns:
  • error

Show/Hide Method Body
{
	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
}

ExistsInRepo


Parameters:
  • pkg string

Returns:
  • error

Show/Hide Method Body
{
	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

AcceptUserAgreement sets the package manager status to enabled


Returns:
  • error

Show/Hide Method Body
{
	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

GetUserAgreementStatus returns if the user has accepted the package manager

agreement or not


Returns:
  • bool

Show/Hide Method Body
{
	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

CheckStatus checks if the package manager is enabled or not


Returns:
  • error

Show/Hide Method Body
{
	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 type

ABRootPkgManagerStatus represents the status of the package manager

in the ABRoot configuration file

Type Definition:

int

UnstagedPackage struct

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.

Fields:

  • Name (string)
  • Status (string)

NewPackageManager function

NewPackageManager returns a new PackageManager struct

Parameters:

  • dryRun bool

Returns:

  • *PackageManager
  • error
Show/Hide Function Body
{
	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 function

assertPkgMngApiSetUp checks whether the repo API is properly configured.

If a configuration exists but is malformed, returns an error.

Returns:

  • bool
  • error
Show/Hide Function Body
{
	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 function

GetRepoContentsForPkg retrieves package information from the repository API

Parameters:

  • pkg string

Returns:

  • map[string]interface{}
  • error
Show/Hide Function Body
{
	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
}

PCSpecs struct

Fields:

  • CPU (string)
  • GPU ([]string)
  • Memory (string)

GPUInfo struct

Fields:

  • Address (string)
  • Description (string)

getCPUInfo function

Returns:

  • string
  • error
Show/Hide Function Body
{
	info, err := cpu.Info()
	if err != nil {
		return "", err
	}
	if len(info) == 0 {
		return "", fmt.Errorf("CPU information not found")
	}
	return info[0].ModelName, nil
}

parseGPUInfo function

Parameters:

  • line string

Returns:

  • string
  • error
Show/Hide Function Body
{
	parts := strings.SplitN(line, " ", 3)
	if len(parts) < 3 {
		return "", fmt.Errorf("GPU information not found")
	}

	parts = strings.SplitN(parts[2], ":", 2)
	if len(parts) < 2 {
		return "", fmt.Errorf("GPU information not found")
	}

	return strings.TrimSpace(parts[1]), nil
}

getGPUInfo function

Returns:

  • []string
  • error
Show/Hide Function Body
{
	cmd := exec.Command("lspci")
	output, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Println("Error getting GPU info:", err)
		return nil, err
	}

	lines := strings.Split(string(output), "\n")

	var gpus []string
	for _, line := range lines {
		if strings.Contains(line, "VGA compatible controller") {
			gpu, err := parseGPUInfo(line)
			if err != nil {
				continue
			}
			gpus = append(gpus, gpu)
		}
	}

	return gpus, nil
}

getMemoryInfo function

Returns:

  • string
  • error
Show/Hide Function Body
{
	vm, err := mem.VirtualMemory()
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%d MB", vm.Total/1024/1024), nil
}

GetPCSpecs function

Returns:

  • PCSpecs

References:

Show/Hide Function Body
{
	cpu, _ := getCPUInfo()
	gpu, _ := getGPUInfo()
	memory, _ := getMemoryInfo()

	return PCSpecs{
		CPU:    cpu,
		GPU:    gpu,
		Memory: memory,
	}
}

ABSystem struct

An ABSystem allows to perform system operations such as upgrades,

package changes and rollback on an ABRoot-compliant system.

Fields:

  • Checks (*Checks)
  • RootM (*ABRootManager)
  • Registry (*Registry)
  • CurImage (*ABImage)

Methods:

CheckAll

CheckAll performs all checks from the Checks struct


Returns:
  • error

Show/Hide Method Body
{
	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

CheckUpdate checks if there is an update available


Returns:
  • string
  • bool

Show/Hide Method Body
{
	PrintVerboseInfo("ABSystem.CheckUpdate", "running...")
	return s.Registry.HasUpdate(s.CurImage.Digest)
}


Parameters:
  • systemNewPath string

Returns:
  • error

Show/Hide Method Body
{
	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

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.


Parameters:
  • operation ABSystemOperation

Returns:
  • error

References:


Show/Hide Method Body
{
	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

Rollback swaps the master grub files if the current root is not the default


Parameters:
  • checkOnly bool

Returns:
  • response ABRollbackResponse
  • err error

Show/Hide Method Body
{
	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

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)


Returns:
  • bool

Show/Hide Method Body
{
	if _, err := os.Stat(userLockFile); os.IsNotExist(err) {
		return false
	}

	PrintVerboseInfo("ABSystem.UserLockRequested", "lock file exists")
	return true
}

UpgradeLockExists

UpgradeLockExists checks if the lock file exists and returns a boolean


Returns:
  • bool

Show/Hide Method Body
{
	if _, err := os.Stat(lockFile); os.IsNotExist(err) {
		return false
	}

	PrintVerboseInfo("ABSystem.UpgradeLockExists", "lock file exists")
	return true
}

LockUpgrade

LockUpgrade creates a lock file, preventing upgrades from proceeding


Returns:
  • error

Show/Hide Method Body
{
	_, err := os.Create(lockFile)
	if err != nil {
		PrintVerboseErr("ABSystem.LockUpgrade", 0, err)
		return err
	}

	PrintVerboseInfo("ABSystem.LockUpgrade", "lock file created")
	return nil
}

UnlockUpgrade

UnlockUpgrade removes the lock file, allowing upgrades to proceed


Returns:
  • error

Show/Hide Method Body
{
	err := os.Remove(lockFile)
	if err != nil {
		PrintVerboseErr("ABSystem.UnlockUpgrade", 0, err)
		return err
	}

	PrintVerboseInfo("ABSystem.UnlockUpgrade", "lock file removed")
	return nil
}

CreateStageFile

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.


Returns:
  • error

Show/Hide Method Body
{
	_, err := os.Create(stageFile)
	if err != nil {
		PrintVerboseErr("ABSystem.CreateStageFile", 0, err)
		return err
	}

	PrintVerboseInfo("ABSystem.CreateStageFile", "stage file created")
	return nil
}

RemoveStageFile

RemoveStageFile removes the stage file disabling the ability to interrupt

the upgrade process


Returns:
  • error

Show/Hide Method Body
{
	err := os.Remove(stageFile)
	if err != nil {
		PrintVerboseErr("ABSystem.RemoveStageFile", 0, err)
		return err
	}

	PrintVerboseInfo("ABSystem.RemoveStageFile", "stage file removed")
	return nil
}

ABSystemOperation type

ABSystemOperation represents a system operation to be performed by the

ABSystem, must be given as a parameter to the RunOperation function.

Type Definition:

string

ABRollbackResponse type

ABRollbackResponse represents the response of a rollback operation

Type Definition:

string

NewABSystem function

NewABSystem initializes a new ABSystem, which contains all the functions

to perform system operations such as upgrades, package changes and rollback.

It returns a pointer to the initialized ABSystem and an error, if any.

Returns:

  • *ABSystem
  • error
Show/Hide Function Body
{
	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 function

isAbrootRunning checks if an instance of the `abroot` command is running

other than the current process

Returns:

  • bool
Show/Hide Function Body
{
	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 function

removeUpgradeLock removes the lock file, allowing upgrades to proceed

Returns:

  • error
Show/Hide Function Body
{
	err := os.Remove(lockFile)
	if err != nil {
		PrintVerboseErr("removeUpgradeLock", 0, err)
		return err
	}

	PrintVerboseInfo("removeUpgradeLock", "upgrade lock removed")
	return nil
}

rsyncCmd function

rsyncCmd executes the rsync command with the requested options.

If silent is true, rsync progress will not appear in stdout.

Parameters:

  • src string
  • dst string
  • opts []string
  • silent bool

Returns:

  • error
Show/Hide Function Body
{
	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 function

rsyncDryRun executes the rsync command with the --dry-run option.

Parameters:

  • src string
  • dst string
  • excluded []string

Returns:

  • error
Show/Hide Function Body
{
	opts := []string{"--dry-run"}

	if len(excluded) > 0 {
		for _, exclude := range excluded {
			opts = append(opts, "--exclude="+exclude)
		}
	}

	return rsyncCmd(src, dst, opts, false)
}

AtomicRsync function

AtomicRsync executes the rsync command in an atomic-like manner.

It does so by dry-running the rsync, and if it succeeds, it runs

the rsync again performing changes.

If the keepUnwanted option

is set to true, it will omit the --delete option, so that the already

existing and unwanted files will not be deleted.

To ensure the changes are applied atomically, we rsync on a _new directory first,

and use atomicSwap to replace the _new with the dst directory.

Parameters:

  • src string
  • dst string
  • transitionalPath string
  • finalPath string
  • excluded []string
  • keepUnwanted bool

Returns:

  • error
Show/Hide Function Body
{
	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)
}

init function

Show/Hide Function Body
{
	if !RootCheck(false) {
		return
	}

	if _, err := os.Stat(abrootDir); os.IsNotExist(err) {
		err := os.Mkdir(abrootDir, 0755)
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
	}
}

RootCheck function

Parameters:

  • display bool

Returns:

  • bool
Show/Hide Function Body
{
	if os.Geteuid() != 0 {
		if display {
			fmt.Println("You must be root to run this command")
		}

		return false
	}

	return true
}

fileExists function

fileExists checks if a file exists

Parameters:

  • path string

Returns:

  • bool
Show/Hide Function Body
{
	if _, err := os.Stat(path); err == nil {
		PrintVerboseInfo("fileExists", "File exists:", path)
		return true
	}

	PrintVerboseInfo("fileExists", "File does not exist:", path)
	return false
}

CopyFile function

CopyFile copies a file from source to dest

Parameters:

  • source string
  • dest string

Returns:

  • error
Show/Hide Function Body
{
	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 function

isDeviceLUKSEncrypted checks whether a device specified by devicePath is a LUKS-encrypted device

Parameters:

  • devicePath string

Returns:

  • bool
  • error
Show/Hide Function Body
{
	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 function

getDirSize calculates the total size of a directory recursively.

Parameters:

  • path string

Returns:

  • int64
  • error
Show/Hide Function Body
{
	ds, err := os.Stat(path)
	if err != nil {
		return 0, err
	}
	if !ds.IsDir() {
		return 0, fmt.Errorf("%s is not a directory", path)
	}

	inodes := map[uint64]bool{}
	var totalSize int64 = 0

	dfs := os.DirFS(path)
	err = fs.WalkDir(dfs, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if !d.IsDir() {
			fileinfo, err := d.Info()
			if err != nil {
				return err
			}

			fileinfoSys := fileinfo.Sys().(*syscall.Stat_t)
			if fileinfoSys.Nlink > 1 {
				if _, ok := inodes[fileinfoSys.Ino]; !ok {
					totalSize += fileinfo.Size()
					inodes[fileinfoSys.Ino] = true
				}
			} else {
				totalSize += fileinfo.Size()
				inodes[fileinfoSys.Ino] = true
			}
		}

		return nil
	})
	if err != nil {
		return 0, err
	}

	return totalSize, nil
}

Checks struct

Represents a Checks struct which contains all the checks which can

be performed one by one or all at once using PerformAllChecks()

Methods:

PerformAllChecks

PerformAllChecks performs all checks


Returns:
  • error

Show/Hide Method Body
{
	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

CheckCompatibilityFS checks if the filesystem is compatible with ABRoot v2

if not, it returns an error. Note that currently only ext4, btrfs and xfs

are supported/tested. Here we assume some utilities are installed, such as

findmnt and lsblk


Returns:
  • error

Show/Hide Method Body
{
	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

CheckConnectivity checks if the system is connected to the internet


Returns:
  • error

Show/Hide Method Body
{
	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

CheckRoot checks if the user is root and returns an error if not


Returns:
  • error

Show/Hide Method Body
{
	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 function

NewChecks returns a new Checks struct

Returns:

  • *Checks
Show/Hide Function Body
{
	return &Checks{}
}

getKernelVersion function

getKernelVersion returns the latest kernel version available in the root

Parameters:

  • bootPath string

Returns:

  • string
Show/Hide Function Body
{
	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 function

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.

Parameters:

  • newKernelVer string
  • initMountpoint string
  • partFuture string

Returns:

  • err error
Show/Hide Function Body
{
	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 struct

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

Fields:

  • Partitions ([]ABRootPartition)
  • VarPartition (Partition)

Methods:

GetPartitions

GetPartitions gets the root partitions from the current device


Returns:
  • error

Show/Hide Method Body
{
	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

IsCurrent checks if a partition is the current one


Parameters:
  • partition Partition

Returns:
  • bool

References:


Show/Hide Method Body
{
	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

IdentifyPartition identifies a partition


Parameters:
  • partition Partition

Returns:
  • identifiedAs string
  • err error

References:


Show/Hide Method Body
{
	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

GetPresent gets the present partition


Returns:
  • partition ABRootPartition
  • err error

Show/Hide Method Body
{
	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

GetFuture gets the future partition


Returns:
  • partition ABRootPartition
  • err error

Show/Hide Method Body
{
	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

GetOther gets the other partition


Returns:
  • partition ABRootPartition
  • err error

Show/Hide Method Body
{
	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

GetPartition gets a partition by label


Parameters:
  • label string

Returns:
  • partition ABRootPartition
  • err error

Show/Hide Method Body
{
	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

GetBoot gets the boot partition from the current device


Returns:
  • partition Partition
  • err error

Show/Hide Method Body
{
	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

GetInit gets the init volume when using LVM Thin-Provisioning


Returns:
  • partition Partition
  • err error

Show/Hide Method Body
{
	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 struct

ABRootPartition represents a partition managed by ABRoot

Fields:

  • Label (string)
  • IdentifiedAs (string)
  • Partition (Partition)
  • MountPoint (string)
  • MountOptions (string)
  • Uuid (string)
  • FsType (string)
  • Current (bool)

NewABRootManager function

NewABRootManager creates a new ABRootManager

Returns:

  • *ABRootManager
Show/Hide Function Body
{
	PrintVerboseInfo("NewABRootManager", "running...")

	a := &ABRootManager{}
	a.GetPartitions()

	return a
}

Grub struct

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

Fields:

  • PresentRoot (string)
  • FutureRoot (string)

Methods:

IsBootedIntoPresentRoot


Returns:
  • bool
  • error

Show/Hide Method Body
{
	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 function

generateABGrubConf generates a new grub config with the given details

Parameters:

  • kernelVersion string
  • rootPath string
  • rootUuid string
  • rootLabel string
  • generatedGrubConfigPath string

Returns:

  • error
Show/Hide Function Body
{
	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 function

NewGrub creates a new Grub instance

Parameters:

  • bootPart Partition

Returns:

  • *Grub
  • error

References:

Show/Hide Function Body
{
	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
}

ImageRecipe struct

An ImageRecipe represents a Dockerfile/Containerfile-like recipe

Fields:

  • From (string)
  • Labels (map[string]string)
  • Args (map[string]string)
  • Content (string)

Methods:

Write

Write writes a ImageRecipe to the given path, returning an error if any


Parameters:
  • path string

Returns:
  • error

Show/Hide Method Body
{
	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 function

NewImageRecipe creates a new ImageRecipe instance and returns a pointer to it

Parameters:

  • image string
  • labels map[string]string
  • args map[string]string
  • content string

Returns:

  • *ImageRecipe
Show/Hide Function Body
{
	PrintVerboseInfo("NewImageRecipe", "running...")

	return &ImageRecipe{
		From:    image,
		Labels:  labels,
		Args:    args,
		Content: content,
	}
}

RepairRootIntegrity function

Parameters:

  • rootPath string

Returns:

  • err error
Show/Hide Function Body
{
	fixupOlderSystems(rootPath)

	err = repairLinks(rootPath)
	if err != nil {
		return err
	}

	err = repairPaths(rootPath)
	if err != nil {
		return err
	}

	return nil
}

repairPaths function

Parameters:

  • rootPath string

Returns:

  • err error
Show/Hide Function Body
{
	for _, path := range pathsToRepair {
		err = repairPath(filepath.Join(rootPath, path))
		if err != nil {
			return err
		}
	}
	return nil
}

repairPath function

Parameters:

  • path string

Returns:

  • err error
Show/Hide Function Body
{
	if info, err := os.Lstat(path); err == nil && info.IsDir() {
		return nil
	}

	err = os.Remove(path)
	if err != nil && !os.IsNotExist(err) {
		PrintVerboseErr("repairPath", 1, "Can't remove ", path, " : ", err)
		return err
	}

	PrintVerboseInfo("repairPath", "Repairing ", path)
	err = os.MkdirAll(path, 0o755)
	if err != nil {
		PrintVerboseErr("repairPath", 2, "Can't create ", path, " : ", err)
		return err
	}

	return nil
}

fixupOlderSystems function

this is here to keep compatibility with older systems

e.g. /media was a folder instead of a symlink to /var/media

Parameters:

  • rootPath string
Show/Hide Function Body
{
	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
			}
		}
	}
}

init function

Show/Hide Function Body
{
	if os.Getenv("ABROOT_KARGS_PATH") != "" {
		KargsPath = os.Getenv("ABROOT_KARGS_PATH")
	}
}

kargsCreateIfMissing function

kargsCreateIfMissing creates the kargs file if it doesn't exist

Returns:

  • error
Show/Hide Function Body
{
	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 function

KargsWrite makes a backup of the current kargs file and then

writes the new content to it

Parameters:

  • content string

Returns:

  • error
Show/Hide Function Body
{
	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 function

KargsBackup makes a backup of the current kargs file

Returns:

  • error
Show/Hide Function Body
{
	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 function

KargsRead reads the content of the kargs file

Returns:

  • string
  • error
Show/Hide Function Body
{
	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 function

KargsFormat formats the contents of the kargs file, ensuring that

there are no duplicate entries, multiple spaces or trailing newline

Parameters:

  • content string

Returns:

  • string
  • error
Show/Hide Function Body
{
	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 function

KargsEdit copies the kargs file to a temporary file and opens it in the

user's preferred editor by querying the $EDITOR environment variable.

Once closed, its contents are written back to the main kargs file.

This function returns a boolean parameter indicating whether any changes

were made to the kargs file.

Returns:

  • bool
  • error
Show/Hide Function Body
{
	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 function

init initializes the log file and sets up logging

Show/Hide Function Body
{
	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 function

IsVerbose checks if verbose mode is enabled

Returns:

  • bool
Show/Hide Function Body
{
	flag := cmdr.FlagValBool("verbose")
	_, arg := os.LookupEnv("ABROOT_VERBOSE")
	return flag || arg
}

formatMessage function

formatMessage formats log messages based on prefix, level, and depth

Parameters:

  • prefix string
  • level string
  • depth float32
  • args ...interface{}

Returns:

  • string
Show/Hide Function Body
{
	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 function

printFormattedMessage prints formatted log messages to Stdout

Parameters:

  • formattedMsg string
Show/Hide Function Body
{
	printLog.Printf("%s\n", formattedMsg)
}

logToFileIfEnabled function

logToFileIfEnabled logs messages to the file if logging is enabled

Parameters:

  • formattedMsg string
Show/Hide Function Body
{
	if logFile != nil {
		LogToFile(formattedMsg)
	}
}

PrintVerboseNoLog function

PrintVerboseNoLog prints verbose messages without logging to the file

Parameters:

  • prefix string
  • level string
  • depth float32
  • args ...interface{}
Show/Hide Function Body
{
	if IsVerbose() {
		formattedMsg := formatMessage(prefix, level, depth, args...)
		printFormattedMessage(formattedMsg)
	}
}

PrintVerbose function

PrintVerbose prints verbose messages and logs to the file if enabled

Parameters:

  • prefix string
  • level string
  • depth float32
  • args ...interface{}
Show/Hide Function Body
{
	PrintVerboseNoLog(prefix, level, depth, args...)

	logToFileIfEnabled(formatMessage(prefix, level, depth, args...))
}

PrintVerboseSimpleNoLog function

PrintVerboseSimpleNoLog prints simple verbose messages without logging to the file

Parameters:

  • args ...interface{}
Show/Hide Function Body
{
	PrintVerboseNoLog("", "", -1, args...)
}

PrintVerboseSimple function

PrintVerboseSimple prints simple verbose messages and logs to the file if enabled

Parameters:

  • args ...interface{}
Show/Hide Function Body
{
	PrintVerbose("", "", -1, args...)
}

PrintVerboseErrNoLog function

PrintVerboseErrNoLog prints verbose error messages without logging to the file

Parameters:

  • prefix string
  • depth float32
  • args ...interface{}
Show/Hide Function Body
{
	PrintVerboseNoLog(prefix, "err", depth, args...)
}

PrintVerboseErr function

PrintVerboseErr prints verbose error messages and logs to the file if enabled

Parameters:

  • prefix string
  • depth float32
  • args ...interface{}
Show/Hide Function Body
{
	PrintVerbose(prefix, "err", depth, args...)
}

PrintVerboseWarnNoLog function

PrintVerboseWarnNoLog prints verbose warning messages without logging to the file

Parameters:

  • prefix string
  • depth float32
  • args ...interface{}
Show/Hide Function Body
{
	PrintVerboseNoLog(prefix, "warn", depth, args...)
}

PrintVerboseWarn function

PrintVerboseWarn prints verbose warning messages and logs to the file if enabled

Parameters:

  • prefix string
  • depth float32
  • args ...interface{}
Show/Hide Function Body
{
	PrintVerbose(prefix, "warn", depth, args...)
}

PrintVerboseInfoNoLog function

PrintVerboseInfoNoLog prints verbose info messages without logging to the file

Parameters:

  • prefix string
  • args ...interface{}
Show/Hide Function Body
{
	PrintVerboseNoLog(prefix, "info", -1, args...)
}

PrintVerboseInfo function

PrintVerboseInfo prints verbose info messages and logs to the file if enabled

Parameters:

  • prefix string
  • args ...interface{}
Show/Hide Function Body
{
	PrintVerbose(prefix, "info", -1, args...)
}

LogToFile function

LogToFile writes messages to the log file

Parameters:

  • msg string
  • args ...interface{}

Returns:

  • error
Show/Hide Function Body
{
	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 function

GetLogFile returns the log file handle

Returns:

  • *os.File
Show/Hide Function Body
{
	return logFile
}

AtomicSwap function

atomicSwap allows swapping 2 files or directories in-place and atomically,

using the renameat2 syscall. This should be used instead of os.Rename,

which is not atomic at all

Parameters:

  • src string
  • dst string

Returns:

  • error
Show/Hide Function Body
{
	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 type

ConfEditResult is the result of the ConfEdit function

Type Definition:

int

ConfEdit function

ConfEdit opens the configuration file in the default editor

Returns:

  • ConfEditResult
  • error

References:

Show/Hide Function Body
{
	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 struct

DiskManager exposes functions to interact with the system's disks

and partitions (e.g. mount, unmount, get partitions, etc.)

Methods:

GetPartitionByLabel

GetPartitionByLabel finds a partition by searching for its label.

If no partition can be found with the given label, returns error.


Parameters:
  • label string

Returns:
  • Partition
  • error

References:


Show/Hide Method Body
{
	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

getPartitions gets a disk's partitions. If device is an empty string, gets

all partitions from all disks


Parameters:
  • device string

Returns:
  • []Partition
  • error

Show/Hide Method Body
{
	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 struct

Partition represents either a standard partition or a device-mapper

partition, such as an LVM volume

Fields:

  • Label (string)
  • MountPoint (string)
  • MountOptions (string)
  • Uuid (string)
  • FsType (string)
  • Device (string)
  • Parent (*Partition)

Methods:

Mount

Mount mounts a partition to a directory, returning an error if any occurs


Parameters:
  • destination string

Returns:
  • error

Show/Hide Method Body
{
	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

Unmount unmounts a partition


Returns:
  • error

Show/Hide Method Body
{
	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
}

IsDevMapper

Returns whether the partition is a device-mapper virtual partition


Returns:
  • bool

Show/Hide Method Body
{
	return p.Parent != nil
}

IsEncrypted

IsEncrypted returns whether the partition is encrypted


Returns:
  • bool

Show/Hide Method Body
{
	return strings.HasPrefix(p.FsType, "crypto_")
}

Children struct

The children a block device or partition may have

Fields:

  • MountPoint (string) - json:"mountpoint"
  • FsType (string) - json:"fstype"
  • Label (string) - json:"label"
  • Uuid (string) - json:"uuid"
  • LogicalName (string) - json:"name"
  • Size (string) - json:"size"
  • MountOptions (string) - json:"mountopts"
  • Children ([]Children) - json:"children"

NewDiskManager function

NewDiskManager creates and returns a pointer to a new DiskManager instance

from which you can interact with the system's disks and partitions

Returns:

  • *DiskManager
Show/Hide Function Body
{
	return &DiskManager{}
}

iterChildren function

iterChildren iterates through the children of a device or partition

recursively

Parameters:

  • childs *[]Children
  • result *[]Partition
Show/Hide Function Body
{
	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 function

BaseImagePackageDiff retrieves the added, removed, upgraded and downgraded

base packages (the ones bundled with the image).

Parameters:

  • currentDigest string
  • newDigest string

Returns:

  • added []diff.PackageDiff
  • upgraded []diff.PackageDiff
  • downgraded []diff.PackageDiff
  • removed []diff.PackageDiff
  • err error
Show/Hide Function Body
{
	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 function

OverlayPackageDiff retrieves the added, removed, upgraded and downgraded

overlay packages (the ones added manually via `abroot pkg add`).

Returns:

  • added []diff.PackageDiff
  • upgraded []diff.PackageDiff
  • downgraded []diff.PackageDiff
  • removed []diff.PackageDiff
  • err error
Show/Hide Function Body
{
	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
}

NotEnoughSpaceError struct

Methods:

Error


Returns:
  • string

Show/Hide Method Body
{
	return "not enough space in disk"
}

padString function

Parameters:

  • str string
  • size int

Returns:

  • string
Show/Hide Function Body
{
	if len(str) < size {
		return str + strings.Repeat(" ", size-len(str))
	} else {
		return str
	}
}

OciExportRootFs function

OciExportRootFs generates a rootfs from an image recipe file

Parameters:

  • buildImageName string
  • imageRecipe *ImageRecipe
  • transDir string
  • dest string

Returns:

  • error
Show/Hide Function Body
{
	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
}

checkImageSize function

returns nil if there's enough space in the filesystem for the image

returns NotEnoughSpaceError if there is not enough space

returns other error if the sizes were not calculated correctly

Parameters:

  • imageDir string
  • filesystemMount string

Returns:

  • error
Show/Hide Function Body
{
	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 function

pullImageWithProgressbar pulls the image specified in the provided recipe

and reports the download progress using pterm progressbars. Each blob has

its own bar, similar to how docker and podman report downloads in their

respective CLIs

Parameters:

  • pt *prometheus.Prometheus
  • name string
  • image *ImageRecipe

Returns:

  • error
Show/Hide Function Body
{
	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 function

FindImageWithLabel returns the name of the first image containinig the provided key-value pair

or an empty string if none was found

FindImageWithLabel returns the name of the first image containing the

provided key-value pair or an empty string if none was found

Parameters:

  • key string
  • value string

Returns:

  • string
  • error
Show/Hide Function Body
{
	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 function

RetrieveImageForRoot retrieves the image created for the provided root

based on the label. Note for distro maintainers: labels must follow those

defined in the ABRoot config file

Parameters:

  • root string

Returns:

  • string
  • error
Show/Hide Function Body
{
	PrintVerboseInfo("RetrieveImageForRoot", "running...")

	image, err := FindImageWithLabel("ABRoot.root", root)
	if err != nil {
		PrintVerboseErr("RetrieveImageForRoot", 0, err)
		return "", err
	}

	return image, nil
}

DeleteImageForRoot function

DeleteImageForRoot deletes the image created for the provided root

Parameters:

  • root string

Returns:

  • error
Show/Hide Function Body
{
	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
}

Registry struct

A Registry instance exposes functions to interact with the configured

Docker registry

Fields:

  • API (string)

Methods:

HasUpdate

HasUpdate checks if the image/tag from the registry has a different digest

it returns the new digest and a boolean indicating if an update is available


Parameters:
  • digest string

Returns:
  • string
  • bool

Show/Hide Method Body
{
	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

GetManifest returns the manifest of the image, a token is required

to perform the request and is generated using GetToken()


Parameters:
  • token string

Returns:
  • *Manifest
  • error

Show/Hide Method Body
{
	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 struct

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

Fields:

  • Manifest ([]byte)
  • Digest (string)
  • Layers ([]string)

NewRegistry function

NewRegistry returns a new Registry instance, exposing functions to

interact with the configured Docker registry

Returns:

  • *Registry
Show/Hide Function Body
{
	PrintVerboseInfo("NewRegistry", "running...")
	return &Registry{
		API: fmt.Sprintf("https://%s/%s", settings.Cnf.Registry, settings.Cnf.RegistryAPIVersion),
	}
}

getRegistryAuthUrl function

Returns:

  • string
  • string
  • error
Show/Hide Function Body
{
	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 function

GetToken generates a token using the provided tokenURL and returns it

Returns:

  • string
  • error
Show/Hide Function Body
{
	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 struct

Chroot represents a chroot instance, which can be used to run commands

inside a chroot environment

Fields:

  • root (string)
  • rootUuid (string)
  • rootDevice (string)
  • etcMounted (bool)

Methods:

Close

Close unmounts all the bind mounts and closes the chroot environment


Returns:
  • error

Show/Hide Method Body
{
	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

Execute runs a command in the chroot environment, the command is

a string and the arguments are a list of strings. If an error occurs

it is returned.


Parameters:
  • cmd string

Returns:
  • error

Show/Hide Method Body
{
	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

ExecuteCmds runs a list of commands in the chroot environment,

stops at the first error


Parameters:
  • cmds []string

Returns:
  • error

Show/Hide Method Body
{
	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 function

NewChroot creates a new chroot environment from the given root path and

returns its Chroot instance or an error if something went wrong

Parameters:

  • root string
  • rootUuid string
  • rootDevice string
  • mountUserEtc bool
  • userEtcPath string

Returns:

  • *Chroot
  • error
Show/Hide Function Body
{
	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 function

MergeDiff merges the diff lines between the first and second files into

the destination file. If any errors occur, they are returned.

Parameters:

  • firstFile string
  • secondFile string
  • destination string

Returns:

  • error
Show/Hide Function Body
{
	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 function

DiffFiles returns the diff lines between source and dest files using the

diff command (assuming it is installed). If no diff is found, nil is

returned. If any errors occur, they are returned.

Parameters:

  • sourceFile string
  • destFile string

Returns:

  • []byte
  • error
Show/Hide Function Body
{
	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 function

WriteDiff applies the diff lines to the destination file using the patch

command (assuming it is installed). If any errors occur, they are returned.

Parameters:

  • destFile string
  • diffLines []byte

Returns:

  • error
Show/Hide Function Body
{
	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
}

ABImage struct

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

Fields:

  • Digest (string) - json:"digest"
  • Timestamp (time.Time) - json:"timestamp"
  • Image (string) - json:"image"

Methods:

WriteTo

WriteTo writes the json to a destination path, if the suffix is not empty,

it will be appended to the filename


Parameters:
  • dest string
  • suffix string

Returns:
  • error

Show/Hide Method Body
{
	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 function

NewABImage creates a new ABImage instance and returns a pointer to it,

if the digest is empty, it returns an error

Parameters:

  • digest string
  • image string

Returns:

  • *ABImage
  • error
Show/Hide Function Body
{
	if digest == "" {
		return nil, fmt.Errorf("NewABImage: digest is empty")
	}

	return &ABImage{
		Digest:    digest,
		Timestamp: time.Now(),
		Image:     image,
	}, nil
}

NewABImageFromRoot function

NewABImageFromRoot returns the current ABImage by parsing /abimage.abr, if

it fails, it returns an error (e.g. if the file doesn't exist).

Note for distro maintainers: if the /abimage.abr is not present, it could

mean that the user is running an older version of ABRoot (pre v2) or the

root state is corrupted. In the latter case, generating a new ABImage should

fix the issue, Digest and Timestamp can be random, but Image should reflect

an existing image on the configured Docker registry. Anyway, support on this

is not guaranteed, so please don't open issues about this.

Returns:

  • *ABImage
  • error
Show/Hide Function Body
{
	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
}

encoding/json import

Import example:

import "encoding/json"

errors import

Import example:

import "errors"

fmt import

Import example:

import "fmt"

io import

Import example:

import "io"

net/http import

Import example:

import "net/http"

net/url import

Import example:

import "net/url"

os import

Import example:

import "os"

path/filepath import

Import example:

import "path/filepath"

strings import

Import example:

import "strings"

time import

Import example:

import "time"

github.com/vanilla-os/abroot/settings import

Import example:

import "github.com/vanilla-os/abroot/settings"

fmt import

Import example:

import "fmt"

os/exec import

Import example:

import "os/exec"

strings import

Import example:

import "strings"

github.com/shirou/gopsutil/cpu import

Import example:

import "github.com/shirou/gopsutil/cpu"

github.com/shirou/gopsutil/mem import

Import example:

import "github.com/shirou/gopsutil/mem"

errors import

Import example:

import "errors"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

path/filepath import

Import example:

import "path/filepath"

strconv import

Import example:

import "strconv"

strings import

Import example:

import "strings"

github.com/google/uuid import

Import example:

import "github.com/google/uuid"

github.com/linux-immutability-tools/EtcBuilder/cmd import

Import example:

import "github.com/linux-immutability-tools/EtcBuilder/cmd"

Imported as:

EtcBuilder

github.com/vanilla-os/abroot/settings import

Import example:

import "github.com/vanilla-os/abroot/settings"

github.com/vanilla-os/sdk/pkg/v1/goodies import

Import example:

import "github.com/vanilla-os/sdk/pkg/v1/goodies"

bufio import

Import example:

import "bufio"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

strconv import

Import example:

import "strconv"

strings import

Import example:

import "strings"

github.com/vanilla-os/orchid/cmdr import

Import example:

import "github.com/vanilla-os/orchid/cmdr"

fmt import

Import example:

import "fmt"

io import

Import example:

import "io"

io/fs import

Import example:

import "io/fs"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

syscall import

Import example:

import "syscall"

errors import

Import example:

import "errors"

fmt import

Import example:

import "fmt"

net import

Import example:

import "net"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

runtime import

Import example:

import "runtime"

time import

Import example:

import "time"

errors import

Import example:

import "errors"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

path import

Import example:

import "path"

path/filepath import

Import example:

import "path/filepath"

strings import

Import example:

import "strings"

github.com/hashicorp/go-version import

Import example:

import "github.com/hashicorp/go-version"

errors import

Import example:

import "errors"

github.com/vanilla-os/abroot/settings import

Import example:

import "github.com/vanilla-os/abroot/settings"

errors import

Import example:

import "errors"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

path/filepath import

Import example:

import "path/filepath"

strings import

Import example:

import "strings"

github.com/vanilla-os/abroot/settings import

Import example:

import "github.com/vanilla-os/abroot/settings"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

path/filepath import

Import example:

import "path/filepath"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

strings import

Import example:

import "strings"

fmt import

Import example:

import "fmt"

log import

Import example:

import "log"

os import

Import example:

import "os"

path/filepath import

Import example:

import "path/filepath"

time import

Import example:

import "time"

github.com/vanilla-os/orchid/cmdr import

Import example:

import "github.com/vanilla-os/orchid/cmdr"

os import

Import example:

import "os"

golang.org/x/sys/unix import

Import example:

import "golang.org/x/sys/unix"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

github.com/vanilla-os/abroot/settings import

Import example:

import "github.com/vanilla-os/abroot/settings"

encoding/json import

Import example:

import "encoding/json"

errors import

Import example:

import "errors"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

strings import

Import example:

import "strings"

syscall import

Import example:

import "syscall"

encoding/json import

Import example:

import "encoding/json"

fmt import

Import example:

import "fmt"

io import

Import example:

import "io"

net/http import

Import example:

import "net/http"

strings import

Import example:

import "strings"

github.com/vanilla-os/abroot/extras/dpkg import

Import example:

import "github.com/vanilla-os/abroot/extras/dpkg"

github.com/vanilla-os/abroot/settings import

Import example:

import "github.com/vanilla-os/abroot/settings"

github.com/vanilla-os/differ/diff import

Import example:

import "github.com/vanilla-os/differ/diff"

context import

Import example:

import "context"

errors import

Import example:

import "errors"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

path/filepath import

Import example:

import "path/filepath"

strings import

Import example:

import "strings"

syscall import

Import example:

import "syscall"

time import

Import example:

import "time"

github.com/containers/buildah import

Import example:

import "github.com/containers/buildah"

github.com/containers/image/v5/types import

Import example:

import "github.com/containers/image/v5/types"

github.com/containers/storage/types import

Import example:

import "github.com/containers/storage/types"

Imported as:

cstypes

github.com/dustin/go-humanize import

Import example:

import "github.com/dustin/go-humanize"

Imported as:

humanize

github.com/pterm/pterm import

Import example:

import "github.com/pterm/pterm"

github.com/vanilla-os/abroot/settings import

Import example:

import "github.com/vanilla-os/abroot/settings"

github.com/vanilla-os/prometheus import

Import example:

import "github.com/vanilla-os/prometheus"

encoding/json import

Import example:

import "encoding/json"

fmt import

Import example:

import "fmt"

io import

Import example:

import "io"

net/http import

Import example:

import "net/http"

strings import

Import example:

import "strings"

github.com/vanilla-os/abroot/settings import

Import example:

import "github.com/vanilla-os/abroot/settings"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

path/filepath import

Import example:

import "path/filepath"

strings import

Import example:

import "strings"

syscall import

Import example:

import "syscall"

bytes import

Import example:

import "bytes"

os import

Import example:

import "os"

os/exec import

Import example:

import "os/exec"

encoding/json import

Import example:

import "encoding/json"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

path/filepath import

Import example:

import "path/filepath"

time import

Import example:

import "time"