Files
clr-installer/model/model.go
T
Mark D Horn b4c1c98a55 model: Save autoUpdate to the YAML when false
Since we have code which assumes we want autoUpdate to be true
unless it is added to the YAML file, we need to be sure it is
write to the YAML when false.

Signed-off-by: Mark D Horn <mark.d.horn@intel.com>
2019-09-12 19:20:47 -07:00

562 lines
16 KiB
Go

// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: GPL-3.0-only
package model
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"gopkg.in/yaml.v2"
"github.com/clearlinux/clr-installer/args"
"github.com/clearlinux/clr-installer/errors"
"github.com/clearlinux/clr-installer/kernel"
"github.com/clearlinux/clr-installer/keyboard"
"github.com/clearlinux/clr-installer/language"
"github.com/clearlinux/clr-installer/network"
"github.com/clearlinux/clr-installer/storage"
"github.com/clearlinux/clr-installer/telemetry"
"github.com/clearlinux/clr-installer/timezone"
"github.com/clearlinux/clr-installer/user"
"github.com/clearlinux/clr-installer/utils"
)
const (
// DemoVersion is hard coded string we display in log files
// when running in demo (aka documentation mode). We will
// now use this as a flag to not include the version in UI.
DemoVersion = "X.Y.Z"
)
// Version of Clear Installer.
// Also used by the Makefile for releases.
// Default to the version of the program
// but may be overridden for demo/documentation mode.
// Set by Go linker in the Makefile
var Version = "undefined"
// BuildDate is set by the Go linker with the build datetime
var BuildDate = "undefined"
var testAlias = []string{}
// SystemInstall represents the system install "configuration", the target
// medias, bundles to install and whatever state a install may require
type SystemInstall struct {
InstallSelected map[string]storage.InstallTarget `yaml:"-"`
TargetMedias []*storage.BlockDevice `yaml:"targetMedia"`
NetworkInterfaces []*network.Interface `yaml:"networkInterfaces,omitempty,flow"`
Keyboard *keyboard.Keymap `yaml:"keyboard,omitempty,flow"`
Language *language.Language `yaml:"language,omitempty,flow"`
Bundles []string `yaml:"bundles,omitempty,flow"`
UserBundles []string `yaml:"userBundles,omitempty,flow"`
HTTPSProxy string `yaml:"httpsProxy,omitempty,flow"`
Telemetry *telemetry.Telemetry `yaml:"telemetry,omitempty,flow"`
Timezone *timezone.TimeZone `yaml:"timezone,omitempty,flow"`
Users []*user.User `yaml:"users,omitempty,flow"`
KernelArguments *kernel.Arguments `yaml:"kernel-arguments,omitempty,flow"`
Kernel *kernel.Kernel `yaml:"kernel,omitempty,flow"`
PostReboot bool `yaml:"postReboot,omitempty,flow"`
SwupdMirror string `yaml:"swupdMirror,omitempty,flow"`
PostArchive bool `yaml:"postArchive,omitempty,flow"`
Hostname string `yaml:"hostname,omitempty,flow"`
AutoUpdate bool `yaml:"autoUpdate,flow"`
TelemetryURL string `yaml:"telemetryURL,omitempty,flow"`
TelemetryTID string `yaml:"telemetryTID,omitempty,flow"`
TelemetryPolicy string `yaml:"telemetryPolicy,omitempty,flow"`
PreInstall []*InstallHook `yaml:"pre-install,omitempty,flow"`
PostInstall []*InstallHook `yaml:"post-install,omitempty,flow"`
Version uint `yaml:"version,omitempty,flow"`
StorageAlias []*StorageAlias `yaml:"block-devices,omitempty,flow"`
LegacyBios bool `yaml:"legacyBios,omitempty,flow"`
CopyNetwork bool `yaml:"copyNetwork,omitempty,flow"`
Environment map[string]string `yaml:"env,omitempty,flow"`
CryptPass string `yaml:"-"`
MakeISO bool `yaml:"iso,omitempty,flow"`
KeepImage bool `yaml:"keepImage,omitempty,flow"`
LockFile string `yaml:"-"`
ClearCfFile string `yaml:"-"`
PreCheckDone bool `yaml:"preCheckDone,omitempty,flow"`
}
// SystemUsage is used to include additional information into the telemetry payload
type SystemUsage struct {
InstallModel SystemInstall `yaml:",inline"`
Hypervisor string `yaml:"hypervisor,omitempty,flow"`
}
// InstallHook is a commands to be executed in a given point of the install process
type InstallHook struct {
Chroot bool `yaml:"chroot,omitempty,flow"`
Cmd string `yaml:"cmd,omitempty,flow"`
}
// StorageAlias is used to expand variables in the targetMedia definitions
// a partition's block device name attribute could be declared in the form of:
// Name: ${alias}p1
// where ${alias} was previously declared pointing to a block device file such as:
// block-devices : [
// {name: "alias", file: "/dev/nvme0n1"}
// ]
type StorageAlias struct {
Name string `yaml:"name,omitempty,flow"`
File string `yaml:"file,omitempty,flow"`
DeviceFile bool `yaml:"devicefile,omitempty,flow"`
}
// ClearInstallSelected clears the map of Installation Selected targets
func (si *SystemInstall) ClearInstallSelected() {
si.InstallSelected = map[string]storage.InstallTarget{}
}
// ClearExtraKernelArguments clears all of the of custom extra kernel arguments
func (si *SystemInstall) ClearExtraKernelArguments() {
if si.KernelArguments == nil {
si.KernelArguments = &kernel.Arguments{}
}
si.KernelArguments.Add = []string{}
}
// AddExtraKernelArguments adds a set of custom extra kernel arguments to be added to the
// clr-boot-manager configuration
func (si *SystemInstall) AddExtraKernelArguments(args []string) {
if si.KernelArguments == nil {
si.KernelArguments = &kernel.Arguments{}
}
for _, curr := range args {
if utils.StringSliceContains(si.KernelArguments.Add, curr) {
continue
}
si.KernelArguments.Add = append(si.KernelArguments.Add, curr)
}
}
// ClearRemoveKernelArguments clears all of the of custom remove kernel arguments
func (si *SystemInstall) ClearRemoveKernelArguments() {
if si.KernelArguments == nil {
si.KernelArguments = &kernel.Arguments{}
}
si.KernelArguments.Remove = []string{}
}
// RemoveKernelArguments adds a set of kernel arguments to be "black listed" on
// clear-boot-manager, meaning these arguments will never end up in the boot manager
// entry configuration
func (si *SystemInstall) RemoveKernelArguments(args []string) {
if si.KernelArguments == nil {
si.KernelArguments = &kernel.Arguments{}
}
for _, curr := range args {
if utils.StringSliceContains(si.KernelArguments.Remove, curr) {
continue
}
si.KernelArguments.Remove = append(si.KernelArguments.Remove, curr)
}
}
// ContainsBundle returns true if the data model has a bundle and false otherwise
func (si *SystemInstall) ContainsBundle(bundle string) bool {
for _, curr := range si.Bundles {
if curr == bundle {
return true
}
}
return false
}
// RemoveBundle removes a bundle from the data model
func (si *SystemInstall) RemoveBundle(bundle string) {
bundles := []string{}
for _, curr := range si.Bundles {
if curr != bundle {
bundles = append(bundles, curr)
}
}
si.Bundles = bundles
}
// AddBundle adds a new bundle to the data model, we make sure to not duplicate entries
func (si *SystemInstall) AddBundle(bundle string) {
for _, curr := range si.Bundles {
if curr == bundle {
return
}
}
si.Bundles = append(si.Bundles, bundle)
}
// ContainsUserBundle returns true if the data model has a user bundle and false otherwise
func (si *SystemInstall) ContainsUserBundle(bundle string) bool {
for _, curr := range si.UserBundles {
if curr == bundle {
return true
}
}
return false
}
// RemoveUserBundle removes a user bundle from the data model
func (si *SystemInstall) RemoveUserBundle(bundle string) {
bundles := []string{}
for _, curr := range si.UserBundles {
if curr != bundle {
bundles = append(bundles, curr)
}
}
si.UserBundles = bundles
}
// AddUserBundle adds a new user bundle to the data model, we make sure to not duplicate entries
func (si *SystemInstall) AddUserBundle(bundle string) {
for _, curr := range si.UserBundles {
if curr == bundle {
return
}
}
si.UserBundles = append([]string{bundle}, si.UserBundles...)
}
// RemoveAllUsers remove from the data model all previously added user
func (si *SystemInstall) RemoveAllUsers() {
si.Users = []*user.User{}
}
// AddUser adds a new user to the data model, this function also prevents duplicate entries
func (si *SystemInstall) AddUser(usr *user.User) {
for _, curr := range si.Users {
if curr.Equals(usr) {
return
}
}
si.Users = append(si.Users, usr)
}
// EncryptionRequiresPassphrase checks all partition to see if encryption was enabled
func (si *SystemInstall) EncryptionRequiresPassphrase() bool {
enabled := false
for _, curr := range si.TargetMedias {
enabled = enabled || curr.EncryptionRequiresPassphrase()
}
return enabled
}
// Validate checks the model for possible inconsistencies or "minimum required"
// information
func (si *SystemInstall) Validate() error {
// si will be nil if we fail to unmarshall (coverage tests has a case for that)
if si == nil {
return errors.ValidationErrorf("model is nil")
}
if si.TargetMedias == nil || len(si.TargetMedias) == 0 {
return errors.ValidationErrorf("System Installation must provide a target media")
}
if err := storage.Validate(si.TargetMedias, si.LegacyBios, si.CryptPass); err != nil {
return err
}
if si.Timezone == nil {
return errors.ValidationErrorf("Timezone not set")
}
if si.Keyboard == nil {
return errors.ValidationErrorf("Keyboard not set")
}
if si.Language == nil {
return errors.ValidationErrorf("System Language not set")
}
if si.Telemetry == nil {
return errors.ValidationErrorf("Telemetry not acknowledged")
}
if si.Kernel == nil {
return errors.ValidationErrorf("A kernel must be provided")
}
return nil
}
// AddTargetMedia adds a BlockDevice instance to the list of TargetMedias
// if bd was previously added to as a target media its pointer is updated
func (si *SystemInstall) AddTargetMedia(bd *storage.BlockDevice) {
if si.TargetMedias == nil {
si.TargetMedias = []*storage.BlockDevice{}
}
nList := []*storage.BlockDevice{bd}
for _, curr := range si.TargetMedias {
if !bd.Equals(curr) {
nList = append(nList, curr)
}
}
si.TargetMedias = nList
}
// AddNetworkInterface adds an Interface instance to the list of NetworkInterfaces
func (si *SystemInstall) AddNetworkInterface(iface *network.Interface) {
if si.NetworkInterfaces == nil {
si.NetworkInterfaces = []*network.Interface{}
}
si.NetworkInterfaces = append(si.NetworkInterfaces, iface)
}
// LoadFile loads a model from a yaml file pointed by path
func LoadFile(path string, options args.Args) (*SystemInstall, error) {
var result SystemInstall
// Default to archiving by default
result.PostArchive = true
// Default to Auto Updating enabled by default
result.AutoUpdate = true
if _, err := os.Stat(path); err == nil {
configStr, err := ioutil.ReadFile(path)
if err != nil {
return nil, errors.Wrap(err)
}
err = yaml.Unmarshal(configStr, &result)
if err != nil {
return nil, errors.Wrap(err)
}
}
// Set default Timezone if not defined
if result.Timezone == nil {
result.Timezone = &timezone.TimeZone{Code: timezone.DefaultTimezone}
}
// Set default Keyboard if not defined
if result.Keyboard == nil {
result.Keyboard = &keyboard.Keymap{Code: keyboard.DefaultKeyboard}
}
// Set default Language if not defined
if result.Language == nil {
result.Language = &language.Language{Code: language.DefaultLanguage}
}
// Running in VirtualBox force the default to 'kernel-lts' if
// we are using the system default configuration file
// See https://github.com/clearlinux/clr-installer/issues/203
if options.ConfigFile == "" && utils.IsVirtualBox() {
result.Kernel = &kernel.Kernel{Bundle: "kernel-lts"}
}
tmp := map[string]*StorageAlias{}
for _, bds := range result.StorageAlias {
tmp[bds.Name] = bds
}
for _, bds := range options.BlockDevices {
var tks []string
if tks = strings.Split(bds, ":"); len(tks) < 2 {
continue
}
tmp[tks[0]] = &StorageAlias{Name: tks[0], File: tks[1]}
}
result.StorageAlias = []*StorageAlias{}
for _, bds := range tmp {
result.StorageAlias = append(result.StorageAlias, bds)
}
if len(result.StorageAlias) > 0 {
alias := map[string]string{}
keepMe := []*StorageAlias{}
for _, curr := range result.StorageAlias {
if !isAliasInUse(result.TargetMedias, curr) {
continue
}
fi, err := os.Lstat(curr.File)
inTestAlias := isTestAlias(curr.File)
// could be an image file to be created so we fail only if the error doesn't
// indicate the image file doesn't exist
if err != nil && !inTestAlias && !os.IsNotExist(err) {
return nil, errors.Wrap(err)
}
keepMe = append(keepMe, curr)
if !inTestAlias && os.IsNotExist(err) {
continue
}
if (fi != nil && fi.Mode()&os.ModeDevice == 0) && !inTestAlias {
continue
}
curr.DeviceFile = true
alias[curr.Name] = filepath.Base(curr.File)
}
// keep only the aliases we're using
result.StorageAlias = keepMe
for _, bd := range result.TargetMedias {
bd.ExpandName(alias)
}
}
if result.Version > 0 {
result.AutoUpdate = false
}
return &result, nil
}
func isAliasInUse(bds []*storage.BlockDevice, alias *StorageAlias) bool {
for _, curr := range bds {
rep := fmt.Sprintf("${%s}", alias.Name)
if strings.Contains(curr.Name, rep) {
return true
}
if isAliasInUse(curr.Children, alias) {
return true
}
}
return false
}
func isTestAlias(file string) bool {
if len(testAlias) == 0 {
return false
}
return utils.StringSliceContains(testAlias, file)
}
// EnableTelemetry operates on the telemetry flag and enables or disables the target
// systems telemetry support based in enable argument
func (si *SystemInstall) EnableTelemetry(enable bool) {
if si.Telemetry == nil {
si.Telemetry = &telemetry.Telemetry{}
}
si.Telemetry.SetEnable(enable)
}
// IsTelemetryEnabled returns true if telemetry is enabled, false otherwise
func (si *SystemInstall) IsTelemetryEnabled() bool {
if si.Telemetry == nil {
return false
}
return si.Telemetry.Enabled
}
// WriteFile writes a yaml formatted representation of si into the provided file path
func (si *SystemInstall) WriteFile(path string) error {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()
b, err := yaml.Marshal(si)
if err != nil {
return err
}
// Write our header
_, err = f.WriteString("#clear-linux-config\n")
if err != nil {
return err
}
// Write our version
_, err = f.WriteString("#generated by clr-installer:" + Version + "\n")
if err != nil {
return err
}
// Write datetime stamp
t := time.Now().UTC()
_, err = f.WriteString("#generated on: " + fmt.Sprintf("%d-%02d-%02d_%02d:%02d:%02d_UTC\n",
t.Year(), t.Month(), t.Day(),
t.Hour(), t.Minute(), t.Second()))
if err != nil {
return err
}
_, err = f.Write(b)
if err != nil {
return err
}
return nil
}
// WriteScrubModelTargetMedias writes out a copy the model with the
// TargetMedias removed to a temporary file
func (si *SystemInstall) WriteScrubModelTargetMedias() (string, error) {
// Sanitized the model to remove media
var cleanModel SystemInstall
// Marshal current into bytes
confBytes, bytesErr := yaml.Marshal(si)
if bytesErr != nil {
return "", errors.Wrap(bytesErr)
}
// Unmarshal into a copy
if yamlErr := yaml.Unmarshal(confBytes, &cleanModel); yamlErr != nil {
return "", errors.Wrap(bytesErr)
}
// Sanitize the config data to remove any potential
// Remove the target media
cleanModel.TargetMedias = nil
tmpYaml, err := ioutil.TempFile("", "clr-installer-noMedia-*.yaml")
if err != nil {
return "", errors.Errorf("Could not make YAML tempfile: %v", err)
}
if saveErr := cleanModel.WriteFile(tmpYaml.Name()); saveErr != nil {
return "", errors.Errorf("Could not save config to %s", tmpYaml.Name())
}
return tmpYaml.Name(), nil
}