spiegel_podman/pkg/machine/fcos.go
Brent Baude ebb45b5bdd machine refactoring preparations for hyperv
before we can support hyperv as a virtualization option for podman
machine, several areas in machine will require cleanup.  this is the
first pass of these changes to keep the review burden low.  changes
include:

  * convert artifact, format (image format) and compression to enums
    with string methods
  * rename Provider interface to VirtProvider
  * change Provider implementation in QEMU to QEMUVirt
  * change Provider implementation in WSL to WSLVirt

as mentioned earlier, there will be several more of these refactoring
PRs because assumptions were made about associations of platforms and
virt providers as well as compression and image formats.

Signed-off-by: Brent Baude <bbaude@redhat.com>
2023-02-20 13:43:49 -06:00

300 lines
6.9 KiB
Go

//go:build amd64 || arm64
// +build amd64 arm64
package machine
import (
"encoding/json"
"fmt"
"io"
"net/http"
url2 "net/url"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/coreos/stream-metadata-go/fedoracoreos"
"github.com/coreos/stream-metadata-go/release"
"github.com/coreos/stream-metadata-go/stream"
digest "github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
)
type imageCompression int64
type artifact int64
type imageFormat int64
const (
// Used for testing the latest podman in fcos
// special builds
podmanTesting = "podman-testing"
PodmanTestingHost = "fedorapeople.org"
PodmanTestingURL = "groups/podman/testing"
Xz imageCompression = iota
Zip
Gz
Bz2
Qemu artifact = iota
HyperV
None
qcow imageFormat = iota
vhdx
Tar
)
//
// TODO artifact, imageformat, and imagecompression should be probably combined into some sort
// of object which can "produce" the correct output we are looking for bc things like
// image format contain both the image type AND the compression. This work can be done before
// or after the hyperv work. For now, my preference is to NOT change things and just get things
// typed strongly
//
func (a artifact) String() string {
if a == HyperV {
return "hyperv"
}
return "qemu"
}
func (imf imageFormat) String() string {
switch imf {
case vhdx:
return "vhdx.zip"
case Tar:
return "tar.xz"
}
return "qcow2.xz"
}
func (c imageCompression) String() string {
switch c {
case Gz:
return "gz"
case Zip:
return "zip"
case Bz2:
return "bz2"
}
return "xz"
}
func compressionFromFile(path string) imageCompression {
switch {
case strings.HasSuffix(path, Bz2.String()):
return Bz2
case strings.HasSuffix(path, Gz.String()):
return Gz
case strings.HasSuffix(path, Zip.String()):
return Zip
}
return Xz
}
type FcosDownload struct {
Download
}
func NewFcosDownloader(vmType, vmName, imageStream string) (DistributionDownload, error) {
info, err := GetFCOSDownload(imageStream, Xz)
if err != nil {
return nil, err
}
urlSplit := strings.Split(info.Location, "/")
imageName := urlSplit[len(urlSplit)-1]
url, err := url2.Parse(info.Location)
if err != nil {
return nil, err
}
cacheDir, err := GetCacheDir(vmType)
if err != nil {
return nil, err
}
fcd := FcosDownload{
Download: Download{
Arch: GetFcosArch(),
Artifact: Qemu,
CacheDir: cacheDir,
Format: qcow,
ImageName: imageName,
LocalPath: filepath.Join(cacheDir, imageName),
Sha256sum: info.Sha256Sum,
URL: url,
VMName: vmName,
},
}
dataDir, err := GetDataDir(vmType)
if err != nil {
return nil, err
}
fcd.Download.LocalUncompressedFile = fcd.GetLocalUncompressedFile(dataDir)
return fcd, nil
}
func (f FcosDownload) Get() *Download {
return &f.Download
}
type FcosDownloadInfo struct {
CompressionType string
Location string
Release string
Sha256Sum string
}
func (f FcosDownload) HasUsableCache() (bool, error) {
// check the sha of the local image if it exists
// get the sha of the remote image
// == do not bother to pull
if _, err := os.Stat(f.LocalPath); os.IsNotExist(err) {
return false, nil
}
fd, err := os.Open(f.LocalPath)
if err != nil {
return false, err
}
defer func() {
if err := fd.Close(); err != nil {
logrus.Error(err)
}
}()
sum, err := digest.SHA256.FromReader(fd)
if err != nil {
return false, err
}
return sum.Encoded() == f.Sha256sum, nil
}
func (f FcosDownload) CleanCache() error {
// Set cached image to expire after 2 weeks
// FCOS refreshes around every 2 weeks, assume old images aren't needed
expire := 14 * 24 * time.Hour
return RemoveImageAfterExpire(f.CacheDir, expire)
}
func GetFcosArch() string {
var arch string
// TODO fill in more architectures
switch runtime.GOARCH {
case "arm64":
arch = "aarch64"
default:
arch = "x86_64"
}
return arch
}
// getStreamURL is a wrapper for the fcos.GetStream URL
// so that we can inject a special stream and url for
// testing podman before it merges into fcos builds
func getStreamURL(streamType string) url2.URL {
// For the podmanTesting stream type, we point to
// a custom url on fedorapeople.org
if streamType == podmanTesting {
return url2.URL{
Scheme: "https",
Host: PodmanTestingHost,
Path: fmt.Sprintf("%s/%s.json", PodmanTestingURL, "podman4"),
}
}
return fedoracoreos.GetStreamURL(streamType)
}
// This should get Exported and stay put as it will apply to all fcos downloads
// getFCOS parses fedoraCoreOS's stream and returns the image download URL and the release version
func GetFCOSDownload(imageStream string, compression imageCompression) (*FcosDownloadInfo, error) {
var (
fcosstable stream.Stream
altMeta release.Release
streamType string
)
switch imageStream {
case "podman-testing":
streamType = "podman-testing"
case "testing", "":
streamType = fedoracoreos.StreamTesting
case "next":
streamType = fedoracoreos.StreamNext
case "stable":
streamType = fedoracoreos.StreamStable
default:
return nil, fmt.Errorf("invalid stream %s: valid streams are `testing` and `stable`", imageStream)
}
streamurl := getStreamURL(streamType)
resp, err := http.Get(streamurl.String())
if err != nil {
return nil, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
logrus.Error(err)
}
}()
if imageStream == podmanTesting {
if err := json.Unmarshal(body, &altMeta); err != nil {
return nil, err
}
arches, ok := altMeta.Architectures[GetFcosArch()]
if !ok {
return nil, fmt.Errorf("unable to pull VM image: no targetArch in stream")
}
qcow2, ok := arches.Media.Qemu.Artifacts[qcow.String()]
if !ok {
return nil, fmt.Errorf("unable to pull VM image: no qcow2.xz format in stream")
}
disk := qcow2.Disk
return &FcosDownloadInfo{
Location: disk.Location,
Sha256Sum: disk.Sha256,
CompressionType: compression.String(),
}, nil
}
if err := json.Unmarshal(body, &fcosstable); err != nil {
return nil, err
}
arch, ok := fcosstable.Architectures[GetFcosArch()]
if !ok {
return nil, fmt.Errorf("unable to pull VM image: no targetArch in stream")
}
artifacts := arch.Artifacts
if artifacts == nil {
return nil, fmt.Errorf("unable to pull VM image: no artifact in stream")
}
qemu, ok := artifacts[Qemu.String()]
if !ok {
return nil, fmt.Errorf("unable to pull VM image: no qemu artifact in stream")
}
formats := qemu.Formats
if formats == nil {
return nil, fmt.Errorf("unable to pull VM image: no formats in stream")
}
qcow, ok := formats[qcow.String()]
if !ok {
return nil, fmt.Errorf("unable to pull VM image: no qcow2.xz format in stream")
}
disk := qcow.Disk
if disk == nil {
return nil, fmt.Errorf("unable to pull VM image: no disk in stream")
}
return &FcosDownloadInfo{
Location: disk.Location,
Release: qemu.Release,
Sha256Sum: disk.Sha256,
CompressionType: compression.String(),
}, nil
}