245 lines
6.1 KiB
Go
245 lines
6.1 KiB
Go
package libvirt
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"libvirt.org/go/libvirtxml"
|
|
)
|
|
|
|
// network transparent image.
|
|
type image interface {
|
|
Size() (uint64, error)
|
|
Import(func(io.Reader) error, libvirtxml.StorageVolume) error
|
|
String() string
|
|
IsQCOW2() (bool, error)
|
|
}
|
|
|
|
type localImage struct {
|
|
path string
|
|
}
|
|
|
|
func (i *localImage) String() string {
|
|
return i.path
|
|
}
|
|
|
|
func (i *localImage) Size() (uint64, error) {
|
|
file, err := os.Open(i.path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
fi, err := file.Stat()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uint64(fi.Size()), nil
|
|
}
|
|
|
|
//nolint:gomnd
|
|
func (i *localImage) IsQCOW2() (bool, error) {
|
|
file, err := os.Open(i.path)
|
|
defer file.Close()
|
|
if err != nil {
|
|
return false, fmt.Errorf("error while opening %s: %w", i.path, err)
|
|
}
|
|
buf := make([]byte, 8)
|
|
_, err = io.ReadAtLeast(file, buf, 8)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return isQCOW2Header(buf)
|
|
}
|
|
|
|
func (i *localImage) Import(copier func(io.Reader) error, vol libvirtxml.StorageVolume) error {
|
|
file, err := os.Open(i.path)
|
|
defer file.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("error while opening %s: %w", i.path, err)
|
|
}
|
|
|
|
fi, err := file.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// we can skip the upload if the modification times are the same
|
|
if vol.Target.Timestamps != nil && vol.Target.Timestamps.Mtime != "" {
|
|
if fi.ModTime() == timeFromEpoch(vol.Target.Timestamps.Mtime) {
|
|
log.Printf("Modification time is the same: skipping image copy")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return copier(file)
|
|
}
|
|
|
|
type httpImage struct {
|
|
url *url.URL
|
|
}
|
|
|
|
func (i *httpImage) String() string {
|
|
return i.url.String()
|
|
}
|
|
|
|
func (i *httpImage) Size() (uint64, error) {
|
|
response, err := http.Head(i.url.String())
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if response.StatusCode == http.StatusForbidden {
|
|
// possibly only the HEAD method is forbidden, try a Body-less GET instead
|
|
response, err = http.Get(i.url.String())
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
response.Body.Close()
|
|
}
|
|
if response.StatusCode != http.StatusOK {
|
|
return 0,
|
|
fmt.Errorf(
|
|
"error accessing remote resource: %s - %s",
|
|
i.url.String(),
|
|
response.Status)
|
|
}
|
|
|
|
length, err := strconv.Atoi(response.Header.Get("Content-Length"))
|
|
if err != nil {
|
|
err = fmt.Errorf(
|
|
"error while getting Content-Length of \"%s\": %w - got %s",
|
|
i.url.String(),
|
|
err,
|
|
response.Header.Get("Content-Length"))
|
|
return 0, err
|
|
}
|
|
return uint64(length), nil
|
|
}
|
|
|
|
//nolint:gomnd
|
|
func (i *httpImage) IsQCOW2() (bool, error) {
|
|
client := &http.Client{}
|
|
req, _ := http.NewRequest("GET", i.url.String(), nil)
|
|
req.Header.Set("Range", "bytes=0-7")
|
|
response, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode != http.StatusPartialContent {
|
|
return false, fmt.Errorf(
|
|
"can't retrieve partial header of resource to determine file type: %s - %s",
|
|
i.url.String(),
|
|
response.Status)
|
|
}
|
|
|
|
header, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if len(header) < 8 {
|
|
return false, fmt.Errorf(
|
|
"can't retrieve read header of resource to determine file type: %s - %d bytes read",
|
|
i.url.String(),
|
|
len(header))
|
|
}
|
|
|
|
return isQCOW2Header(header)
|
|
}
|
|
|
|
func (i *httpImage) Import(copier func(io.Reader) error, vol libvirtxml.StorageVolume) error {
|
|
// number of download retries on non client errors (eg. 5xx)
|
|
const maxHTTPRetries int = 3
|
|
// wait time between retries
|
|
const retryWait time.Duration = 2 * time.Second
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequest("GET", i.url.String(), nil)
|
|
|
|
if err != nil {
|
|
log.Printf("[DEBUG:] Error creating new request for source url %s: %s", i.url.String(), err)
|
|
return fmt.Errorf("error while downloading %s: %w", i.url.String(), err)
|
|
}
|
|
|
|
if vol.Target.Timestamps != nil && vol.Target.Timestamps.Mtime != "" {
|
|
req.Header.Set("If-Modified-Since", timeFromEpoch(vol.Target.Timestamps.Mtime).UTC().Format(http.TimeFormat))
|
|
}
|
|
|
|
var response *http.Response
|
|
for retryCount := 0; retryCount < maxHTTPRetries; retryCount++ {
|
|
response, err = client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("error while downloading %s: %w", i.url.String(), err)
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
log.Printf("[DEBUG]: url resp status code %s (retry #%d)\n", response.Status, retryCount)
|
|
if response.StatusCode == http.StatusNotModified {
|
|
return nil
|
|
} else if response.StatusCode == http.StatusOK {
|
|
return copier(response.Body)
|
|
} else if response.StatusCode < http.StatusInternalServerError {
|
|
break
|
|
} else if retryCount < maxHTTPRetries {
|
|
// The problem is not client but server side
|
|
// retry a few times after a small wait
|
|
time.Sleep(retryWait)
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("error while downloading %s: %v", i.url.String(), response)
|
|
}
|
|
|
|
func newImage(source string) (image, error) {
|
|
url, err := url.Parse(source)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't parse source '%s' as url: %w", source, err)
|
|
}
|
|
|
|
if strings.HasPrefix(url.Scheme, "http") {
|
|
return &httpImage{url: url}, nil
|
|
} else if url.Scheme == "file" && runtime.GOOS == "windows" {
|
|
// workaround #1, file:///C:/foo/bar.iso URL on Windows path has "/" prefix
|
|
// making it invalid
|
|
return &localImage{path: filepath.Clean(strings.TrimPrefix(url.Path, "/"))}, nil
|
|
} else if url.Scheme == "file" || url.Scheme == "" {
|
|
return &localImage{path: url.Path}, nil
|
|
} else {
|
|
// workaround #2, plain path (eg. C:\foo\bar.iso) detected as URL
|
|
// with scheme "C". We check if the non-URL exists as path
|
|
if runtime.GOOS == "windows" {
|
|
if stat, err := os.Stat(url.String()); err == nil && !stat.IsDir() {
|
|
return &localImage{path: filepath.Clean(source)}, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("don't know how to handle image URI from '%v' (scheme: %s)", url, url.Scheme)
|
|
}
|
|
}
|
|
|
|
// isQCOW2Header returns True when the buffer starts with the qcow2 header.
|
|
//nolint:gomnd
|
|
func isQCOW2Header(buf []byte) (bool, error) {
|
|
if len(buf) < 8 {
|
|
return false, fmt.Errorf("expected header of 8 bytes. Got %d", len(buf))
|
|
}
|
|
if buf[0] == 'Q' && buf[1] == 'F' &&
|
|
buf[2] == 'I' && buf[3] == 0xfb &&
|
|
buf[4] == 0x00 && buf[5] == 0x00 &&
|
|
buf[6] == 0x00 && buf[7] == 0x03 {
|
|
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|