feat: add osinstall cli utility (#368)
This commit is contained in:
parent
59e8fc2bce
commit
8ee9022b71
@ -25,6 +25,7 @@ policies:
|
||||
- kernel
|
||||
- osctl
|
||||
- osd
|
||||
- osinstall
|
||||
- rootfs
|
||||
- tools
|
||||
- '*'
|
||||
|
12
Dockerfile
12
Dockerfile
@ -307,3 +307,15 @@ ARG APP
|
||||
FROM scratch AS blockd
|
||||
COPY --from=blockd-build /blockd /blockd
|
||||
ENTRYPOINT ["/blockd"]
|
||||
|
||||
# The osinstall target builds the installer binaries.
|
||||
|
||||
FROM base AS osinstall-linux-amd64-build
|
||||
ARG SHA
|
||||
ARG TAG
|
||||
ARG VERSION_PKG="github.com/autonomy/talos/internal/pkg/version"
|
||||
WORKDIR /src/internal/app/osinstall
|
||||
RUN GOOS=linux GOARCH=amd64 go build -a -ldflags "-s -w -X ${VERSION_PKG}.Name=Client -X ${VERSION_PKG}.SHA=${SHA} -X ${VERSION_PKG}.Tag=${TAG}" -o /osinstall-linux-amd64
|
||||
RUN chmod +x /osinstall-linux-amd64
|
||||
FROM scratch AS osinstall-linux-amd64
|
||||
COPY --from=osinstall-linux-amd64-build /osinstall-linux-amd64 /osinstall-linux-amd64
|
||||
|
10
Makefile
10
Makefile
@ -34,7 +34,7 @@ COMMON_ARGS += --frontend-opt build-arg:GOLANG_VERSION=$(GOLANG_VERSION)
|
||||
COMMON_ARGS += --frontend-opt build-arg:SHA=$(SHA)
|
||||
COMMON_ARGS += --frontend-opt build-arg:TAG=$(TAG)
|
||||
|
||||
all: ci kernel initramfs rootfs osctl-linux-amd64 osctl-darwin-amd64 test lint docs installer
|
||||
all: ci kernel initramfs rootfs osctl-linux-amd64 osctl-darwin-amd64 osinstall-linux-amd64 test lint docs installer
|
||||
|
||||
.PHONY: builddeps
|
||||
builddeps: gitmeta buildctl
|
||||
@ -165,6 +165,14 @@ osctl-darwin-amd64:
|
||||
--frontend-opt target=$@ \
|
||||
$(COMMON_ARGS)
|
||||
|
||||
osinstall-linux-amd64:
|
||||
@buildctl --addr $(BUILDKIT_HOST) \
|
||||
build \
|
||||
--exporter=local \
|
||||
--exporter-opt output=build \
|
||||
--frontend-opt target=$@ \
|
||||
$(COMMON_ARGS)
|
||||
|
||||
udevd:
|
||||
@buildctl --addr $(BUILDKIT_HOST) \
|
||||
build \
|
||||
|
@ -96,7 +96,7 @@ func (b *BareMetal) Prepare(data *userdata.UserData) (err error) {
|
||||
|
||||
// Install provides the functionality to install talos by
|
||||
// download the necessary bits and write them to a target device
|
||||
// nolint: gocyclo
|
||||
// nolint: gocyclo, dupl
|
||||
func (b *BareMetal) Install(data *userdata.UserData) error {
|
||||
var err error
|
||||
|
||||
@ -333,6 +333,7 @@ func NewDevice(name string, label string, size uint, force bool, test bool, data
|
||||
}
|
||||
|
||||
// Partition creates a new partition on the specified device
|
||||
// nolint: dupl
|
||||
func (d *Device) Partition() error {
|
||||
var typeID string
|
||||
switch d.Label {
|
||||
@ -449,7 +450,7 @@ func (d *Device) Unmount() error {
|
||||
}
|
||||
|
||||
// Simple extract function
|
||||
// nolint: gocyclo
|
||||
// nolint: gocyclo, dupl
|
||||
func untar(tarball *os.File, dst string) error {
|
||||
|
||||
var input io.Reader
|
||||
|
@ -215,15 +215,18 @@ func (i *Initializer) Switch() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint: dupl
|
||||
func mountpoints() (mountpoints *mount.Points, err error) {
|
||||
mountpoints = mount.NewMountPoints()
|
||||
for _, name := range []string{constants.RootPartitionLabel, constants.DataPartitionLabel} {
|
||||
for _, name := range []string{constants.RootPartitionLabel, constants.DataPartitionLabel, constants.BootPartitionLabel} {
|
||||
var target string
|
||||
switch name {
|
||||
case constants.RootPartitionLabel:
|
||||
target = "/"
|
||||
target = constants.RootMountPoint
|
||||
case constants.DataPartitionLabel:
|
||||
target = "/var"
|
||||
target = constants.DataMountPoint
|
||||
case constants.BootPartitionLabel:
|
||||
target = constants.BootMountPoint
|
||||
}
|
||||
|
||||
var dev *probe.ProbedBlockDevice
|
||||
|
51
internal/app/osinstall/cmd/install.go
Normal file
51
internal/app/osinstall/cmd/install.go
Normal file
@ -0,0 +1,51 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// nolint: dupl,golint
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/autonomy/talos/internal/app/osinstall/internal/userdata"
|
||||
"github.com/autonomy/talos/internal/pkg/install"
|
||||
udata "github.com/autonomy/talos/internal/pkg/userdata"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// installCmd reads in a userData file and attempts to parse it
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install Talos to a specified disk",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var ud *udata.UserData
|
||||
var err error
|
||||
if userdataFile != "" {
|
||||
ud, err = userdata.UserData(userdataFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = install.Prepare(ud)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
err = install.Mount()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = install.Install(ud)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
installCmd.Flags().StringVarP(&userdataFile, "userdata", "u", "", "path or url of userdata file")
|
||||
rootCmd.AddCommand(installCmd)
|
||||
}
|
30
internal/app/osinstall/cmd/root.go
Normal file
30
internal/app/osinstall/cmd/root.go
Normal file
@ -0,0 +1,30 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var userdataFile string
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "osinstall",
|
||||
Short: "A CLI for installing Talos nodes",
|
||||
Long: ``,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
32
internal/app/osinstall/cmd/validate.go
Normal file
32
internal/app/osinstall/cmd/validate.go
Normal file
@ -0,0 +1,32 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// nolint: dupl,golint
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/autonomy/talos/internal/app/osinstall/internal/userdata"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// validateCmd reads in a userData file and attempts to parse it
|
||||
var validateCmd = &cobra.Command{
|
||||
Use: "validate",
|
||||
Short: "Validate userdata",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ud, err := userdata.UserData(userdataFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println(ud)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
validateCmd.Flags().StringVarP(&userdataFile, "userdata", "u", "", "path or url of userdata file")
|
||||
rootCmd.AddCommand(validateCmd)
|
||||
}
|
39
internal/app/osinstall/cmd/version.go
Normal file
39
internal/app/osinstall/cmd/version.go
Normal file
@ -0,0 +1,39 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/autonomy/talos/internal/pkg/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
shortVersion bool
|
||||
)
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Prints the version",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if shortVersion {
|
||||
version.PrintShortVersion()
|
||||
} else {
|
||||
if err := version.PrintLongVersion(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionCmd.Flags().BoolVar(&shortVersion, "short", false, "Print the short version")
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
23
internal/app/osinstall/internal/userdata/userdata.go
Normal file
23
internal/app/osinstall/internal/userdata/userdata.go
Normal file
@ -0,0 +1,23 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ud "github.com/autonomy/talos/internal/pkg/userdata"
|
||||
)
|
||||
|
||||
// UserData provides an abstraction to call the appropriate method to
|
||||
// load user data
|
||||
// TODO: Merge this in to internal/pkg/userdata
|
||||
func UserData(location string) (userData *ud.UserData, err error) {
|
||||
if strings.HasPrefix(location, "http") {
|
||||
userData, err = ud.Download(location, nil)
|
||||
} else {
|
||||
userData, err = ud.Open(location)
|
||||
}
|
||||
return userData, err
|
||||
}
|
11
internal/app/osinstall/main.go
Normal file
11
internal/app/osinstall/main.go
Normal file
@ -0,0 +1,11 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/autonomy/talos/internal/app/osinstall/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
@ -99,3 +99,8 @@ func (bd *BlockDevice) RereadPartitionTable() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Device returns the backing file for the block device
|
||||
func (bd *BlockDevice) Device() *os.File {
|
||||
return bd.f
|
||||
}
|
||||
|
@ -20,14 +20,26 @@ const (
|
||||
// the boot path.
|
||||
BootPartitionLabel = "ESP"
|
||||
|
||||
// BootMountPoint is the label of the partition to use for mounting at
|
||||
// the boot path.
|
||||
BootMountPoint = "/boot"
|
||||
|
||||
// DataPartitionLabel is the label of the partition to use for mounting at
|
||||
// the data path.
|
||||
DataPartitionLabel = "DATA"
|
||||
|
||||
// DataMountPoint is the label of the partition to use for mounting at
|
||||
// the data path.
|
||||
DataMountPoint = "/var"
|
||||
|
||||
// RootPartitionLabel is the label of the partition to use for mounting at
|
||||
// the root path.
|
||||
RootPartitionLabel = "ROOT"
|
||||
|
||||
// RootMountPoint is the label of the partition to use for mounting at
|
||||
// the root path.
|
||||
RootMountPoint = "/"
|
||||
|
||||
// PATH defines all locations where executables are stored.
|
||||
PATH = "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/opt/cni/bin"
|
||||
|
||||
|
228
internal/pkg/install/install.go
Normal file
228
internal/pkg/install/install.go
Normal file
@ -0,0 +1,228 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package install
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/autonomy/talos/internal/pkg/constants"
|
||||
"github.com/autonomy/talos/internal/pkg/userdata"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Install fetches the necessary data locations and copies or extracts
|
||||
// to the target locations
|
||||
// nolint: gocyclo
|
||||
func Install(data *userdata.UserData) (err error) {
|
||||
|
||||
dataURLs := make(map[string][]string)
|
||||
dataURLs[path.Join(constants.NewRoot, constants.RootMountPoint)] = data.Install.Root.Data
|
||||
|
||||
if data.Install.Boot != nil {
|
||||
dataURLs[path.Join(constants.NewRoot, constants.BootMountPoint)] = data.Install.Boot.Data
|
||||
}
|
||||
|
||||
var sourceFile *os.File
|
||||
var destFile *os.File
|
||||
|
||||
var previousMountPoint string
|
||||
for dest, urls := range dataURLs {
|
||||
|
||||
if dest != previousMountPoint {
|
||||
log.Printf("Downloading assets for %s\n", dest)
|
||||
previousMountPoint = dest
|
||||
}
|
||||
|
||||
// Extract artifact if necessary, otherwise place at root of partition/filesystem
|
||||
for _, artifact := range urls {
|
||||
log.Println(artifact)
|
||||
switch {
|
||||
case strings.HasPrefix(artifact, "http"):
|
||||
var u *url.URL
|
||||
log.Printf("Downloading %s\n", artifact)
|
||||
u, err = url.Parse(artifact)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sourceFile, err = download(u, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO add support for checksum validation of downloaded file
|
||||
case strings.HasPrefix(artifact, "/"):
|
||||
sourceFile, err = os.Open(artifact)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destFile, err = os.Create(filepath.Join(dest, filepath.Base(artifact)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// Local directories/links
|
||||
link := strings.Split(artifact, ":")
|
||||
if len(link) == 1 {
|
||||
if err = os.MkdirAll(filepath.Join(dest, artifact), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = os.Symlink(link[1], filepath.Join(dest, link[0])); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(sourceFile.Name(), ".tar") || strings.HasSuffix(sourceFile.Name(), ".tar.gz"):
|
||||
log.Printf("Extracting %s\n", artifact)
|
||||
err = untar(sourceFile, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case strings.HasPrefix(sourceFile.Name(), "/"):
|
||||
if _, err = io.Copy(destFile, sourceFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = destFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
if err = sourceFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Remove(sourceFile.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Simple extract function
|
||||
// nolint: gocyclo, dupl
|
||||
func untar(tarball *os.File, dst string) error {
|
||||
|
||||
var input io.Reader
|
||||
var err error
|
||||
|
||||
if strings.HasSuffix(tarball.Name(), ".tar.gz") {
|
||||
input, err = gzip.NewReader(tarball)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
defer input.(*gzip.Reader).Close()
|
||||
} else {
|
||||
input = tarball
|
||||
}
|
||||
|
||||
tr := tar.NewReader(input)
|
||||
|
||||
for {
|
||||
var header *tar.Header
|
||||
|
||||
header, err = tr.Next()
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
return err
|
||||
case err != nil:
|
||||
return err
|
||||
case header == nil:
|
||||
continue
|
||||
}
|
||||
|
||||
// the target location where the dir/file should be created
|
||||
target := filepath.Join(dst, header.Name)
|
||||
|
||||
// May need to add in support for these
|
||||
/*
|
||||
// Type '1' to '6' are header-only flags and may not have a data body.
|
||||
TypeLink = '1' // Hard link
|
||||
TypeSymlink = '2' // Symbolic link
|
||||
TypeChar = '3' // Character device node
|
||||
TypeBlock = '4' // Block device node
|
||||
TypeDir = '5' // Directory
|
||||
TypeFifo = '6' // FIFO node
|
||||
*/
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err = os.MkdirAll(target, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
var downloadedFileput *os.File
|
||||
|
||||
downloadedFileput, err = os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(downloadedFileput, tr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = downloadedFileput.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeSymlink:
|
||||
dest := filepath.Join(dst, header.Name)
|
||||
source := header.Linkname
|
||||
if err := os.Symlink(source, dest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func download(artifact *url.URL, base string) (*os.File, error) {
|
||||
downloadedFile, err := os.Create(filepath.Join(base, filepath.Base(artifact.Path)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the data
|
||||
resp, err := http.Get(artifact.String())
|
||||
if err != nil {
|
||||
return downloadedFile, err
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
// nolint: errcheck
|
||||
downloadedFile.Close()
|
||||
return nil, errors.Errorf("Failed to download %s, got %d", artifact, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Write the body to file
|
||||
_, err = io.Copy(downloadedFile, resp.Body)
|
||||
if err != nil {
|
||||
return downloadedFile, err
|
||||
}
|
||||
|
||||
// Reset downloadedFile file position to 0 so we can immediately read from it
|
||||
_, err = downloadedFile.Seek(0, 0)
|
||||
|
||||
return downloadedFile, err
|
||||
}
|
68
internal/pkg/install/mount.go
Normal file
68
internal/pkg/install/mount.go
Normal file
@ -0,0 +1,68 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package install
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/autonomy/talos/internal/pkg/blockdevice/probe"
|
||||
"github.com/autonomy/talos/internal/pkg/constants"
|
||||
"github.com/autonomy/talos/internal/pkg/mount"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Mount discovers the appropriate partitions by label and mounts them up
|
||||
// to the appropriate mountpoint.
|
||||
// TODO: See if we can consolidate this with rootfs/mount
|
||||
func Mount() (err error) {
|
||||
log.Println("Discovering mountpoints")
|
||||
|
||||
var mp *mount.Points
|
||||
if mp, err = mountpoints(); err != nil {
|
||||
return errors.Errorf("error initializing block devices: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Attempting to mount filesystems")
|
||||
iter := mp.Iter()
|
||||
for iter.Next() {
|
||||
log.Println("- ", iter.Value())
|
||||
if err = mount.WithRetry(iter.Value(), mount.WithPrefix(constants.NewRoot)); err != nil {
|
||||
return errors.Errorf("error mounting partitions: %v", err)
|
||||
}
|
||||
}
|
||||
if iter.Err() != nil {
|
||||
return iter.Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint: dupl
|
||||
func mountpoints() (mountpoints *mount.Points, err error) {
|
||||
mountpoints = mount.NewMountPoints()
|
||||
for _, name := range []string{constants.RootPartitionLabel, constants.DataPartitionLabel, constants.BootPartitionLabel} {
|
||||
var target string
|
||||
switch name {
|
||||
case constants.RootPartitionLabel:
|
||||
target = constants.RootMountPoint
|
||||
case constants.DataPartitionLabel:
|
||||
target = constants.DataMountPoint
|
||||
case constants.BootPartitionLabel:
|
||||
target = constants.BootMountPoint
|
||||
}
|
||||
|
||||
var dev *probe.ProbedBlockDevice
|
||||
if dev, err = probe.GetDevWithFileSystemLabel(name); err != nil {
|
||||
return nil, errors.Errorf("failed to find device with label %s: %v", name, err)
|
||||
}
|
||||
|
||||
mountpoint := mount.NewMountPoint(dev.Path, target, dev.SuperBlock.Type(), unix.MS_NOATIME, "")
|
||||
|
||||
mountpoints.Set(name, mountpoint)
|
||||
}
|
||||
|
||||
return mountpoints, nil
|
||||
}
|
332
internal/pkg/install/prepare.go
Normal file
332
internal/pkg/install/prepare.go
Normal file
@ -0,0 +1,332 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package install
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/autonomy/talos/internal/pkg/blockdevice"
|
||||
"github.com/autonomy/talos/internal/pkg/blockdevice/filesystem/xfs"
|
||||
"github.com/autonomy/talos/internal/pkg/blockdevice/probe"
|
||||
"github.com/autonomy/talos/internal/pkg/blockdevice/table"
|
||||
"github.com/autonomy/talos/internal/pkg/blockdevice/table/gpt/partition"
|
||||
"github.com/autonomy/talos/internal/pkg/constants"
|
||||
"github.com/autonomy/talos/internal/pkg/userdata"
|
||||
"github.com/autonomy/talos/internal/pkg/version"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Prepare handles setting/consolidating/defaulting userdata pieces specific to
|
||||
// installation
|
||||
// TODO: See if this would be more appropriate in userdata
|
||||
// nolint: dupl, gocyclo
|
||||
func Prepare(data *userdata.UserData) (err error) {
|
||||
log.Println("preparing ")
|
||||
|
||||
// Root Device Init
|
||||
if data.Install.Root.Device == "" {
|
||||
return errors.Errorf("%s", "install.rootdevice is required")
|
||||
}
|
||||
|
||||
if data.Install.Root.Size == 0 {
|
||||
// Set to 1G default for funzies
|
||||
data.Install.Root.Size = 2048 * 1000 * 1000
|
||||
}
|
||||
|
||||
if len(data.Install.Root.Data) == 0 {
|
||||
// Should probably have a canonical location to fetch rootfs - github?/s3?
|
||||
// need to figure out how to download latest instead of hardcoding
|
||||
data.Install.Root.Data = append(data.Install.Root.Data, "https://github.com/autonomy/talos/releases/download/"+version.Tag+"/rootfs.tar.gz")
|
||||
}
|
||||
|
||||
// Data Device Init
|
||||
if data.Install.Data.Device == "" {
|
||||
data.Install.Data.Device = data.Install.Root.Device
|
||||
}
|
||||
|
||||
if data.Install.Data.Size == 0 {
|
||||
// Set to 1G default for funzies
|
||||
data.Install.Data.Size = 1024 * 1000 * 1000
|
||||
}
|
||||
|
||||
// Boot Device Init
|
||||
if data.Install.Boot != nil {
|
||||
if data.Install.Boot.Device == "" {
|
||||
data.Install.Boot.Device = data.Install.Root.Device
|
||||
}
|
||||
if data.Install.Boot.Size == 0 {
|
||||
// Set to 512MB default for funzies
|
||||
data.Install.Boot.Size = 512 * 1000 * 1000
|
||||
}
|
||||
if len(data.Install.Boot.Data) == 0 {
|
||||
data.Install.Boot.Data = append(data.Install.Boot.Data, "https://github.com/autonomy/talos/releases/download/"+version.Tag+"/vmlinuz")
|
||||
data.Install.Boot.Data = append(data.Install.Boot.Data, "https://github.com/autonomy/talos/releases/download/"+version.Tag+"/initramfs.xz")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the disks are unused
|
||||
// Maybe a simple check against bd.UUID is more appropriate?
|
||||
if !data.Install.Wipe {
|
||||
var dev *probe.ProbedBlockDevice
|
||||
for _, device := range []string{data.Install.Boot.Device, data.Install.Root.Device, data.Install.Data.Device} {
|
||||
dev, err = probe.GetDevWithFileSystemLabel(device)
|
||||
if err != nil {
|
||||
// We continue here because we only care if we can discover the
|
||||
// device successfully and confirm that the disk is not in use.
|
||||
// TODO(andrewrynhard): We should return a custom error type here
|
||||
// that we can use to confirm the device was not found.
|
||||
continue
|
||||
}
|
||||
if dev.SuperBlock != nil {
|
||||
return errors.Errorf("target install device %s is not empty, found existing %s file system", device, dev.SuperBlock.Type())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a map of all the devices we need to be concerned with
|
||||
devices := make(map[string]*Device)
|
||||
labeldev := make(map[string]string)
|
||||
|
||||
// PR: Should we only allow boot device creation if data.Install.Wipe?
|
||||
if data.Install.Boot.Device != "" {
|
||||
devices[constants.BootPartitionLabel] = NewDevice(data.Install.Boot.Device,
|
||||
constants.BootPartitionLabel,
|
||||
data.Install.Boot.Size,
|
||||
data.Install.Wipe,
|
||||
false,
|
||||
data.Install.Boot.Data)
|
||||
labeldev[constants.BootPartitionLabel] = data.Install.Boot.Device
|
||||
}
|
||||
|
||||
devices[constants.RootPartitionLabel] = NewDevice(data.Install.Root.Device,
|
||||
constants.RootPartitionLabel,
|
||||
data.Install.Root.Size,
|
||||
data.Install.Wipe,
|
||||
false,
|
||||
data.Install.Root.Data)
|
||||
labeldev[constants.RootPartitionLabel] = data.Install.Root.Device
|
||||
|
||||
devices[constants.DataPartitionLabel] = NewDevice(data.Install.Data.Device,
|
||||
constants.DataPartitionLabel,
|
||||
data.Install.Data.Size,
|
||||
data.Install.Wipe,
|
||||
false,
|
||||
data.Install.Data.Data)
|
||||
|
||||
labeldev[constants.DataPartitionLabel] = data.Install.Data.Device
|
||||
|
||||
if data.Install.Wipe {
|
||||
log.Println("Preparing to zero out devices")
|
||||
var zero *os.File
|
||||
zero, err = os.Open("/dev/zero")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Calculating total disk usage")
|
||||
diskSizes := make(map[string]uint, len(devices))
|
||||
for _, dev := range devices {
|
||||
// Adding 264*512b to cover partition table size
|
||||
// In theory, a GUID Partition Table disk can be up to 264 sectors in a single logical block in length.
|
||||
// Logical blocks are commonly 512 bytes or one sector in size.
|
||||
// TODO verify this against gpt.go
|
||||
diskSizes[dev.Name] += dev.Size + 164010
|
||||
}
|
||||
|
||||
log.Println("Zeroing out each disk")
|
||||
var f *os.File
|
||||
for dev, size := range diskSizes {
|
||||
f, err = os.OpenFile(dev, os.O_RDWR, os.ModeDevice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.CopyN(f, zero, int64(size)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = zero.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Use the below to only open a block device once
|
||||
uniqueDevices := make(map[string]*blockdevice.BlockDevice)
|
||||
|
||||
// Associate block device to a partition table. This allows us to
|
||||
// make use of a single partition table across an entire block device.
|
||||
log.Println("Opening block devices in preparation for partitioning")
|
||||
partitionTables := make(map[*blockdevice.BlockDevice]table.PartitionTable)
|
||||
for label, device := range labeldev {
|
||||
if dev, ok := uniqueDevices[device]; ok {
|
||||
devices[label].BlockDevice = dev
|
||||
devices[label].PartitionTable = partitionTables[dev]
|
||||
continue
|
||||
}
|
||||
|
||||
var bd *blockdevice.BlockDevice
|
||||
|
||||
bd, err = blockdevice.Open(device, blockdevice.WithNewGPT(data.Install.Wipe))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
defer bd.Close()
|
||||
|
||||
var pt table.PartitionTable
|
||||
pt, err = bd.PartitionTable(!data.Install.Wipe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uniqueDevices[device] = bd
|
||||
partitionTables[bd] = pt
|
||||
|
||||
devices[label].BlockDevice = bd
|
||||
devices[label].PartitionTable = pt
|
||||
}
|
||||
|
||||
// devices = Device
|
||||
if data.Install.Wipe {
|
||||
for _, label := range []string{constants.BootPartitionLabel, constants.RootPartitionLabel, constants.DataPartitionLabel} {
|
||||
// Wipe disk
|
||||
// Partition the disk
|
||||
log.Printf("Partitioning %s - %s\n", devices[label].Name, label)
|
||||
err = devices[label].Partition()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Installation/preparation necessary
|
||||
if data.Install != nil {
|
||||
|
||||
// uniqueDevices = blockdevice
|
||||
seen := make(map[string]interface{})
|
||||
for _, dev := range devices {
|
||||
if _, ok := seen[dev.Name]; ok {
|
||||
continue
|
||||
}
|
||||
seen[dev.Name] = nil
|
||||
|
||||
err = dev.PartitionTable.Write()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the device files
|
||||
log.Printf("Reread Partition Table %s\n", dev.Name)
|
||||
if err = dev.BlockDevice.RereadPartitionTable(); err != nil {
|
||||
log.Println("break here?")
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, dev := range devices {
|
||||
// Create the filesystem
|
||||
log.Printf("Formatting Partition %s - %s\n", dev.Name, dev.Label)
|
||||
err = dev.Format()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Device represents a single partition.
|
||||
type Device struct {
|
||||
DataURLs []string
|
||||
Label string
|
||||
MountBase string
|
||||
Name string
|
||||
|
||||
// This seems overkill to save partition table
|
||||
// when we can get partition table from BlockDevice
|
||||
// but we want to have a shared partition table for each
|
||||
// device so we can properly append partitions and have
|
||||
// an atomic write partition operation
|
||||
PartitionTable table.PartitionTable
|
||||
|
||||
// This guy might be overkill but we can clean up later
|
||||
// Made up of Name + part.No(), so maybe it's worth
|
||||
// just storing part.No() and adding a method d.PartName()
|
||||
PartitionName string
|
||||
|
||||
Size uint
|
||||
|
||||
BlockDevice *blockdevice.BlockDevice
|
||||
|
||||
Force bool
|
||||
Test bool
|
||||
}
|
||||
|
||||
// NewDevice creates a Device with basic metadata. BlockDevice and PartitionTable
|
||||
// need to be set outsite of this.
|
||||
func NewDevice(name string, label string, size uint, force bool, test bool, data []string) *Device {
|
||||
return &Device{
|
||||
DataURLs: data,
|
||||
Force: force,
|
||||
Label: label,
|
||||
MountBase: "/tmp",
|
||||
Name: name,
|
||||
Size: size,
|
||||
Test: test,
|
||||
}
|
||||
}
|
||||
|
||||
// Partition creates a new partition on the specified device
|
||||
// nolint: dupl
|
||||
func (d *Device) Partition() error {
|
||||
var typeID string
|
||||
switch d.Label {
|
||||
case constants.BootPartitionLabel:
|
||||
// EFI System Partition
|
||||
typeID = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
|
||||
case constants.RootPartitionLabel:
|
||||
// Root Partition
|
||||
switch runtime.GOARCH {
|
||||
case "386":
|
||||
typeID = "44479540-F297-41B2-9AF7-D131D5F0458A"
|
||||
case "amd64":
|
||||
typeID = "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709"
|
||||
default:
|
||||
return errors.Errorf("%s", "unsupported cpu architecture")
|
||||
}
|
||||
case constants.DataPartitionLabel:
|
||||
// Data Partition
|
||||
typeID = "AF3DC60F-8384-7247-8E79-3D69D8477DE4"
|
||||
default:
|
||||
return errors.Errorf("%s", "unknown partition label")
|
||||
}
|
||||
|
||||
part, err := d.PartitionTable.Add(uint64(d.Size), partition.WithPartitionType(typeID), partition.WithPartitionName(d.Label), partition.WithPartitionTest(d.Test))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.PartitionName = d.Name + strconv.Itoa(int(part.No()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format creates a xfs filesystem on the device/partition
|
||||
func (d *Device) Format() error {
|
||||
return xfs.MakeFS(d.PartitionName, xfs.WithLabel(d.Label), xfs.WithForce(d.Force))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user