Merge pull request #203 from gibmat/flasher-tool-cdn

Update flasher tool to use Linux Containers CDN
This commit is contained in:
Stéphane Graber
2025-07-25 10:05:41 -04:00
committed by GitHub
7 changed files with 108 additions and 98 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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).