github.com/vmware/govmomi@v0.43.0/ovf/importer/importer.go (about) 1 /* 2 Copyright (c) 2024-2024 VMware, Inc. All Rights Reserved. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package importer 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "fmt" 24 "os" 25 "path" 26 "path/filepath" 27 "strings" 28 29 "github.com/vmware/govmomi/find" 30 "github.com/vmware/govmomi/nfc" 31 "github.com/vmware/govmomi/object" 32 "github.com/vmware/govmomi/ovf" 33 "github.com/vmware/govmomi/vapi/library" 34 "github.com/vmware/govmomi/vim25" 35 "github.com/vmware/govmomi/vim25/progress" 36 "github.com/vmware/govmomi/vim25/soap" 37 "github.com/vmware/govmomi/vim25/types" 38 ) 39 40 type Importer struct { 41 Log progress.LogFunc 42 43 Name string 44 VerifyManifest bool 45 Hidden bool 46 47 Client *vim25.Client 48 Finder *find.Finder 49 Sinker progress.Sinker 50 51 Datacenter *object.Datacenter 52 Datastore *object.Datastore 53 ResourcePool *object.ResourcePool 54 Host *object.HostSystem 55 Folder *object.Folder 56 57 Archive Archive 58 Manifest map[string]*library.Checksum 59 } 60 61 func (imp *Importer) ReadManifest(fpath string) error { 62 base := filepath.Base(fpath) 63 ext := filepath.Ext(base) 64 mfName := strings.Replace(base, ext, ".mf", 1) 65 66 mf, _, err := imp.Archive.Open(mfName) 67 if err != nil { 68 msg := fmt.Sprintf("manifest %q: %s", mf, err) 69 fmt.Fprintln(os.Stderr, msg) 70 return errors.New(msg) 71 } 72 imp.Manifest, err = library.ReadManifest(mf) 73 _ = mf.Close() 74 return err 75 } 76 77 func (imp *Importer) Import(ctx context.Context, fpath string, opts Options) (*types.ManagedObjectReference, error) { 78 79 o, err := ReadOvf(fpath, imp.Archive) 80 if err != nil { 81 return nil, err 82 } 83 84 e, err := ReadEnvelope(o) 85 if err != nil { 86 return nil, fmt.Errorf("failed to parse ovf: %s", err) 87 } 88 89 if e.VirtualSystem != nil { 90 if e.VirtualSystem != nil { 91 if opts.Name == nil { 92 opts.Name = &e.VirtualSystem.ID 93 if e.VirtualSystem.Name != nil { 94 opts.Name = e.VirtualSystem.Name 95 } 96 } 97 } 98 if imp.Hidden { 99 // TODO: userConfigurable is optional and defaults to false, so we should *add* userConfigurable=true 100 // if not set for a Property. But, there'd be a bunch more work involved to preserve other data in doing 101 // a complete xml.Marshal of the .ovf 102 o = bytes.ReplaceAll(o, []byte(`userConfigurable="false"`), []byte(`userConfigurable="true"`)) 103 } 104 } 105 106 name := "Govc Virtual Appliance" 107 if opts.Name != nil { 108 name = *opts.Name 109 } 110 111 nmap, err := imp.NetworkMap(ctx, e, opts.NetworkMapping) 112 if err != nil { 113 return nil, err 114 } 115 116 cisp := types.OvfCreateImportSpecParams{ 117 DiskProvisioning: opts.DiskProvisioning, 118 EntityName: name, 119 IpAllocationPolicy: opts.IPAllocationPolicy, 120 IpProtocol: opts.IPProtocol, 121 OvfManagerCommonParams: types.OvfManagerCommonParams{ 122 DeploymentOption: opts.Deployment, 123 Locale: "US"}, 124 PropertyMapping: OVFMap(opts.PropertyMapping), 125 NetworkMapping: nmap, 126 } 127 128 m := ovf.NewManager(imp.Client) 129 spec, err := m.CreateImportSpec(ctx, string(o), imp.ResourcePool, imp.Datastore, cisp) 130 if err != nil { 131 return nil, err 132 } 133 if spec.Error != nil { 134 return nil, errors.New(spec.Error[0].LocalizedMessage) 135 } 136 if spec.Warning != nil { 137 for _, w := range spec.Warning { 138 _, _ = imp.Log(fmt.Sprintf("Warning: %s\n", w.LocalizedMessage)) 139 } 140 } 141 142 if opts.Annotation != "" { 143 switch s := spec.ImportSpec.(type) { 144 case *types.VirtualMachineImportSpec: 145 s.ConfigSpec.Annotation = opts.Annotation 146 case *types.VirtualAppImportSpec: 147 s.VAppConfigSpec.Annotation = opts.Annotation 148 } 149 } 150 151 if imp.VerifyManifest { 152 if err := imp.ReadManifest(fpath); err != nil { 153 return nil, err 154 } 155 } 156 157 lease, err := imp.ResourcePool.ImportVApp(ctx, spec.ImportSpec, imp.Folder, imp.Host) 158 if err != nil { 159 return nil, err 160 } 161 162 info, err := lease.Wait(ctx, spec.FileItem) 163 if err != nil { 164 return nil, err 165 } 166 167 u := lease.StartUpdater(ctx, info) 168 defer u.Done() 169 170 for _, i := range info.Items { 171 if err := imp.Upload(ctx, lease, i); err != nil { 172 return nil, err 173 } 174 } 175 176 return &info.Entity, lease.Complete(ctx) 177 } 178 179 func (imp *Importer) NetworkMap(ctx context.Context, e *ovf.Envelope, networks []Network) ([]types.OvfNetworkMapping, error) { 180 var nmap []types.OvfNetworkMapping 181 for _, m := range networks { 182 if m.Network == "" { 183 continue // Not set, let vSphere choose the default network 184 } 185 if err := ValidateNetwork(e, m); err != nil && imp.Log != nil { 186 _, _ = imp.Log(err.Error() + "\n") 187 } 188 189 var ref types.ManagedObjectReference 190 191 net, err := imp.Finder.Network(ctx, m.Network) 192 if err != nil { 193 switch err.(type) { 194 case *find.NotFoundError: 195 if !ref.FromString(m.Network) { 196 return nil, err 197 } // else this is a raw MO ref 198 default: 199 return nil, err 200 } 201 } else { 202 ref = net.Reference() 203 } 204 205 nmap = append(nmap, types.OvfNetworkMapping{ 206 Name: m.Name, 207 Network: ref, 208 }) 209 } 210 211 return nmap, nil 212 } 213 214 func OVFMap(op []Property) (p []types.KeyValue) { 215 for _, v := range op { 216 p = append(p, types.KeyValue{ 217 Key: v.Key, 218 Value: v.Value, 219 }) 220 } 221 222 return 223 } 224 225 func ValidateNetwork(e *ovf.Envelope, net Network) error { 226 var names []string 227 228 if e.Network != nil { 229 for _, n := range e.Network.Networks { 230 if n.Name == net.Name { 231 return nil 232 } 233 names = append(names, n.Name) 234 } 235 } 236 237 return fmt.Errorf("warning: invalid NetworkMapping.Name=%q, valid names=%s", net.Name, names) 238 } 239 240 func ValidateChecksum(ctx context.Context, lease *nfc.Lease, sum *library.Checksum, file string, key string) error { 241 // Perform the checksum match eagerly, after each file upload, instead 242 // of after uploading all the files, to provide fail-fast behavior. 243 // (Trade-off here is multiple GetManifest() API calls to the server.) 244 manifests, err := lease.GetManifest(ctx) 245 if err != nil { 246 return err 247 } 248 for _, m := range manifests { 249 if m.Key == key { 250 // Compare server-side computed checksum of uploaded file 251 // against the client's manifest entry (assuming client's 252 // manifest has correct checksums - client doesn't compute 253 // checksum of the file before uploading). 254 255 // Try matching sha1 first (newer versions have moved to sha256). 256 if strings.ToUpper(sum.Algorithm) == "SHA1" { 257 if sum.Checksum != m.Sha1 { 258 msg := fmt.Sprintf("manifest checksum %v mismatch with uploaded checksum %v for file %v", 259 sum.Checksum, m.Sha1, file) 260 return errors.New(msg) 261 } 262 // Uploaded file checksum computed by server matches with local manifest entry. 263 return nil 264 } 265 // If not sha1, check for other types (in a separate field). 266 if !strings.EqualFold(sum.Algorithm, m.ChecksumType) { 267 msg := fmt.Sprintf("manifest checksum type %v mismatch with uploaded checksum type %v for file %v", 268 sum.Algorithm, m.ChecksumType, file) 269 return errors.New(msg) 270 } 271 if !strings.EqualFold(sum.Checksum, m.Checksum) { 272 msg := fmt.Sprintf("manifest checksum %v mismatch with uploaded checksum %v for file %v", 273 sum.Checksum, m.Checksum, file) 274 return errors.New(msg) 275 } 276 // Uploaded file checksum computed by server matches with local manifest entry. 277 return nil 278 } 279 } 280 msg := fmt.Sprintf("missing manifest entry on server for uploaded file %v (key %v), manifests=%#v", file, key, manifests) 281 return errors.New(msg) 282 } 283 284 func (imp *Importer) Upload(ctx context.Context, lease *nfc.Lease, item nfc.FileItem) error { 285 file := item.Path 286 287 f, size, err := imp.Archive.Open(file) 288 if err != nil { 289 return err 290 } 291 defer f.Close() 292 293 logger := progress.NewProgressLogger(imp.Log, fmt.Sprintf("Uploading %s... ", path.Base(file))) 294 defer logger.Wait() 295 296 opts := soap.Upload{ 297 ContentLength: size, 298 Progress: logger, 299 } 300 301 err = lease.Upload(ctx, item, f, opts) 302 if err != nil { 303 return err 304 } 305 306 if imp.VerifyManifest { 307 mapImportKeyToKey := func(urls []types.HttpNfcLeaseDeviceUrl, importKey string) string { 308 for _, url := range urls { 309 if url.ImportKey == importKey { 310 return url.Key 311 } 312 } 313 return "" 314 } 315 leaseInfo, err := lease.Wait(ctx, nil) 316 if err != nil { 317 return err 318 } 319 sum, ok := imp.Manifest[file] 320 if !ok { 321 return fmt.Errorf("missing checksum for %v in manifest file", file) 322 } 323 return ValidateChecksum(ctx, lease, sum, file, mapImportKeyToKey(leaseInfo.DeviceUrl, item.DeviceId)) 324 } 325 return nil 326 }