mirror of
https://github.com/clearlinux/clr-installer.git
synced 2026-09-05 21:21:39 +00:00
Fixes #10 Enable encrypted partitions for all but /boot. A single passphrase is used for all partitions which enable encryption expect swap, which currently uses a random password each boot. NOTE: If we need to support a large swap for laptop suspend/restore, we should change from a random passphrase to the same known passphrase. /etc/crypttab will attempt to use either the disk label or the UUID to identify the disk instead of the physical disk name. Swap partition would normally loose its UUID or Label each boot due to dm-crypt and mkswap overwriting the connect. To maintain identification of the partition, we use a trick from here: https://wiki.archlinux.org/index.php/Dm-crypt/Swap_encryption of creating a small (1M) ext2 partition with label at the beginning of the swap partition, then use an offset in the /etc/crypttab to prevent overwriting this information. Signed-off-by: Mark D Horn <mark.d.horn@intel.com>
436 lines
12 KiB
Go
436 lines
12 KiB
Go
// Copyright © 2018 Intel Corporation
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"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"
|
|
)
|
|
|
|
// 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 {
|
|
TargetMedias []*storage.BlockDevice `yaml:"targetMedia"`
|
|
NetworkInterfaces []*network.Interface `yaml:"networkInterfaces"`
|
|
Keyboard *keyboard.Keymap `yaml:"keyboard,omitempty,flow"`
|
|
Language *language.Language `yaml:"language,omitempty,flow"`
|
|
Bundles []string `yaml:"bundles,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"`
|
|
Environment map[string]string `yaml:"env,omitempty,flow"`
|
|
CryptPass string `yaml:"-"`
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
_, err = f.Write(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|