github.com/openshift/installer@v1.4.17/pkg/infrastructure/baremetal/image.go (about)

     1  // This file is largely based on existing code from terraform-provider-libvirt 0.6.12.
     2  // https://github.com/dmacvicar/terraform-provider-libvirt
     3  // Original code distributed under the terms of Apache License 2.0.
     4  package baremetal
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/sirupsen/logrus"
    17  	"libvirt.org/go/libvirtxml"
    18  )
    19  
    20  type image interface {
    21  	Size() (uint64, error)
    22  	Import(func(io.Reader) error, libvirtxml.StorageVolume) error
    23  	String() string
    24  	IsQCOW2() (bool, error)
    25  }
    26  
    27  type localImage struct {
    28  	path string
    29  }
    30  
    31  func (i *localImage) String() string {
    32  	return i.path
    33  }
    34  
    35  func isQCOW2Header(buf []byte) (bool, error) {
    36  	if len(buf) < 8 {
    37  		return false, fmt.Errorf("expected header of 8 bytes. Got %d", len(buf))
    38  	}
    39  	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 {
    40  		return true, nil
    41  	}
    42  	return false, nil
    43  }
    44  
    45  func (i *localImage) Size() (uint64, error) {
    46  	fi, err := os.Stat(i.path)
    47  	if err != nil {
    48  		return 0, err
    49  	}
    50  	return uint64(fi.Size()), nil
    51  }
    52  
    53  func (i *localImage) IsQCOW2() (bool, error) {
    54  	file, err := os.Open(i.path)
    55  	if err != nil {
    56  		return false, fmt.Errorf("error while opening %s: %w", i.path, err)
    57  	}
    58  	defer file.Close()
    59  	buf := make([]byte, 8)
    60  	_, err = io.ReadAtLeast(file, buf, 8)
    61  	if err != nil {
    62  		return false, err
    63  	}
    64  	return isQCOW2Header(buf)
    65  }
    66  
    67  func (i *localImage) Import(copier func(io.Reader) error, vol libvirtxml.StorageVolume) error {
    68  	file, err := os.Open(i.path)
    69  	if err != nil {
    70  		return fmt.Errorf("error while opening %s: %w", i.path, err)
    71  	}
    72  	defer file.Close()
    73  
    74  	fi, err := file.Stat()
    75  	if err != nil {
    76  		return err
    77  	}
    78  	// we can skip the upload if the modification times are the same
    79  	if vol.Target.Timestamps != nil && vol.Target.Timestamps.Mtime != "" {
    80  		if fi.ModTime() == timeFromEpoch(vol.Target.Timestamps.Mtime) {
    81  			logrus.Info("Modification time is the same: skipping image copy")
    82  			return nil
    83  		}
    84  	}
    85  
    86  	return copier(file)
    87  }
    88  
    89  type httpImage struct {
    90  	url *url.URL
    91  }
    92  
    93  func (i *httpImage) String() string {
    94  	return i.url.String()
    95  }
    96  
    97  func (i *httpImage) Size() (uint64, error) {
    98  	response, err := http.Head(i.url.String())
    99  	if err != nil {
   100  		return 0, err
   101  	}
   102  	if response.StatusCode == 403 {
   103  		// possibly only the HEAD method is forbidden, try a Body-less GET instead
   104  		response, err = http.Get(i.url.String())
   105  		if err != nil {
   106  			return 0, err
   107  		}
   108  
   109  		response.Body.Close()
   110  	}
   111  	if response.StatusCode != 200 {
   112  		return 0,
   113  			fmt.Errorf(
   114  				"error accessing remote resource: %s - %s",
   115  				i.url.String(),
   116  				response.Status)
   117  	}
   118  
   119  	length, err := strconv.Atoi(response.Header.Get("Content-Length"))
   120  	if err != nil {
   121  		err = fmt.Errorf(
   122  			"error while getting Content-Length of \"%s\": %w - got %s",
   123  			i.url.String(),
   124  			err,
   125  			response.Header.Get("Content-Length"))
   126  		return 0, err
   127  	}
   128  	return uint64(length), nil
   129  }
   130  
   131  func (i *httpImage) IsQCOW2() (bool, error) {
   132  	client := &http.Client{}
   133  	req, err := http.NewRequest("GET", i.url.String(), nil)
   134  	if err != nil {
   135  		return false, err
   136  	}
   137  	req.Header.Set("Range", "bytes=0-7")
   138  	response, err := client.Do(req)
   139  
   140  	if err != nil {
   141  		return false, err
   142  	}
   143  	defer response.Body.Close()
   144  
   145  	if response.StatusCode != 206 {
   146  		return false, fmt.Errorf(
   147  			"can't retrieve partial header of resource to determine file type: %s - %s",
   148  			i.url.String(),
   149  			response.Status)
   150  	}
   151  
   152  	header, err := io.ReadAll(response.Body)
   153  	if err != nil {
   154  		return false, err
   155  	}
   156  
   157  	if len(header) < 8 {
   158  		return false, fmt.Errorf(
   159  			"can't retrieve read header of resource to determine file type: %s - %d bytes read",
   160  			i.url.String(),
   161  			len(header))
   162  	}
   163  
   164  	return isQCOW2Header(header)
   165  }
   166  
   167  func (i *httpImage) Import(copier func(io.Reader) error, vol libvirtxml.StorageVolume) error {
   168  	// number of download retries on non client errors (eg. 5xx)
   169  	const maxHTTPRetries int = 3
   170  	// wait time between retries
   171  	const retryWait time.Duration = 2 * time.Second
   172  
   173  	client := &http.Client{}
   174  	req, err := http.NewRequest("GET", i.url.String(), nil)
   175  
   176  	if err != nil {
   177  		return fmt.Errorf("error while downloading %s: %w", i.url.String(), err)
   178  	}
   179  
   180  	if vol.Target.Timestamps != nil && vol.Target.Timestamps.Mtime != "" {
   181  		req.Header.Set("If-Modified-Since", timeFromEpoch(vol.Target.Timestamps.Mtime).UTC().Format(http.TimeFormat))
   182  	}
   183  
   184  	var response *http.Response
   185  	for retryCount := 0; retryCount < maxHTTPRetries; retryCount++ {
   186  		response, err = client.Do(req)
   187  		if err != nil {
   188  			return fmt.Errorf("error while downloading %s: %w", i.url.String(), err)
   189  		}
   190  		defer response.Body.Close()
   191  
   192  		logrus.Debugf("url resp status code %s (retry #%d)\n", response.Status, retryCount)
   193  
   194  		switch response.StatusCode {
   195  		case http.StatusNotModified:
   196  			return nil
   197  		case http.StatusOK:
   198  			return copier(response.Body)
   199  		default:
   200  			if response.StatusCode < 500 {
   201  				break
   202  			}
   203  			// The problem is not client but server side
   204  			// retry a few times after a small wait
   205  			if retryCount < maxHTTPRetries {
   206  				time.Sleep(retryWait)
   207  			}
   208  		}
   209  	}
   210  	return fmt.Errorf("error while downloading %s: %v", i.url.String(), response)
   211  }
   212  
   213  func newImage(source string) (image, error) {
   214  	url, err := url.Parse(source)
   215  	if err != nil {
   216  		return nil, fmt.Errorf("can't parse source '%s' as url: %w", source, err)
   217  	}
   218  
   219  	if strings.HasPrefix(url.Scheme, "http") {
   220  		return &httpImage{url: url}, nil
   221  	}
   222  
   223  	if url.Scheme == "file" || url.Scheme == "" {
   224  		return &localImage{path: url.Path}, nil
   225  	}
   226  
   227  	return nil, fmt.Errorf("don't know how to read from '%s': %w", url.String(), err)
   228  }
   229  
   230  func timeFromEpoch(str string) time.Time {
   231  	var s, ns int
   232  	var err error
   233  
   234  	ts := strings.Split(str, ".")
   235  	if len(ts) == 2 {
   236  		ns, err = strconv.Atoi(ts[1])
   237  		if err != nil {
   238  			ns = 0
   239  		}
   240  	}
   241  	s, err = strconv.Atoi(ts[0])
   242  	if err != nil {
   243  		s = 0
   244  	}
   245  
   246  	return time.Unix(int64(s), int64(ns))
   247  }