mirror of
https://github.com/outbackdingo/incus-os.git
synced 2026-01-27 10:19:24 +00:00
Merge pull request #203 from gibmat/flasher-tool-cdn
Update flasher tool to use Linux Containers CDN
This commit is contained in:
@@ -10,9 +10,11 @@ page.
|
||||
./flasher-tool
|
||||
|
||||
You will first be prompted for the image format you want to use, either iso
|
||||
(default) or raw image (img).
|
||||
(default) or raw image (img). Note that the iso isn't a hybrid image; if you
|
||||
want to boot from a USB stick you should choose the img format.
|
||||
|
||||
The flasher tool will then connect to GitHub and download the latest release.
|
||||
The flasher tool will then connect to the Linux Containers CDN and download the
|
||||
latest release.
|
||||
|
||||
After downloading, you will be presented with an interactive menu you can use to
|
||||
customize the install options.
|
||||
@@ -26,11 +28,11 @@ Three special environment variables are recognized by the flasher tool, which ca
|
||||
used to provide defaults:
|
||||
|
||||
* `INCUSOS_IMAGE`: Specifies a local Incus OS install image to work with, and will
|
||||
disable checking GitHub for a newer version.
|
||||
disable checking the Linux Containers CDN for a newer version.
|
||||
|
||||
* `INCUSOS_IMAGE_FORMAT`: When downloading from GitHub, specifies the Incus OS
|
||||
install image format (`iso` or `img`) to fetch, and will disable prompting the
|
||||
user for this information.
|
||||
* `INCUSOS_IMAGE_FORMAT`: When downloading from the Linux Containers CDN, specifies
|
||||
the Incus OS install image format (`iso` or `img`) to fetch, and will disable
|
||||
prompting the user for this information.
|
||||
|
||||
* `INCUSOS_SEED_TAR`: Specifies a user-created [install seed](install-seed.md)
|
||||
archive to write to the install image. Disables all prompting of the user, and is
|
||||
|
||||
@@ -5,25 +5,23 @@ import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
ghapi "github.com/google/go-github/v72/github"
|
||||
"github.com/lxc/incus/v6/shared/ask"
|
||||
"github.com/lxc/incus/v6/shared/revert"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
apiseed "github.com/lxc/incus-os/incus-osd/api/seed"
|
||||
"github.com/lxc/incus-os/incus-osd/internal/providers"
|
||||
"github.com/lxc/incus-os/incus-osd/internal/seed"
|
||||
"github.com/lxc/incus-os/incus-osd/internal/systemd"
|
||||
)
|
||||
@@ -48,7 +46,7 @@ func main() {
|
||||
// Determine what image we should modify.
|
||||
imageFilename := os.Getenv("INCUSOS_IMAGE")
|
||||
if imageFilename == "" {
|
||||
slog.InfoContext(ctx, "Fetching latest release from GitHub")
|
||||
slog.InfoContext(ctx, "Fetching latest release from the Linux Containers CDN")
|
||||
|
||||
imageFilename, err = downloadCurrentIncusOSRelease(ctx, asker)
|
||||
if err != nil {
|
||||
@@ -468,9 +466,10 @@ func injectSeedIntoImage(imageFilename string, data []byte) error {
|
||||
}
|
||||
|
||||
func downloadCurrentIncusOSRelease(ctx context.Context, asker ask.Asker) (string, error) {
|
||||
gh := ghapi.NewClient(nil)
|
||||
|
||||
var err error
|
||||
provider, err := providers.Load(ctx, nil, "images", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
imageFormat := os.Getenv("INCUSOS_IMAGE_FORMAT")
|
||||
|
||||
@@ -482,83 +481,15 @@ func downloadCurrentIncusOSRelease(ctx context.Context, asker ask.Asker) (string
|
||||
}
|
||||
|
||||
// Get the latest release.
|
||||
release, _, err := gh.Repositories.GetLatestRelease(ctx, "lxc", "incus-os")
|
||||
release, err := provider.GetOSUpdate(ctx, "IncusOS")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get assets from the latest release.
|
||||
assets, _, err := gh.Repositories.ListReleaseAssets(ctx, "lxc", "incus-os", release.GetID(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get the asset ID for the image.
|
||||
var filename string
|
||||
|
||||
var assetID int64
|
||||
|
||||
for _, a := range assets {
|
||||
if strings.HasSuffix(*a.BrowserDownloadURL, "."+imageFormat+".gz") {
|
||||
filename = strings.TrimSuffix(*a.Name, ".gz")
|
||||
assetID = *a.ID
|
||||
}
|
||||
}
|
||||
|
||||
if assetID == 0 {
|
||||
return "", fmt.Errorf("failed to get IncusOS %s asset ID for release '%s'", imageFormat, release.GetName())
|
||||
}
|
||||
|
||||
// Check if the latest image already exists locally.
|
||||
_, err = os.Stat(filename)
|
||||
if err == nil {
|
||||
slog.InfoContext(ctx, "Latest image already exists, skipping download")
|
||||
|
||||
return filename, nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "Downloading and decompressing image '"+filename+"' from GitHub")
|
||||
slog.InfoContext(ctx, "Downloading and decompressing IncusOS image ("+imageFormat+") version "+release.Version()+" from Linux Containers CDN")
|
||||
|
||||
// Download and decompress the image.
|
||||
rc, _, err := gh.Repositories.DownloadReleaseAsset(ctx, "lxc", "incus-os", assetID, http.DefaultClient)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer rc.Close()
|
||||
|
||||
// Setup a gzip reader to decompress during streaming.
|
||||
body, err := gzip.NewReader(rc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer body.Close()
|
||||
|
||||
// Create the target path.
|
||||
// #nosec G304
|
||||
fd, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer fd.Close()
|
||||
|
||||
// Read from the decompressor in chunks to avoid excessive memory consumption.
|
||||
for {
|
||||
_, err = io.CopyN(fd, body, 4*1024*1024)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
return release.DownloadImage(ctx, imageFormat, "IncusOS", ".", nil)
|
||||
}
|
||||
|
||||
// Spawn the editor with a temporary YAML file for editing configs.
|
||||
|
||||
@@ -665,7 +665,7 @@ func checkDoOSUpdate(ctx context.Context, s *state.State, t *tui.TUI, p provider
|
||||
slog.InfoContext(ctx, "Downloading OS update", "release", update.Version())
|
||||
modal.Update("Downloading " + s.OS.Name + " update version " + update.Version())
|
||||
|
||||
err := update.Download(ctx, s.OS.Name, systemd.SystemUpdatesPath, modal.UpdateProgress)
|
||||
err := update.DownloadUpdate(ctx, s.OS.Name, systemd.SystemUpdatesPath, modal.UpdateProgress)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -406,15 +406,15 @@ func (o *imagesOSUpdate) IsNewerThan(otherVersion string) bool {
|
||||
return datetimeComparison(o.version, otherVersion)
|
||||
}
|
||||
|
||||
func (o *imagesOSUpdate) Download(ctx context.Context, osName string, target string, progressFunc func(float64)) error {
|
||||
func (o *imagesOSUpdate) DownloadUpdate(ctx context.Context, osName string, targetPath string, progressFunc func(float64)) error {
|
||||
// Clear the target path.
|
||||
err := os.RemoveAll(target)
|
||||
err := os.RemoveAll(targetPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the target path.
|
||||
err = os.MkdirAll(target, 0o700)
|
||||
err = os.MkdirAll(targetPath, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -439,7 +439,7 @@ func (o *imagesOSUpdate) Download(ctx context.Context, osName string, target str
|
||||
}
|
||||
|
||||
// Download the actual update.
|
||||
err = o.provider.downloadAsset(ctx, asset, filepath.Join(target, strings.TrimSuffix(fileName, ".gz")), progressFunc)
|
||||
err = o.provider.downloadAsset(ctx, asset, filepath.Join(targetPath, strings.TrimSuffix(fileName, ".gz")), progressFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -448,6 +448,41 @@ func (o *imagesOSUpdate) Download(ctx context.Context, osName string, target str
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *imagesOSUpdate) DownloadImage(ctx context.Context, imageType string, osName string, targetPath string, progressFunc func(float64)) (string, error) {
|
||||
// Create the target path.
|
||||
err := os.MkdirAll(targetPath, 0o700)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, asset := range o.assets {
|
||||
fileName := filepath.Base(asset)
|
||||
|
||||
// Only select OS files.
|
||||
if !strings.HasPrefix(fileName, osName+"_") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the file names.
|
||||
fields := strings.SplitN(fileName, ".", 2)
|
||||
if len(fields) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Continue if not the full image we're looking for.
|
||||
if fields[1] != imageType+".gz" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Download the image.
|
||||
err = o.provider.downloadAsset(ctx, asset, filepath.Join(targetPath, strings.TrimSuffix(fileName, ".gz")), progressFunc)
|
||||
|
||||
return strings.TrimSuffix(fileName, ".gz"), err
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to download image type '%s' for %s release %s", imageType, osName, o.version)
|
||||
}
|
||||
|
||||
// Secure Boot key updates from the GitHub provider.
|
||||
type imagesSecureBootCertUpdate struct {
|
||||
url string
|
||||
|
||||
@@ -298,15 +298,15 @@ func (o *localOSUpdate) IsNewerThan(otherVersion string) bool {
|
||||
return datetimeComparison(o.version, otherVersion)
|
||||
}
|
||||
|
||||
func (o *localOSUpdate) Download(ctx context.Context, osName string, target string, progressFunc func(float64)) error {
|
||||
func (o *localOSUpdate) DownloadUpdate(ctx context.Context, osName string, targetPath string, progressFunc func(float64)) error {
|
||||
// Clear the path.
|
||||
err := os.RemoveAll(target)
|
||||
err := os.RemoveAll(targetPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the target path.
|
||||
err = os.MkdirAll(target, 0o700)
|
||||
err = os.MkdirAll(targetPath, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -329,7 +329,7 @@ func (o *localOSUpdate) Download(ctx context.Context, osName string, target stri
|
||||
}
|
||||
|
||||
// Download the actual update.
|
||||
err = o.provider.copyAsset(ctx, filepath.Base(asset), target, progressFunc)
|
||||
err = o.provider.copyAsset(ctx, filepath.Base(asset), targetPath, progressFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -338,6 +338,11 @@ func (o *localOSUpdate) Download(ctx context.Context, osName string, target stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*localOSUpdate) DownloadImage(_ context.Context, _ string, _ string, _ string, _ func(float64)) (string, error) {
|
||||
// No reason to support fetching a full install image from the local (development) provider.
|
||||
return "", errors.New("downloading full image not supported by local provider")
|
||||
}
|
||||
|
||||
// Secure Boot key updates from the Local provider.
|
||||
type localSecureBootCertUpdate struct {
|
||||
provider *local
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -555,15 +556,15 @@ func (o *operationsCenterOSUpdate) IsNewerThan(otherVersion string) bool {
|
||||
return datetimeComparison(o.version, otherVersion)
|
||||
}
|
||||
|
||||
func (o *operationsCenterOSUpdate) Download(ctx context.Context, osName string, target string, progressFunc func(float64)) error {
|
||||
func (o *operationsCenterOSUpdate) DownloadUpdate(ctx context.Context, osName string, targetPath string, progressFunc func(float64)) error {
|
||||
// Clear the target path.
|
||||
err := os.RemoveAll(target)
|
||||
err := os.RemoveAll(targetPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the target path.
|
||||
err = os.MkdirAll(target, 0o700)
|
||||
err = os.MkdirAll(targetPath, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -588,7 +589,7 @@ func (o *operationsCenterOSUpdate) Download(ctx context.Context, osName string,
|
||||
}
|
||||
|
||||
// Download the actual update.
|
||||
err = o.provider.downloadAsset(ctx, asset, filepath.Join(target, strings.TrimSuffix(fileName, ".gz")), progressFunc)
|
||||
err = o.provider.downloadAsset(ctx, asset, filepath.Join(targetPath, strings.TrimSuffix(fileName, ".gz")), progressFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -596,3 +597,38 @@ func (o *operationsCenterOSUpdate) Download(ctx context.Context, osName string,
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *operationsCenterOSUpdate) DownloadImage(ctx context.Context, imageType string, osName string, targetPath string, progressFunc func(float64)) (string, error) {
|
||||
// Create the target path.
|
||||
err := os.MkdirAll(targetPath, 0o700)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, asset := range o.assets {
|
||||
fileName := filepath.Base(asset)
|
||||
|
||||
// Only select OS files.
|
||||
if !strings.HasPrefix(fileName, osName+"_") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the file names.
|
||||
fields := strings.SplitN(fileName, ".", 2)
|
||||
if len(fields) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Continue if not the full image we're looking for.
|
||||
if fields[1] != imageType+".gz" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Download the image.
|
||||
err = o.provider.downloadAsset(ctx, asset, filepath.Join(targetPath, strings.TrimSuffix(fileName, ".gz")), progressFunc)
|
||||
|
||||
return strings.TrimSuffix(fileName, ".gz"), err
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to download image type '%s' for %s release %s", imageType, osName, o.version)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ type OSUpdate interface {
|
||||
Version() string
|
||||
IsNewerThan(otherVersion string) bool
|
||||
|
||||
Download(ctx context.Context, osName string, targetPath string, progressFunc func(float64)) error
|
||||
DownloadUpdate(ctx context.Context, osName string, targetPath string, progressFunc func(float64)) error
|
||||
DownloadImage(ctx context.Context, imageType string, osName string, targetPath string, progressFunc func(float64)) (string, error)
|
||||
}
|
||||
|
||||
// SecureBootCertUpdate represents a Secure Boot UEFI certificate update (typically a db or dbx addition).
|
||||
|
||||
Reference in New Issue
Block a user