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 }