mirror of
https://github.com/clearlinux/clr-installer.git
synced 2026-04-28 11:13:46 +00:00
This change modifies the behavior of the clr-installer. By default, the installer includes NetworkManager bundle in the list of bundles to be installed. This commit introduces an exception: if systemd-networkd is already present in the bundle list, the addition of NetworkManager will be skipped. Signed-off-by: Alex Jaramillo <alex.v.jaramillo@intel.com>
1132 lines
31 KiB
Go
1132 lines
31 KiB
Go
// Copyright © 2020 Intel Corporation
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
|
|
"github.com/clearlinux/clr-installer/args"
|
|
"github.com/clearlinux/clr-installer/boolset"
|
|
"github.com/clearlinux/clr-installer/cmd"
|
|
"github.com/clearlinux/clr-installer/conf"
|
|
"github.com/clearlinux/clr-installer/errors"
|
|
"github.com/clearlinux/clr-installer/hostname"
|
|
"github.com/clearlinux/clr-installer/isoutils"
|
|
"github.com/clearlinux/clr-installer/kernel"
|
|
"github.com/clearlinux/clr-installer/keyboard"
|
|
"github.com/clearlinux/clr-installer/language"
|
|
"github.com/clearlinux/clr-installer/log"
|
|
"github.com/clearlinux/clr-installer/model"
|
|
"github.com/clearlinux/clr-installer/network"
|
|
"github.com/clearlinux/clr-installer/progress"
|
|
"github.com/clearlinux/clr-installer/proxy"
|
|
"github.com/clearlinux/clr-installer/storage"
|
|
"github.com/clearlinux/clr-installer/swupd"
|
|
"github.com/clearlinux/clr-installer/telemetry"
|
|
"github.com/clearlinux/clr-installer/timezone"
|
|
cuser "github.com/clearlinux/clr-installer/user"
|
|
"github.com/clearlinux/clr-installer/utils"
|
|
)
|
|
|
|
var (
|
|
// NetworkPassing is used to track if the latest network configuration
|
|
// is passing; changes in proxy, etc.
|
|
NetworkPassing bool
|
|
)
|
|
|
|
const (
|
|
// NetWorkManager is the application to manage network.
|
|
NetWorkManager = "Network Manager"
|
|
NetworkdBundle = "systemd-networkd-autostart"
|
|
)
|
|
|
|
func sortMountPoint(bds []*storage.BlockDevice) []*storage.BlockDevice {
|
|
sort.Slice(bds[:], func(i, j int) bool {
|
|
return filepath.HasPrefix(bds[j].MountPoint, bds[i].MountPoint)
|
|
})
|
|
|
|
return bds
|
|
}
|
|
|
|
// Install is the main install controller, this is the entry point for a full
|
|
// installation
|
|
// nolint: gocyclo // TODO: Refactor this
|
|
func Install(rootDir string, model *model.SystemInstall, options args.Args) error {
|
|
var err error
|
|
var prg progress.Progress
|
|
var encryptedUsed, softRaidUsed, lvmRootUsed, lvmOtherUsed bool
|
|
|
|
vars := map[string]string{
|
|
"chrootDir": rootDir,
|
|
"yamlDir": filepath.Dir(options.ConfigFile),
|
|
}
|
|
|
|
for k, v := range model.Environment {
|
|
vars[k] = v
|
|
}
|
|
|
|
preConfFile := log.GetPreConfFile()
|
|
|
|
if err = model.WriteFile(preConfFile); err != nil {
|
|
log.Error("Failed to write pre-install YAML file (%v) %q", err, preConfFile)
|
|
}
|
|
|
|
advanced := false
|
|
for _, tm := range model.TargetMedias {
|
|
advanced = advanced || tm.IsAdvancedConfiguration()
|
|
}
|
|
|
|
if model.EncryptionRequiresPassphrase(advanced) && model.CryptPass == "" {
|
|
model.CryptPass = storage.GetPassPhrase()
|
|
if model.CryptPass == "" {
|
|
return errors.Errorf("Can not create encrypted file system, no passphrase")
|
|
}
|
|
}
|
|
|
|
if !options.StubImage {
|
|
if err = applyHooks("pre-install", vars, model.PreInstall); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
version := utils.VersionUintString(model.Version)
|
|
|
|
log.Debug("Clear Linux OS version: %s", version)
|
|
|
|
// do we have the minimum required to install a system?
|
|
if err = model.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Using MassInstaller (non-UI) the network will not have been checked yet
|
|
if !NetworkPassing &&
|
|
!options.StubImage &&
|
|
!swupd.OfflineIsUsable(version, options) &&
|
|
len(model.UserBundles) != 0 {
|
|
if err = ConfigureNetwork(model); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
expandMe := []*storage.BlockDevice{}
|
|
detachMe := []string{}
|
|
removeMe := []string{}
|
|
aliasMap := map[string]string{}
|
|
usingPhysicalMedia := true
|
|
|
|
// prepare image file, case the user has declared image alias then create
|
|
// the image, setup the loop device, prepare the variable expansion
|
|
for _, alias := range model.StorageAlias {
|
|
var file string
|
|
|
|
if alias.DeviceFile {
|
|
continue
|
|
}
|
|
|
|
// create the image and add the alias name to the variable expansion list
|
|
for _, tm := range model.TargetMedias {
|
|
if tm.Name == fmt.Sprintf("${%s}", alias.Name) {
|
|
if err = storage.MakeImage(tm, alias.File); err != nil {
|
|
return err
|
|
}
|
|
|
|
expandMe = append(expandMe, tm)
|
|
usingPhysicalMedia = false
|
|
}
|
|
}
|
|
|
|
// Add the image file to the hooks variables
|
|
vars["imageFile"] = alias.File
|
|
|
|
file, err = storage.SetupLoopDevice(alias.File)
|
|
if err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
aliasMap[alias.Name] = filepath.Base(file)
|
|
detachMe = append(detachMe, file)
|
|
if !model.KeepImage {
|
|
removeMe = append(removeMe, alias.File)
|
|
}
|
|
|
|
retry := 5
|
|
|
|
// wait the loop device to be prepared and available with 5 retry attempts
|
|
for {
|
|
var ok bool
|
|
|
|
if ok, err = utils.FileExists(file); err != nil {
|
|
for _, file := range detachMe {
|
|
storage.DetachLoopDevice(file)
|
|
}
|
|
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
if ok || retry == 0 {
|
|
break
|
|
}
|
|
|
|
retry--
|
|
time.Sleep(time.Second * 1)
|
|
}
|
|
}
|
|
|
|
// defer detaching used loop devices
|
|
defer func() {
|
|
for _, file := range detachMe {
|
|
storage.DetachLoopDevice(file)
|
|
}
|
|
|
|
// Now that image is unmounted, run post-image hooks
|
|
if err = applyHooks("post-image", vars, model.PostImage); err != nil {
|
|
log.Error("Error during post-image hook: %q", err)
|
|
}
|
|
|
|
// The request to keep image may have changed during execution
|
|
// such as dynamically decided to not make an ISO or failing to
|
|
// make an ISO
|
|
if !model.KeepImage {
|
|
for _, file := range removeMe {
|
|
log.Debug("Removing raw image file: %s", file)
|
|
if err = os.Remove(file); err != nil {
|
|
log.Warning("Failed to remove image file: %s", file)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final message to user that the installation has fully completed
|
|
msg := utils.Locale.Get("Installation Steps Complete")
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
prg.Success()
|
|
}()
|
|
|
|
// expand block device's name case we've detected image replacement cases
|
|
for _, tm := range expandMe {
|
|
oldName := tm.Name
|
|
tm.ExpandName(aliasMap)
|
|
newName := tm.Name
|
|
// Remap the InstallSelected
|
|
if model.InstallSelected[newName].Name == oldName ||
|
|
model.InstallSelected[newName].Name == "" {
|
|
oldCopy := model.InstallSelected[oldName]
|
|
oldCopy.Name = newName
|
|
model.InstallSelected[newName] = oldCopy
|
|
} else {
|
|
model.InstallSelected[newName] = model.InstallSelected[oldName]
|
|
}
|
|
delete(model.InstallSelected, oldName)
|
|
}
|
|
|
|
mountPoints := []*storage.BlockDevice{}
|
|
|
|
if usingPhysicalMedia {
|
|
if model.MakeISO {
|
|
msg := "Flag --iso not valid for physical media; disabling"
|
|
fmt.Println(msg)
|
|
log.Warning(msg)
|
|
model.MakeISO = false
|
|
}
|
|
}
|
|
|
|
// prepare all the target block devices
|
|
if err := storage.PrepareInstallationMedia(model.InstallSelected,
|
|
model.TargetMedias, model.MediaOpts, nil); err != nil {
|
|
log.Warning("PrepareInstallationMedia: %+v", err)
|
|
return err
|
|
}
|
|
|
|
// First create a list of all children we need to check
|
|
var childrenToCheck []*storage.BlockDevice
|
|
|
|
for _, curr := range model.TargetMedias {
|
|
// Are we using software RAID
|
|
softRaidUsed = softRaidUsed || curr.UsesRaid()
|
|
|
|
childrenToCheck = append(childrenToCheck, curr.FindAllChildren()...)
|
|
}
|
|
|
|
// prepare the blockdevice's partitions filesystem
|
|
for _, ch := range childrenToCheck {
|
|
if ch.Type == storage.BlockDeviceTypeCrypt {
|
|
encryptedUsed = true
|
|
|
|
if ch.FsTypeNotSwap() {
|
|
msg := utils.Locale.Get("Mapping %s partition to an encrypted partition", ch.Name)
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
if err = ch.MapEncrypted(model.CryptPass); err != nil {
|
|
prg.Failure()
|
|
return err
|
|
}
|
|
prg.Success()
|
|
}
|
|
}
|
|
|
|
if ch.Type == storage.BlockDeviceTypeLVM2Volume {
|
|
if ch.MountPoint == "/" {
|
|
lvmRootUsed = true
|
|
} else {
|
|
lvmOtherUsed = true
|
|
}
|
|
}
|
|
|
|
// if we have a mount point set it for future mounting
|
|
if ch.MountPoint != "" {
|
|
mountPoints = append(mountPoints, ch)
|
|
}
|
|
|
|
// Do not overwrite File System content for pre-existing
|
|
if !ch.FormatPartition {
|
|
msg := utils.Locale.Get("Skipping new file system for %s", ch.Name)
|
|
log.Debug(msg)
|
|
continue
|
|
}
|
|
|
|
msg := utils.Locale.Get("Writing %s file system to %s", ch.FsType, ch.Name)
|
|
if ch.MountPoint != "" {
|
|
msg = msg + fmt.Sprintf(" '%s'", ch.MountPoint)
|
|
}
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
if err = ch.MakeFs(); err != nil {
|
|
prg.Failure()
|
|
return err
|
|
}
|
|
prg.Success()
|
|
}
|
|
|
|
// Update the target devices current labels and UUIDs
|
|
if scanErr := storage.UpdateBlockDevices(model.TargetMedias); scanErr != nil {
|
|
return scanErr
|
|
}
|
|
|
|
if options.StubImage {
|
|
return nil
|
|
}
|
|
|
|
// mount all the prepared partitions
|
|
for _, curr := range sortMountPoint(mountPoints) {
|
|
log.Info("Mounting: %s", curr.MountPoint)
|
|
|
|
if err = curr.Mount(rootDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
log.Info("Umounting rootDir: %s", rootDir)
|
|
if storage.UmountAll() != nil {
|
|
log.Warning("Failed to umount volumes")
|
|
return
|
|
}
|
|
|
|
log.Info("Removing rootDir: %s", rootDir)
|
|
if err = os.RemoveAll(rootDir); err != nil {
|
|
log.Warning("Failed to remove rootDir: %s", rootDir)
|
|
}
|
|
}()
|
|
|
|
err = storage.MountMetaFs(rootDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If we are using NetworkManager add the basic bundle, unless
|
|
// we have systemd-networkd bundle, in which case skip it
|
|
if network.IsNetworkManagerActive() && !model.ContainsBundle(NetworkdBundle) {
|
|
log.Info("Adding bundle '%s' to enable networking", network.RequiredBundle)
|
|
model.AddBundle(network.RequiredBundle)
|
|
}
|
|
|
|
// Add in the User Defined bundles
|
|
for _, curr := range model.UserBundles {
|
|
log.Info("Adding bundle '%s' from user selection", curr)
|
|
model.AddBundle(curr)
|
|
}
|
|
|
|
if model.Telemetry.Enabled {
|
|
log.Info("Adding bundle '%s' to enable telemetry", telemetry.RequiredBundle)
|
|
model.AddBundle(telemetry.RequiredBundle)
|
|
}
|
|
|
|
if len(model.Users) > 0 {
|
|
log.Info("Adding bundle '%s' to support non-root users", cuser.RequiredBundle)
|
|
model.AddBundle(cuser.RequiredBundle)
|
|
}
|
|
|
|
if model.Timezone.Code != timezone.DefaultTimezone {
|
|
log.Info("Adding bundle '%s' due to non-default timezone '%s'",
|
|
timezone.RequiredBundle, model.Timezone.Code)
|
|
model.AddBundle(timezone.RequiredBundle)
|
|
}
|
|
|
|
if model.Keyboard.Code != keyboard.DefaultKeyboard {
|
|
log.Info("Adding bundle '%s' due to non-default keyboard '%s'",
|
|
keyboard.RequiredBundle, model.Keyboard.Code)
|
|
model.AddBundle(keyboard.RequiredBundle)
|
|
}
|
|
|
|
if model.Language.Code != language.DefaultLanguage {
|
|
log.Info("Adding bundle '%s' due to non-default language '%s'",
|
|
language.RequiredBundle, model.Language.Code)
|
|
model.AddBundle(language.RequiredBundle)
|
|
}
|
|
|
|
if encryptedUsed || softRaidUsed || lvmRootUsed {
|
|
log.Info("Adding bundle '%s' to enable encryption, sw RAID, or LVM root", storage.RequiredBundle)
|
|
model.AddBundle(storage.RequiredBundle)
|
|
}
|
|
if lvmOtherUsed {
|
|
log.Info("Adding bundle '%s' to enable LVM", storage.RequiredBundleLVM)
|
|
model.AddBundle(storage.RequiredBundleLVM)
|
|
}
|
|
if encryptedUsed {
|
|
kernelArgs := []string{storage.KernelArgument}
|
|
model.AddExtraKernelArguments(kernelArgs)
|
|
}
|
|
|
|
msg := utils.Locale.Get("Writing mount files")
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
if err = storage.GenerateTabFiles(rootDir, model.TargetMedias); err != nil {
|
|
prg.Failure()
|
|
return err
|
|
}
|
|
prg.Success()
|
|
|
|
if model.KernelArguments != nil && len(model.KernelArguments.Add) > 0 {
|
|
cmdlineDir := filepath.Join(rootDir, "etc", "kernel")
|
|
cmdlineFile := filepath.Join(cmdlineDir, "cmdline")
|
|
cmdline := strings.Join(model.KernelArguments.Add, " ")
|
|
|
|
if err = utils.MkdirAll(cmdlineDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = ioutil.WriteFile(cmdlineFile, []byte(cmdline), 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if model.KernelArguments != nil && len(model.KernelArguments.Remove) > 0 {
|
|
cmdlineDir := filepath.Join(rootDir, "etc", "kernel", "cmdline-removal.d")
|
|
cmdlineFile := filepath.Join(cmdlineDir, "clr-installer.conf")
|
|
cmdline := strings.Join(model.KernelArguments.Remove, " ")
|
|
|
|
if err = utils.MkdirAll(cmdlineDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = ioutil.WriteFile(cmdlineFile, []byte(cmdline), 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if prg, err = contentInstall(rootDir, version, model, options); err != nil {
|
|
prg.Failure()
|
|
return err
|
|
}
|
|
|
|
if model.MediaOpts.SwapFileSize != "" {
|
|
msg := utils.Locale.Get("Creating %s", storage.SwapfileName)
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
if err = storage.CreateSwapFile(rootDir, model.MediaOpts.SwapFileSize); err != nil {
|
|
prg.Failure()
|
|
return err
|
|
}
|
|
prg.Success()
|
|
}
|
|
|
|
if err = configureTimezone(rootDir, model); err != nil {
|
|
// Just log the error, not setting the timezone is not reason to fail the install
|
|
log.Error("Error setting timezone: %v", err)
|
|
}
|
|
|
|
if err = configureKeyboard(rootDir, model); err != nil {
|
|
// Just log the error, not setting the keyboard is not reason to fail the install
|
|
log.Error("Error setting keyboard: %v", err)
|
|
}
|
|
|
|
if err = configureLanguage(rootDir, model); err != nil {
|
|
// Just log the error, not setting the language is not reason to fail the install
|
|
log.Error("Error setting language locale: %v", err)
|
|
}
|
|
|
|
if err = cuser.Apply(rootDir, model.Users); err != nil {
|
|
return err
|
|
}
|
|
|
|
if model.Hostname != "" {
|
|
if err = hostname.SetTargetHostname(rootDir, model.Hostname); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if model.CopyNetwork {
|
|
if err = network.CopyNetworkInterfaces(rootDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if model.CopySwupd {
|
|
swupd.CopyConfigurations(rootDir)
|
|
}
|
|
|
|
if model.AllowInsecureHTTP {
|
|
swupd.CreateConfig(rootDir)
|
|
}
|
|
|
|
if model.Telemetry.URL != "" {
|
|
if err = model.Telemetry.CreateTelemetryConf(rootDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = applyHooks("post-install", vars, model.PostInstall); err != nil {
|
|
return err
|
|
}
|
|
|
|
msg = utils.Locale.Get("Saving the installation results")
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
if err = saveInstallResults(rootDir, model); err != nil {
|
|
log.ErrorError(err)
|
|
}
|
|
prg.Success()
|
|
|
|
if model.MakeISO {
|
|
log.Info("Generating ISO image")
|
|
if err = generateISO(rootDir, model, options); err != nil {
|
|
log.ErrorError(err)
|
|
}
|
|
}
|
|
|
|
msg = utils.Locale.Get("Installation completed")
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
prg.Success()
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyHooks(name string, vars map[string]string, hooks []*model.InstallHook) error {
|
|
locName := utils.Locale.Get(name)
|
|
msg := utils.Locale.Get("Running %s hooks", locName)
|
|
prg := progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
|
|
for idx, curr := range hooks {
|
|
if err := runInstallHook(vars, curr); err != nil {
|
|
prg.Failure()
|
|
return err
|
|
}
|
|
prg.Partial(idx)
|
|
}
|
|
|
|
prg.Success()
|
|
return nil
|
|
}
|
|
|
|
func runInstallHook(vars map[string]string, hook *model.InstallHook) error {
|
|
args := []string{}
|
|
vars["chrooted"] = "0"
|
|
|
|
if hook.Chroot {
|
|
args = append(args, []string{"chroot", vars["chrootDir"]}...)
|
|
vars["chrooted"] = "1"
|
|
|
|
restoreFile := copyHostResolvToTarget(vars["chrootDir"])
|
|
defer func() {
|
|
restoreTargetResolv(vars["chrootDir"], restoreFile)
|
|
}()
|
|
}
|
|
|
|
exec := utils.ExpandVariables(vars, hook.Cmd)
|
|
args = append(args, []string{"bash", "-l", "-c", exec}...)
|
|
|
|
if err := cmd.RunAndLogWithEnv(vars, args...); err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// use the current host's version to bootstrap the sysroot, then update to the
|
|
// latest one and start adding new bundles
|
|
// for the bootstrap we use the hosts's swupd and the following operations are
|
|
// executed using the target swupd
|
|
func contentInstall(rootDir string, version string,
|
|
md *model.SystemInstall, options args.Args) (progress.Progress, error) {
|
|
var prg progress.Progress
|
|
|
|
sw := swupd.New(rootDir, options, md)
|
|
|
|
// Currently, ISO image generation supports only a single kernel.
|
|
// Hence, skip ISO generation if multiple kernel bundles are present.
|
|
// TODO: Remove this logic when ISO generation supports multiple kernels.
|
|
if md.MakeISO {
|
|
for _, curr := range md.Bundles {
|
|
if strings.HasPrefix(curr, "kernel-") {
|
|
msg := "ISO image generation supports only a single kernel. Setting ISO generation to false.\n"
|
|
log.Warning(msg)
|
|
fmt.Printf("Warning: %s", msg)
|
|
md.MakeISO = false
|
|
md.KeepImage = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
bundles := md.Bundles
|
|
|
|
if md.Kernel.Bundle != "none" {
|
|
bundles = append(bundles, md.Kernel.Bundle)
|
|
}
|
|
|
|
// We have usable offline content available
|
|
if swupd.OfflineIsUsable(version, options) {
|
|
if utils.IsLatestVersion(version) {
|
|
log.Info("Overriding version from '%s' to %s to enable offline install", version, utils.ClearVersion)
|
|
version = utils.ClearVersion
|
|
}
|
|
|
|
msg := utils.Locale.Get("Copying cached content to target media")
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
|
|
// Copying offline content here is a performance optimization and is not a hard
|
|
// failure because Swupd may be able to successfully copy offline content or
|
|
// install over the network.
|
|
if err := copyOfflineToStatedir(rootDir, sw.GetStateDir()); err != nil {
|
|
log.Warning("Failed to copy offline content: %s", err)
|
|
prg.Failure()
|
|
time.Sleep(time.Second * 2)
|
|
} else {
|
|
prg.Success()
|
|
}
|
|
} else {
|
|
if swupd.IsOfflineContent() {
|
|
log.Warning("Ignoring the Offline content (%s) due to version set to %s", utils.ClearVersion, version)
|
|
}
|
|
}
|
|
|
|
msg := utils.Locale.Get("Installing base OS and configured bundles")
|
|
log.Info(msg)
|
|
|
|
log.Debug("Installing bundles: %s", strings.Join(bundles, ", "))
|
|
if err := sw.OSInstall(version, swupd.TargetPrefix, bundles); err != nil {
|
|
// If the swupd command failed to run there wont be a progress
|
|
// bar, so we need to create a new one that we can fail
|
|
prg = progress.NewLoop(msg)
|
|
return prg, err
|
|
}
|
|
|
|
// Create custom config in the installer image to override default bundle list
|
|
if md.TargetBundles != nil {
|
|
if err := writeCustomConfig(rootDir, md); err != nil {
|
|
prg = progress.NewLoop(msg)
|
|
return prg, err
|
|
}
|
|
}
|
|
|
|
if md.Offline {
|
|
// Install minimum set of required bundles to offline content directory.
|
|
log.Info("Installing offline content to the target")
|
|
|
|
offlineBundles := []string{
|
|
network.RequiredBundle,
|
|
telemetry.RequiredBundle,
|
|
cuser.RequiredBundle,
|
|
timezone.RequiredBundle,
|
|
keyboard.RequiredBundle,
|
|
language.RequiredBundle,
|
|
storage.RequiredBundle,
|
|
storage.RequiredBundleLVM,
|
|
}
|
|
|
|
// Load default config from chroot for required bundles list
|
|
bundleConfig, err := conf.LookupDefaultChrootConfig(rootDir)
|
|
if err != nil {
|
|
prg = progress.NewLoop(msg)
|
|
return prg, err
|
|
}
|
|
loadedBundles, err := model.LoadFile(bundleConfig, options)
|
|
if err != nil {
|
|
prg = progress.NewLoop(msg)
|
|
return prg, err
|
|
}
|
|
offlineBundles = append(offlineBundles, loadedBundles.Bundles...)
|
|
|
|
// Load available kernel bundles from chroot
|
|
loadedKernels, err := kernel.LoadKernelListChroot(rootDir)
|
|
if err != nil {
|
|
prg = progress.NewLoop(msg)
|
|
return prg, err
|
|
}
|
|
for _, k := range loadedKernels {
|
|
offlineBundles = append(offlineBundles, k.Bundle)
|
|
}
|
|
|
|
log.Debug("Downloading bundles: %s", strings.Join(offlineBundles, ", "))
|
|
if err := sw.DownloadBundles(version, offlineBundles); err != nil {
|
|
prg = progress.NewLoop(msg)
|
|
return prg, err
|
|
}
|
|
}
|
|
|
|
if !md.AutoUpdate.Value() {
|
|
msg := utils.Locale.Get("Disabling automatic updates")
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
if err := sw.DisableUpdate(); err != nil {
|
|
warnMsg := utils.Locale.Get("Disabling automatic updates failed")
|
|
log.Warning(warnMsg)
|
|
return prg, err
|
|
}
|
|
prg.Success()
|
|
}
|
|
|
|
msg = utils.Locale.Get("Installing boot loader")
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
|
|
cbmPath := options.CBMPath
|
|
if cbmPath == "" {
|
|
cbmPath = fmt.Sprintf("%s/usr/bin/clr-boot-manager", rootDir)
|
|
}
|
|
|
|
args := []string{
|
|
cbmPath,
|
|
"update",
|
|
"--image",
|
|
fmt.Sprintf("--path=%s", rootDir),
|
|
}
|
|
|
|
envVars := map[string]string{
|
|
"CBM_DEBUG": "1",
|
|
}
|
|
|
|
if md.MediaOpts.LegacyBios {
|
|
envVars["CBM_FORCE_LEGACY"] = "1"
|
|
}
|
|
|
|
err := cmd.RunAndLogWithEnv(envVars, args...)
|
|
if err != nil {
|
|
return prg, errors.Wrap(err)
|
|
}
|
|
prg.Success()
|
|
|
|
// Clean-up State Directory content
|
|
if options.SwupdStateClean {
|
|
msg = utils.Locale.Get("Cleaning Swupd state directory")
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
if err = sw.CleanUpState(); err != nil {
|
|
log.ErrorError(err)
|
|
}
|
|
prg.Success()
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func copyOfflineToStatedir(rootDir, stateDir string) error {
|
|
// Force an error for testing
|
|
if testFail, _ := utils.FileExists(path.Join(conf.OfflineContentDir, "FAIL")); testFail {
|
|
return fmt.Errorf("Forcing a failing for %s", conf.OfflineContentDir)
|
|
}
|
|
|
|
if err := utils.MkdirAll(filepath.Dir(stateDir), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debug("Overwriting stateDir with offline content")
|
|
if err := os.RemoveAll(stateDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
if isoLoopDev := isoutils.GetIsoLoopDevice(); strings.Compare(isoLoopDev, "") != 0 {
|
|
// Extract offline contents for ISO installer
|
|
log.Debug("Extracting offline content in squashfs to target media")
|
|
|
|
if err := isoutils.ExtractSquashfs(conf.OfflineContentDir, rootDir, isoLoopDev); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(path.Join(rootDir, conf.OfflineContentDir), stateDir); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Copy offline contents for img installer
|
|
log.Debug("Copying offline content to target media")
|
|
|
|
// The performance of utils.CopyAllFiles is much slower than cp when
|
|
// copying a large number of files.
|
|
args := []string{
|
|
"cp",
|
|
"-ar",
|
|
conf.OfflineContentDir,
|
|
stateDir,
|
|
}
|
|
err := cmd.RunAndLog(args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ConfigureNetwork applies the model/configured network interfaces
|
|
func ConfigureNetwork(model *model.SystemInstall) error {
|
|
prg, err := configureNetwork(model)
|
|
if err != nil {
|
|
prg.Failure()
|
|
NetworkPassing = false
|
|
return err
|
|
}
|
|
|
|
NetworkPassing = true
|
|
|
|
return nil
|
|
}
|
|
|
|
func configureNetwork(model *model.SystemInstall) (progress.Progress, error) {
|
|
proxy.SetHTTPSProxy(model.HTTPSProxy)
|
|
|
|
if len(model.NetworkInterfaces) > 0 {
|
|
msg := "Applying network settings"
|
|
prg := progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
if err := network.Apply("/", model.NetworkInterfaces); err != nil {
|
|
prg.Failure()
|
|
return prg, err
|
|
}
|
|
prg.Success()
|
|
|
|
msg = utils.Locale.Get("Restarting network interfaces")
|
|
prg = progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
if err := network.Restart(); err != nil {
|
|
prg.Failure()
|
|
return prg, err
|
|
}
|
|
prg.Success()
|
|
}
|
|
|
|
msg := utils.Locale.Get("Testing connectivity")
|
|
attempts := 3
|
|
prg := progress.NewLoop(msg)
|
|
ok := false
|
|
// 3 attempts to test connectivity
|
|
for i := 0; i < attempts; i++ {
|
|
time.Sleep(2 * time.Second)
|
|
|
|
log.Info(msg)
|
|
if err := network.VerifyConnectivity(); err == nil {
|
|
ok = true
|
|
break
|
|
}
|
|
log.Warning("Attempt to verify connectivity failed")
|
|
|
|
// Restart networking if we failed
|
|
// The likely gain is restarting pacdiscovery to fix autoproxy
|
|
if err := network.Restart(); err != nil {
|
|
log.Warning("Network restart failed")
|
|
ok = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if !ok {
|
|
msg = utils.Locale.Get("Network check failed.")
|
|
msg += " " + utils.Locale.Get("Use %s to configure network.", NetWorkManager)
|
|
return prg, errors.Errorf(msg)
|
|
}
|
|
|
|
prg.Success()
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// configureTimezone applies the model/configured Timezone to the target
|
|
func configureTimezone(rootDir string, model *model.SystemInstall) error {
|
|
if model.Timezone.Code == timezone.DefaultTimezone {
|
|
log.Debug("Skipping setting timezone " + model.Timezone.Code)
|
|
return nil
|
|
}
|
|
|
|
msg := "Setting Timezone to " + model.Timezone.Code
|
|
prg := progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
|
|
err := timezone.SetTargetTimezone(rootDir, model.Timezone.Code)
|
|
if err != nil {
|
|
prg.Failure()
|
|
return err
|
|
}
|
|
prg.Success()
|
|
|
|
return nil
|
|
}
|
|
|
|
// configureKeyboard applies the model/configured keyboard to the target
|
|
func configureKeyboard(rootDir string, model *model.SystemInstall) error {
|
|
if model.Keyboard.Code == keyboard.DefaultKeyboard {
|
|
log.Debug("Skipping setting keyboard " + model.Keyboard.Code)
|
|
return nil
|
|
}
|
|
|
|
msg := "Setting Keyboard to " + model.Keyboard.Code
|
|
prg := progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
|
|
err := keyboard.SetTargetKeyboard(rootDir, model.Keyboard.Code)
|
|
if err != nil {
|
|
prg.Failure()
|
|
return err
|
|
}
|
|
prg.Success()
|
|
|
|
return nil
|
|
}
|
|
|
|
// configureLanguage applies the model/configured language to the target
|
|
func configureLanguage(rootDir string, model *model.SystemInstall) error {
|
|
if model.Language.Code == language.DefaultLanguage {
|
|
log.Debug("Skipping setting language locale " + model.Language.Code)
|
|
return nil
|
|
}
|
|
|
|
msg := utils.Locale.Get("Setting Language locale to %s", model.Language.Code)
|
|
prg := progress.NewLoop(msg)
|
|
log.Info(msg)
|
|
|
|
err := language.SetTargetLanguage(rootDir, model.Language.Code)
|
|
if err != nil {
|
|
prg.Failure()
|
|
return err
|
|
}
|
|
prg.Success()
|
|
|
|
return nil
|
|
}
|
|
|
|
// saveInstallResults saves the results of the installation process
|
|
// onto the target media
|
|
func saveInstallResults(rootDir string, md *model.SystemInstall) error {
|
|
var err error
|
|
errMsgs := []string{}
|
|
|
|
// Log a sanitized YAML file with Telemetry
|
|
var cleanModel model.SystemInstall
|
|
// Marshal current into bytes
|
|
confBytes, bytesErr := yaml.Marshal(md)
|
|
if bytesErr != nil {
|
|
log.Error("Failed to generate a copy of YAML data (%v)", bytesErr)
|
|
errMsgs = append(errMsgs, "Failed to generate YAML file")
|
|
}
|
|
// Unmarshal into a copy
|
|
if yamlErr := yaml.UnmarshalStrict(confBytes, &cleanModel); yamlErr != nil {
|
|
errMsgs = append(errMsgs, "Failed to duplicate YAML file")
|
|
}
|
|
// Sanitize the config data to remove any potential
|
|
// Personal Information from the data set
|
|
cleanModel.Users = nil // Remove User Info
|
|
cleanModel.Hostname = "" // Remove user defined hostname
|
|
cleanModel.HTTPSProxy = "" // Remove user defined Proxy
|
|
cleanModel.SwupdMirror = "" // Remove user defined Swupd Mirror
|
|
cleanModel.NetworkInterfaces = nil // Remove Network information
|
|
|
|
// Remove the Serial number from the target media
|
|
for _, bd := range cleanModel.TargetMedias {
|
|
bd.Serial = ""
|
|
}
|
|
|
|
var payload string
|
|
hypervisor := md.Telemetry.RunningEnvironment()
|
|
extendedModel := model.SystemUsage{
|
|
InstallModel: cleanModel,
|
|
Hypervisor: hypervisor,
|
|
}
|
|
confBytes, bytesErr = yaml.Marshal(extendedModel)
|
|
if bytesErr != nil {
|
|
log.Error("Failed to generate a sanitized data (%v)", bytesErr)
|
|
errMsgs = append(errMsgs, "Failed to generate a sanitized YAML file")
|
|
payload = strings.Join(errMsgs, ";")
|
|
} else {
|
|
payload = string(confBytes[:])
|
|
}
|
|
|
|
if errLog := md.Telemetry.LogRecord("success", 1, payload); errLog != nil {
|
|
log.Error("Failed to log Telemetry success record")
|
|
}
|
|
|
|
if md.PostArchive.Value() {
|
|
log.Info("Saving Installation results to %s", rootDir)
|
|
|
|
saveDir := filepath.Join(rootDir, "root")
|
|
if err = utils.MkdirAll(saveDir, 0755); err != nil {
|
|
// Fallback in the unlikely case we can't use root's home
|
|
saveDir = rootDir
|
|
}
|
|
|
|
confFile := filepath.Join(saveDir, conf.ConfigFile)
|
|
|
|
if err := md.WriteFile(confFile); err != nil {
|
|
log.Error("Failed to write YAML file (%v) %q", err, confFile)
|
|
errMsgs = append(errMsgs, "Failed to write YAML file")
|
|
}
|
|
|
|
logFile := filepath.Join(saveDir, conf.LogFile)
|
|
|
|
if err := log.ArchiveLogFile(logFile); err != nil {
|
|
errMsgs = append(errMsgs, "Failed to archive log file")
|
|
}
|
|
} else {
|
|
log.Info("Skipping archiving of Installation results")
|
|
}
|
|
|
|
if md.IsTelemetryEnabled() {
|
|
if md.IsTelemetryInstalled() {
|
|
log.Info("Copying telemetry records to target system.")
|
|
if err := md.Telemetry.CopyTelemetryRecords(rootDir); err != nil {
|
|
log.Warning("Failed to copy image Telemetry data")
|
|
errMsgs = append(errMsgs, "Failed to copy image Telemetry data")
|
|
}
|
|
log.Info("Running telemctl opt-in")
|
|
if err := md.Telemetry.OptIn(rootDir); err != nil {
|
|
log.Warning("Failed to opt-in to telemetry")
|
|
errMsgs = append(errMsgs, "Failed to opt-in to telemetry")
|
|
}
|
|
if len(errMsgs) > 0 {
|
|
return errors.Errorf("%s", strings.Join(errMsgs, ";"))
|
|
}
|
|
} else {
|
|
log.Info("Telemetry is not present in the installer, skipping copying records")
|
|
}
|
|
} else {
|
|
log.Info("Telemetry disabled, skipping record collection.")
|
|
if md.Telemetry.Installed(rootDir) {
|
|
log.Info("Running telemctl opt-out")
|
|
if err := md.Telemetry.OptOut(rootDir); err != nil {
|
|
log.Warning("Unable to opt-out, telemetry might not be present")
|
|
errMsgs = append(errMsgs, "Unable to opt-out, telemetry might not be present")
|
|
}
|
|
if len(errMsgs) > 0 {
|
|
return errors.Errorf("%s", strings.Join(errMsgs, ";"))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateISO creates an ISO image from the just created raw image
|
|
func generateISO(rootDir string, md *model.SystemInstall, options args.Args) error {
|
|
var err error
|
|
log.Info("Building ISO image")
|
|
|
|
if !md.MediaOpts.LegacyBios {
|
|
for _, alias := range md.StorageAlias {
|
|
if err = isoutils.MakeIso(rootDir, strings.TrimSuffix(alias.File,
|
|
filepath.Ext(alias.File)), md, options); err != nil {
|
|
md.KeepImage = true
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
err = fmt.Errorf("cannot create ISO images for configurations with LegacyBios enabled")
|
|
log.ErrorError(err)
|
|
md.KeepImage = true
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// writeCustomConfig creates a config file in the installer image that overrides the default
|
|
func writeCustomConfig(chrootPath string, md *model.SystemInstall) error {
|
|
customPath := path.Join(chrootPath, conf.CustomConfigDir)
|
|
|
|
if err := utils.MkdirAll(customPath, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create basic config based on provided values
|
|
customModel := &model.SystemInstall{
|
|
Bundles: md.TargetBundles,
|
|
Keyboard: md.Keyboard,
|
|
Language: md.Language,
|
|
Timezone: md.Timezone,
|
|
Kernel: md.Kernel,
|
|
}
|
|
// We should always default to enabled
|
|
customModel.AutoUpdate = boolset.NewTrue()
|
|
|
|
return customModel.WriteFile(path.Join(customPath, conf.ConfigFile))
|
|
}
|
|
|
|
// copyHostResolvToTarget first saves the original /etc/resolve.conf if it exists,
|
|
// then copies the hosts /etc/resolv.conf to the target to enable networking (DNS)
|
|
// in the chrooted environment.
|
|
// Used with restoreTargetResolv
|
|
func copyHostResolvToTarget(rootDir string) string {
|
|
resolvConf := filepath.Join(rootDir, "etc", "resolv.conf")
|
|
restoreFile := ""
|
|
|
|
// make a backup if it exists
|
|
if ok, _ := utils.FileExists(resolvConf); ok {
|
|
restoreFile = fmt.Sprintf("%s.%d", resolvConf, os.Getpid())
|
|
log.Debug("Saving a temp copy of file %q as %q", resolvConf, restoreFile)
|
|
if err := utils.CopyFile(resolvConf, restoreFile); err != nil {
|
|
log.Error("Failed to save file %q: %s", restoreFile, err)
|
|
restoreFile = ""
|
|
}
|
|
}
|
|
|
|
// copy the host to target
|
|
log.Debug("Copying host's /etc/resolv.conf to target to enable DNS for networking during hook %q", resolvConf)
|
|
if err := utils.CopyFile("/etc/resolv.conf", resolvConf); err != nil {
|
|
log.Error("Failed to install file %q: %s", resolvConf, err)
|
|
}
|
|
|
|
return restoreFile
|
|
}
|
|
|
|
// restoreTargetResolv removed the temporary /etc/resolv.conf on the target system
|
|
// then restores the original /etc/resolve.conf if it exists,
|
|
// Used with copyHostResolvToTarget
|
|
func restoreTargetResolv(rootDir string, restoreFile string) {
|
|
resolvConf := filepath.Join(rootDir, "etc", "resolv.conf")
|
|
|
|
if restoreFile != "" {
|
|
log.Debug("Restore original resolv.conf file %q", restoreFile)
|
|
if ok, _ := utils.FileExists(restoreFile); ok {
|
|
// restore the target file
|
|
if err := utils.CopyFile(restoreFile, resolvConf); err != nil {
|
|
log.Error("Failed to restore file %q: %s", resolvConf, err)
|
|
}
|
|
} else {
|
|
log.Warning("Resolv.conf restore file missing: %s", restoreFile)
|
|
}
|
|
} else {
|
|
log.Debug("Removing temp copy of file %q", resolvConf)
|
|
if err := os.Remove(resolvConf); err != nil {
|
|
log.Warning("Failed to clean up temporary network file: %q: %s", resolvConf, err)
|
|
}
|
|
}
|
|
}
|