Files
clr-installer/model/model.go
T
Mark D Horn d96230f8d9 tui: gui: Do not display version in Demo mode
For documentation purposes (screen shots), we do not want to
include the version string. Having a version string in the image
requires updating all image each release.

Signed-off-by: Mark D Horn <mark.d.horn@intel.com>
2019-05-08 16:04:27 -07:00

504 lines
14 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 {
// Even though TargetMedias is an array, we currently only support
// it containing a single BlockDevice which matches/maps to the InstallTarget
// TODO: Change InstallSelected to be a map with the key being the
// device name for which it holds InstallTarget information when/if
// we add support for installing across multiple disks.
InstallSelected 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,omitempty,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"`
}
// 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"`
}
// 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)
}
}
// 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")
}
for _, curr := range si.TargetMedias {
if err := curr.Validate(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
}