VirtualBox

source: vbox/trunk/src/VBox/Main/xml/ovfreader.cpp@ 102869

Last change on this file since 102869 was 101472, checked in by vboxsync, 14 months ago

FE/VBoxManage,FE/Qt,Main/{VirtualBox.xidl,Appliance}: Add the ability to
export and import VMs which contain an NVMe storage controller.
bugref:10159 ticketref:19320

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 50.5 KB
Line 
1/* $Id: ovfreader.cpp 101472 2023-10-17 11:45:00Z vboxsync $ */
2/** @file
3 * OVF reader declarations.
4 *
5 * Depends only on IPRT, including the RTCString and IPRT XML classes.
6 */
7
8/*
9 * Copyright (C) 2008-2023 Oracle and/or its affiliates.
10 *
11 * This file is part of VirtualBox base platform packages, as
12 * available from https://www.virtualbox.org.
13 *
14 * This program is free software; you can redistribute it and/or
15 * modify it under the terms of the GNU General Public License
16 * as published by the Free Software Foundation, in version 3 of the
17 * License.
18 *
19 * This program is distributed in the hope that it will be useful, but
20 * WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 * General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, see <https://www.gnu.org/licenses>.
26 *
27 * SPDX-License-Identifier: GPL-3.0-only
28 */
29
30#define LOG_GROUP LOG_GROUP_MAIN_APPLIANCE
31#include "ovfreader.h"
32#include <VBox/log.h>
33#include <vector>
34
35using namespace std;
36using namespace ovf;
37
38
39
40////////////////////////////////////////////////////////////////////////////////
41//
42// OVF reader implementation
43//
44////////////////////////////////////////////////////////////////////////////////
45
46/**
47 * Default Constructor.
48 * Should be used if you don't have an OVF file, but want to fill the data
49 * m_mapDisks, m_llVirtualSystems manually
50 */
51OVFReader::OVFReader()
52{
53}
54
55/**
56 * Constructor. This parses the given XML file out of the memory. Throws lots of exceptions
57 * on XML or OVF invalidity.
58 * @param pvBuf the memory buffer to parse
59 * @param cbSize the size of the memory buffer
60 * @param path path to a filename for error messages.
61 */
62OVFReader::OVFReader(const void *pvBuf, size_t cbSize, const RTCString &path)
63 : m_strPath(path)
64{
65 xml::XmlMemParser parser;
66 parser.read(pvBuf, cbSize,
67 m_strPath,
68 m_doc);
69 /* Start the parsing */
70 parse();
71}
72
73/**
74 * Constructor. This opens the given XML file and parses it. Throws lots of exceptions
75 * on XML or OVF invalidity.
76 * @param path
77 */
78OVFReader::OVFReader(const RTCString &path)
79 : m_strPath(path)
80{
81 xml::XmlFileParser parser;
82 parser.read(m_strPath,
83 m_doc);
84 /* Start the parsing */
85 parse();
86}
87
88void OVFReader::parse()
89{
90 const xml::ElementNode *pRootElem = m_doc.getRootElement();
91 const xml::AttributeNode *pTypeAttr;
92 const char *pcszTypeAttr = "";
93 RTCString pcszNamespaceURI;
94
95 if (!pRootElem || strcmp(pRootElem->getName(), "Envelope"))
96 throw OVFLogicError(N_("Root element in OVF file must be 'Envelope'."));
97
98 pcszNamespaceURI = pRootElem->getNamespaceURI();
99 if (pcszNamespaceURI.isEmpty())
100 {
101 throw OVFLogicError(N_("Error reading namespace URI in 'Envelope' element, line %d"), pRootElem->getLineNumber());
102 }
103
104 if (strncmp(ovf::OVF20_URI_string, pcszNamespaceURI.c_str(), pcszNamespaceURI.length()) == 0)
105 {
106 m_envelopeData.setOVFVersion(ovf::OVFVersion_2_0);
107 }
108 else if (strncmp(OVF10_URI_string, pcszNamespaceURI.c_str(), pcszNamespaceURI.length()) == 0)
109 {
110 m_envelopeData.setOVFVersion(ovf::OVFVersion_1_0);
111 }
112 else
113 {
114 m_envelopeData.setOVFVersion(ovf::OVFVersion_0_9);
115 }
116
117 if ((pTypeAttr = pRootElem->findAttribute("lang", "xml")))
118 {
119 pcszTypeAttr = pTypeAttr->getValueN(RT_XML_ATTR_TINY);
120 m_envelopeData.lang = pcszTypeAttr;
121 }
122
123 // OVF has the following rough layout:
124 /*
125 -- <References> .... files referenced from other parts of the file, such as VMDK images
126 -- Metadata, comprised of several section commands
127 -- virtual machines, either a single <VirtualSystem>, or a <VirtualSystemCollection>
128 -- optionally <Strings> for localization
129 */
130
131 // get all "File" child elements of "References" section so we can look up files easily;
132 // first find the "References" sections so we can look up files
133 xml::ElementNodesList listFileElements; // receives all /Envelope/References/File nodes
134 const xml::ElementNode *pReferencesElem;
135 if ((pReferencesElem = pRootElem->findChildElement("References")))
136 pReferencesElem->getChildElements(listFileElements, "File");
137
138 // now go though the sections
139 LoopThruSections(pReferencesElem, pRootElem);
140}
141
142/**
143 * Private helper method that goes thru the elements of the given "current" element in the OVF XML
144 * and handles the contained child elements (which can be "Section" or "Content" elements).
145 *
146 * @param pReferencesElem "References" element from OVF, for looking up file specifications;
147 * can be NULL if no such element is present.
148 * @param pCurElem Element whose children are to be analyzed here.
149 */
150void OVFReader::LoopThruSections(const xml::ElementNode *pReferencesElem,
151 const xml::ElementNode *pCurElem)
152{
153 xml::NodesLoop loopChildren(*pCurElem);
154 const xml::ElementNode *pElem;
155 while ((pElem = loopChildren.forAllNodes()))
156 {
157 const char *pcszElemName = pElem->getName();
158 const xml::AttributeNode *pTypeAttr = pElem->findAttribute("type");
159 const char *pcszTypeAttr = pTypeAttr ? pTypeAttr->getValueN(RT_XML_ATTR_TINY) : "";
160
161 if ( !strcmp(pcszElemName, "DiskSection")
162 || ( !strcmp(pcszElemName, "Section")
163 && !strcmp(pcszTypeAttr, "ovf:DiskSection_Type")
164 )
165 )
166 {
167 HandleDiskSection(pReferencesElem, pElem);
168 }
169 else if ( !strcmp(pcszElemName, "NetworkSection")
170 || ( !strcmp(pcszElemName, "Section")
171 && !strcmp(pcszTypeAttr, "ovf:NetworkSection_Type")
172 )
173 )
174 {
175 HandleNetworkSection(pElem);
176 }
177 else if ( !strcmp(pcszElemName, "DeploymentOptionSection"))
178 {
179 /// @todo
180 }
181 else if ( !strcmp(pcszElemName, "Info"))
182 {
183 // child of VirtualSystemCollection -- TODO
184 }
185 else if ( !strcmp(pcszElemName, "ResourceAllocationSection"))
186 {
187 // child of VirtualSystemCollection -- TODO
188 }
189 else if ( !strcmp(pcszElemName, "StartupSection"))
190 {
191 // child of VirtualSystemCollection -- TODO
192 }
193 else if ( !strcmp(pcszElemName, "VirtualSystem")
194 || ( !strcmp(pcszElemName, "Content")
195 && !strcmp(pcszTypeAttr, "ovf:VirtualSystem_Type")
196 )
197 )
198 {
199 HandleVirtualSystemContent(pElem);
200 }
201 else if ( !strcmp(pcszElemName, "VirtualSystemCollection")
202 || ( !strcmp(pcszElemName, "Content")
203 && !strcmp(pcszTypeAttr, "ovf:VirtualSystemCollection_Type")
204 )
205 )
206 {
207 /// @todo ResourceAllocationSection
208
209 // recurse for this, since it has VirtualSystem elements as children
210 LoopThruSections(pReferencesElem, pElem);
211 }
212 }
213}
214
215/**
216 * Private helper method that handles disk sections in the OVF XML.
217 *
218 * Gets called indirectly from IAppliance::read().
219 *
220 * @param pReferencesElem "References" element from OVF, for looking up file
221 * specifications; can be NULL if no such element is
222 * present.
223 * @param pSectionElem Section element for which this helper is getting called.
224 */
225void OVFReader::HandleDiskSection(const xml::ElementNode *pReferencesElem,
226 const xml::ElementNode *pSectionElem)
227{
228 // contains "Disk" child elements
229 xml::NodesLoop loopDisks(*pSectionElem, "Disk");
230 const xml::ElementNode *pelmDisk;
231 while ((pelmDisk = loopDisks.forAllNodes()))
232 {
233 DiskImage d;
234 const char *pcszBad = NULL;
235 const char *pcszDiskId;
236 const char *pcszFormat;
237 if (!pelmDisk->getAttributeValueN("diskId", pcszDiskId, RT_XML_ATTR_TINY))
238 pcszBad = "diskId";
239 else if (!pelmDisk->getAttributeValueN("format", pcszFormat, RT_XML_ATTR_SMALL))
240 pcszBad = "format";
241 else if (!pelmDisk->getAttributeValue("capacity", d.iCapacity))
242 pcszBad = "capacity";
243 else
244 {
245 d.strDiskId = pcszDiskId;
246 d.strFormat = pcszFormat;
247
248 if (!pelmDisk->getAttributeValue("populatedSize", d.iPopulatedSize))
249 // optional
250 d.iPopulatedSize = -1;
251
252 // optional vbox:uuid attribute (if OVF was exported by VirtualBox != 3.2)
253 pelmDisk->getAttributeValueN("uuid", d.uuidVBox, RT_XML_ATTR_TINY, "vbox");
254
255 const char *pcszFileRef;
256 if (pelmDisk->getAttributeValueN("fileRef", pcszFileRef, RT_XML_ATTR_SMALL)) // optional
257 {
258 // look up corresponding /References/File nodes (list built above)
259 const xml::ElementNode *pFileElem;
260 if ( pReferencesElem
261 && (pFileElem = pReferencesElem->findChildElementFromId(pcszFileRef)) != NULL
262 )
263 {
264
265 // copy remaining values from file node then
266 const char *pcszBadInFile = NULL;
267 const char *pcszHref;
268 if (!pFileElem->getAttributeValueN("href", pcszHref, RT_XML_ATTR_SMALL))
269 pcszBadInFile = "href";
270 else if (!pFileElem->getAttributeValue("size", d.iSize))
271 d.iSize = -1; // optional
272
273 d.strHref = pcszHref;
274
275 // if (!(pFileElem->getAttributeValue("size", d.iChunkSize))) TODO
276 d.iChunkSize = -1; // optional
277 const char *pcszCompression;
278 if (pFileElem->getAttributeValueN("compression", pcszCompression, RT_XML_ATTR_TINY))
279 d.strCompression = pcszCompression;
280
281 if (pcszBadInFile)
282 throw OVFLogicError(N_("Error reading \"%s\": missing or invalid attribute '%s' in 'File' element, line %d"),
283 m_strPath.c_str(),
284 pcszBadInFile,
285 pFileElem->getLineNumber());
286 }
287 else
288 throw OVFLogicError(N_("Error reading \"%s\": cannot find References/File element for ID \"%s\" referenced by 'Disk' element, line %d"),
289 m_strPath.c_str(),
290 pcszFileRef,
291 pelmDisk->getLineNumber());
292 }
293 }
294
295 if (pcszBad)
296 throw OVFLogicError(N_("Error reading \"%s\": missing or invalid attribute '%s' in 'DiskSection' element, line %d"),
297 m_strPath.c_str(),
298 pcszBad,
299 pelmDisk->getLineNumber());
300
301 // suggest a size in megabytes to help callers with progress reports
302 d.ulSuggestedSizeMB = 0;
303 if (d.iCapacity != -1)
304 d.ulSuggestedSizeMB = (uint32_t)(d.iCapacity / _1M);
305 else if (d.iPopulatedSize != -1)
306 d.ulSuggestedSizeMB = (uint32_t)(d.iPopulatedSize / _1M);
307 else if (d.iSize != -1)
308 d.ulSuggestedSizeMB = (uint32_t)(d.iSize / _1M);
309 if (d.ulSuggestedSizeMB == 0)
310 d.ulSuggestedSizeMB = 10000; // assume 10 GB, this is for the progress bar only anyway
311
312 m_mapDisks[d.strDiskId] = d;
313 }
314}
315
316/**
317 * Private helper method that handles network sections in the OVF XML.
318 * Gets called indirectly from IAppliance::read().
319 */
320void OVFReader::HandleNetworkSection(const xml::ElementNode * /* pSectionElem */)
321{
322 // we ignore network sections for now
323
324// xml::NodesLoop loopNetworks(*pSectionElem, "Network");
325// const xml::Node *pelmNetwork;
326// while ((pelmNetwork = loopNetworks.forAllNodes()))
327// {
328// Network n;
329// if (!(pelmNetwork->getAttributeValue("name", n.strNetworkName)))
330// return setError(VBOX_E_FILE_ERROR,
331// tr("Error reading \"%s\": missing 'name' attribute in 'Network', line %d"),
332// pcszPath,
333// pelmNetwork->getLineNumber());
334//
335// m->mapNetworks[n.strNetworkName] = n;
336// }
337}
338
339/**
340 * Private helper method that handles a "VirtualSystem" element in the OVF XML.
341 * Gets called indirectly from IAppliance::read().
342 *
343 * @param pelmVirtualSystem
344 */
345void OVFReader::HandleVirtualSystemContent(const xml::ElementNode *pelmVirtualSystem)
346{
347 /* Create a new virtual system and work directly on the list copy. */
348 m_llVirtualSystems.push_back(VirtualSystem());
349 VirtualSystem &vsys = m_llVirtualSystems.back();
350
351 // peek under the <VirtualSystem> node whether we have a <vbox:Machine> node;
352 // that case case, the caller can completely ignore the OVF but only load the VBox machine XML
353 vsys.pelmVBoxMachine = pelmVirtualSystem->findChildElementNS("vbox", "Machine");
354
355 // now look for real OVF
356 const xml::AttributeNode *pIdAttr = pelmVirtualSystem->findAttribute("id");
357 if (pIdAttr)
358 vsys.strName = pIdAttr->getValueN(RT_XML_ATTR_SMALL);
359
360 xml::NodesLoop loop(*pelmVirtualSystem); // all child elements
361 const xml::ElementNode *pelmThis;
362 while ((pelmThis = loop.forAllNodes()))
363 {
364 const char *pcszElemName = pelmThis->getName();
365 const char *pcszTypeAttr = "";
366 if (!strcmp(pcszElemName, "Section")) // OVF 0.9 used "Section" element always with a varying "type" attribute
367 {
368 const xml::AttributeNode *pTypeAttr = pelmThis->findAttribute("type");
369 if (pTypeAttr)
370 pcszTypeAttr = pTypeAttr->getValueN(RT_XML_ATTR_TINY);
371 else
372 throw OVFLogicError(N_("Error reading \"%s\": element 'Section' has no 'type' attribute, line %d"),
373 m_strPath.c_str(),
374 pelmThis->getLineNumber());
375 }
376
377 if ( !strcmp(pcszElemName, "EulaSection")
378 || !strcmp(pcszTypeAttr, "ovf:EulaSection_Type")
379 )
380 {
381 /* <EulaSection>
382 <Info ovf:msgid="6">License agreement for the Virtual System.</Info>
383 <License ovf:msgid="1">License terms can go in here.</License>
384 </EulaSection> */
385
386 const xml::ElementNode *pelmLicense;
387 if ((pelmLicense = pelmThis->findChildElement("License")))
388 vsys.strLicenseText = pelmLicense->getValueN(RT_XML_CONTENT_LARGE);
389 }
390 if ( !strcmp(pcszElemName, "ProductSection")
391 || !strcmp(pcszTypeAttr, "ovf:ProductSection_Type")
392 )
393 {
394 /* <Section ovf:required="false" xsi:type="ovf:ProductSection_Type">
395 <Info>Meta-information about the installed software</Info>
396 <Product>VAtest</Product>
397 <Vendor>SUN Microsystems</Vendor>
398 <Version>10.0</Version>
399 <ProductUrl>http://blogs.sun.com/VirtualGuru</ProductUrl>
400 <VendorUrl>http://www.sun.com</VendorUrl>
401 </Section> */
402 const xml::ElementNode *pelmProduct;
403 if ((pelmProduct = pelmThis->findChildElement("Product")))
404 vsys.strProduct = pelmProduct->getValueN(RT_XML_CONTENT_SMALL);
405 const xml::ElementNode *pelmVendor;
406 if ((pelmVendor = pelmThis->findChildElement("Vendor")))
407 vsys.strVendor = pelmVendor->getValueN(RT_XML_CONTENT_SMALL);
408 const xml::ElementNode *pelmVersion;
409 if ((pelmVersion = pelmThis->findChildElement("Version")))
410 vsys.strVersion = pelmVersion->getValueN(RT_XML_CONTENT_SMALL);
411 const xml::ElementNode *pelmProductUrl;
412 if ((pelmProductUrl = pelmThis->findChildElement("ProductUrl")))
413 vsys.strProductUrl = pelmProductUrl->getValueN(RT_XML_CONTENT_SMALL);
414 const xml::ElementNode *pelmVendorUrl;
415 if ((pelmVendorUrl = pelmThis->findChildElement("VendorUrl")))
416 vsys.strVendorUrl = pelmVendorUrl->getValueN(RT_XML_CONTENT_SMALL);
417 }
418 else if ( !strcmp(pcszElemName, "VirtualHardwareSection")
419 || !strcmp(pcszTypeAttr, "ovf:VirtualHardwareSection_Type")
420 )
421 {
422 const xml::ElementNode *pelmSystem, *pelmVirtualSystemType;
423 if ((pelmSystem = pelmThis->findChildElement("System")))
424 {
425 /* <System>
426 <vssd:Description>Description of the virtual hardware section.</vssd:Description>
427 <vssd:ElementName>vmware</vssd:ElementName>
428 <vssd:InstanceID>1</vssd:InstanceID>
429 <vssd:VirtualSystemIdentifier>MyLampService</vssd:VirtualSystemIdentifier>
430 <vssd:VirtualSystemType>vmx-4</vssd:VirtualSystemType>
431 </System>*/
432 if ((pelmVirtualSystemType = pelmSystem->findChildElement("VirtualSystemType")))
433 vsys.strVirtualSystemType = pelmVirtualSystemType->getValueN(RT_XML_CONTENT_SMALL);
434 }
435
436 /* Parse the items into the hardware item vector. */
437 {
438 std::map<RTCString, const VirtualHardwareItem *> mapHardwareItems;
439 xml::NodesLoop childrenIterator(*pelmThis);
440 const xml::ElementNode *pelmItem;
441 while ((pelmItem = childrenIterator.forAllNodes()) != NULL)
442 {
443 /* Parse according to type. */
444 VirtualHardwareItem *pItem;
445 const char *pszName = pelmItem->getName();
446 if (RTStrCmp(pszName, "Item") == 0)
447 pItem = new VirtualHardwareItem();
448 else if (RTStrCmp(pszName, "StorageItem") == 0)
449 pItem = new StorageItem();
450 else if (RTStrCmp(pszName, "EthernetPortItem") == 0)
451 pItem = new EthernetPortItem();
452 else
453 continue;
454 vsys.vecHardwareItems.push_back(pItem);
455 pItem->m_iLineNumber = pelmItem->getLineNumber();
456 pItem->fillItem(pelmItem);
457
458 /* validate */
459 try
460 {
461 pItem->checkConsistencyAndCompliance();
462 }
463 catch (OVFLogicError &e)
464 {
465 throw OVFLogicError(N_("Error reading \"%s\": \"%s\""), m_strPath.c_str(), e.what());
466 }
467
468 /* Add to mapping vector (for parent ID lookups) if it has a valid instance ID. */
469 if (!pItem->strInstanceID.isEmpty())
470 {
471 std::map<RTCString, const VirtualHardwareItem *>::const_iterator itDup;
472 itDup = mapHardwareItems.find(pItem->strInstanceID);
473 if (itDup == mapHardwareItems.end())
474 mapHardwareItems[pItem->strInstanceID] = pItem;
475 else
476#if 1
477 LogRel(("OVFREADER: Warning reading \"%s\": Duplicate InstanceID \"%s\" on line %d, previous at %d!\n",
478 m_strPath.c_str(), pItem->strInstanceID.c_str(), pItem->m_iLineNumber, itDup->second->m_iLineNumber));
479#else
480 throw OVFLogicError(N_("Error reading \"%s\": Duplicate InstanceID \"%s\" on line %d, previous at %d"),
481 m_strPath.c_str(), pItem->strInstanceID.c_str(),
482 pItem->m_iLineNumber, itDup->second->m_iLineNumber);
483#endif
484 }
485 }
486 }
487
488 HardDiskController *pPrimaryIDEController = NULL;// will be set once found
489
490 // now go thru all hardware items and handle them according to their type;
491 // in this first loop we handle all items _except_ hard disk images,
492 // which we'll handle in a second loop below
493 HardwareItemVector::const_iterator itH;
494 for (itH = vsys.vecHardwareItems.begin(); itH != vsys.vecHardwareItems.end(); ++itH)
495 {
496 const VirtualHardwareItem &i = **itH;
497
498 // do some analysis
499 switch (i.resourceType)
500 {
501 case ResourceType_Processor: // 3
502 /* <rasd:Caption>1 virtual CPU</rasd:Caption>
503 <rasd:Description>Number of virtual CPUs</rasd:Description>
504 <rasd:ElementName>virtual CPU</rasd:ElementName>
505 <rasd:InstanceID>1</rasd:InstanceID>
506 <rasd:ResourceType>3</rasd:ResourceType>
507 <rasd:VirtualQuantity>1</rasd:VirtualQuantity>*/
508 if (i.ullVirtualQuantity < UINT16_MAX)
509 vsys.cCPUs = (uint16_t)i.ullVirtualQuantity;
510 else
511 throw OVFLogicError(N_("Error reading \"%s\": CPU count %RI64 is larger than %d, line %d"),
512 m_strPath.c_str(),
513 i.ullVirtualQuantity,
514 UINT16_MAX,
515 i.m_iLineNumber);
516 break;
517
518 case ResourceType_Memory: // 4
519 /* It's alway stored in bytes in VSD according to the old internal agreement within the team */
520 if ( i.strAllocationUnits == "MegaBytes" // found in OVF created by OVF toolkit
521 || i.strAllocationUnits == "MB" // found in MS docs
522 || i.strAllocationUnits == "byte * 2^20" // suggested by OVF spec DSP0243 page 21
523 )
524 vsys.ullMemorySize = i.ullVirtualQuantity * _1M;
525 else if ( i.strAllocationUnits == "GigaBytes"
526 || i.strAllocationUnits == "GB"
527 || i.strAllocationUnits == "byte * 2^30"
528 )
529 vsys.ullMemorySize = i.ullVirtualQuantity * _1G;
530 else
531 throw OVFLogicError(N_("Error reading \"%s\": Invalid allocation unit \"%s\" specified with memory size item, line %d"),
532 m_strPath.c_str(),
533 i.strAllocationUnits.c_str(),
534 i.m_iLineNumber);
535 break;
536
537 case ResourceType_IDEController: // 5
538 {
539 /* <Item>
540 <rasd:Caption>ideController0</rasd:Caption>
541 <rasd:Description>IDE Controller</rasd:Description>
542 <rasd:InstanceId>5</rasd:InstanceId>
543 <rasd:ResourceType>5</rasd:ResourceType>
544 <rasd:Address>0</rasd:Address>
545 <rasd:BusNumber>0</rasd:BusNumber>
546 </Item> */
547 HardDiskController hdc;
548 hdc.system = HardDiskController::IDE;
549 hdc.strIdController = i.strInstanceID;
550 hdc.strControllerType = i.strResourceSubType;
551
552 hdc.lAddress = i.lAddress;
553
554 if (!pPrimaryIDEController)
555 // this is the first IDE controller found: then mark it as "primary"
556 hdc.fPrimary = true;
557 else
558 {
559 // this is the second IDE controller found: If VMware exports two
560 // IDE controllers, it seems that they are given an "Address" of 0
561 // an 1, respectively, so assume address=0 means primary controller
562 if ( pPrimaryIDEController->lAddress == 0
563 && hdc.lAddress == 1
564 )
565 {
566 pPrimaryIDEController->fPrimary = true;
567 hdc.fPrimary = false;
568 }
569 else if ( pPrimaryIDEController->lAddress == 1
570 && hdc.lAddress == 0
571 )
572 {
573 pPrimaryIDEController->fPrimary = false;
574 hdc.fPrimary = false;
575 }
576 else
577 // then we really can't tell, just hope for the best
578 hdc.fPrimary = false;
579 }
580
581 vsys.mapControllers[i.strInstanceID] = hdc;
582 if (!pPrimaryIDEController)
583 pPrimaryIDEController = &vsys.mapControllers[i.strInstanceID];
584 break;
585 }
586
587 case ResourceType_ParallelSCSIHBA: // 6 SCSI controller
588 {
589 /* <Item>
590 <rasd:Caption>SCSI Controller 0 - LSI Logic</rasd:Caption>
591 <rasd:Description>SCI Controller</rasd:Description>
592 <rasd:ElementName>SCSI controller</rasd:ElementName>
593 <rasd:InstanceID>4</rasd:InstanceID>
594 <rasd:ResourceSubType>LsiLogic</rasd:ResourceSubType>
595 <rasd:ResourceType>6</rasd:ResourceType>
596 </Item> */
597 HardDiskController hdc;
598 hdc.system = HardDiskController::SCSI;
599 hdc.strIdController = i.strInstanceID;
600 hdc.strControllerType = i.strResourceSubType;
601
602 vsys.mapControllers[i.strInstanceID] = hdc;
603 break;
604 }
605
606 case ResourceType_EthernetAdapter: // 10
607 {
608 /* <Item>
609 <rasd:Caption>Ethernet adapter on 'Bridged'</rasd:Caption>
610 <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
611 <rasd:Connection>Bridged</rasd:Connection>
612 <rasd:InstanceID>6</rasd:InstanceID>
613 <rasd:ResourceType>10</rasd:ResourceType>
614 <rasd:ResourceSubType>E1000</rasd:ResourceSubType>
615 </Item>
616
617 OVF spec DSP 0243 page 21:
618 "For an Ethernet adapter, this specifies the abstract network connection name
619 for the virtual machine. All Ethernet adapters that specify the same abstract
620 network connection name within an OVF package shall be deployed on the same
621 network. The abstract network connection name shall be listed in the NetworkSection
622 at the outermost envelope level." */
623
624 // only store the name
625 EthernetAdapter ea;
626 ea.strAdapterType = i.strResourceSubType;
627 ea.strNetworkName = i.strConnection;
628 vsys.llEthernetAdapters.push_back(ea);
629 break;
630 }
631
632 case ResourceType_FloppyDrive: // 14
633 vsys.fHasFloppyDrive = true; // we have no additional information
634 break;
635
636 case ResourceType_CDDrive: // 15
637 /* <Item ovf:required="false">
638 <rasd:Caption>cdrom1</rasd:Caption>
639 <rasd:InstanceId>7</rasd:InstanceId>
640 <rasd:ResourceType>15</rasd:ResourceType>
641 <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
642 <rasd:Parent>5</rasd:Parent>
643 <rasd:AddressOnParent>0</rasd:AddressOnParent>
644 </Item> */
645 // I tried to see what happens if I set an ISO for the CD-ROM in VMware Workstation,
646 // but then the ovftool dies with "Device backing not supported". So I guess if
647 // VMware can't export ISOs, then we don't need to be able to import them right now.
648 vsys.fHasCdromDrive = true; // we have no additional information
649 break;
650
651 case ResourceType_HardDisk: // 17
652 // handled separately in second loop below
653 break;
654
655 case ResourceType_OtherStorageDevice: // 20 SATA/Virtio-SCSI/NVMe controller
656 {
657 /* <Item>
658 <rasd:Description>SATA Controller</rasd:Description>
659 <rasd:Caption>sataController0</rasd:Caption>
660 <rasd:InstanceID>4</rasd:InstanceID>
661 <rasd:ResourceType>20</rasd:ResourceType>
662 <rasd:ResourceSubType>AHCI</rasd:ResourceSubType>
663 <rasd:Address>0</rasd:Address>
664 <rasd:BusNumber>0</rasd:BusNumber>
665 </Item> */
666 if ( i.strResourceSubType.compare("AHCI", RTCString::CaseInsensitive) == 0
667 || i.strResourceSubType.compare("vmware.sata.ahci", RTCString::CaseInsensitive) == 0)
668 {
669 HardDiskController hdc;
670 hdc.system = HardDiskController::SATA;
671 hdc.strIdController = i.strInstanceID;
672 hdc.strControllerType = i.strResourceSubType;
673
674 vsys.mapControllers[i.strInstanceID] = hdc;
675 }
676 else if ( i.strResourceSubType.compare("VirtioSCSI", RTCString::CaseInsensitive) == 0
677 || i.strResourceSubType.compare("virtio-scsi", RTCString::CaseInsensitive) == 0 )
678 {
679 HardDiskController hdc;
680 hdc.system = HardDiskController::VIRTIOSCSI;
681 hdc.strIdController = i.strInstanceID;
682 //<rasd:ResourceSubType>VirtioSCSI</rasd:ResourceSubType>
683 hdc.strControllerType = i.strResourceSubType;
684 vsys.mapControllers[i.strInstanceID] = hdc;
685 }
686 else if ( i.strResourceSubType.compare("NVMe", RTCString::CaseInsensitive) == 0
687 || i.strResourceSubType.compare("vmware.nvme.controller", RTCString::CaseInsensitive) == 0 )
688 {
689 HardDiskController hdc;
690 hdc.system = HardDiskController::NVMe;
691 hdc.strIdController = i.strInstanceID;
692 //<rasd:ResourceSubType>NVMe</rasd:ResourceSubType>
693 hdc.strControllerType = i.strResourceSubType;
694 vsys.mapControllers[i.strInstanceID] = hdc;
695 }
696 else
697 throw OVFLogicError(N_("Error reading \"%s\": Host resource of type \"Other Storage Device (%d)\" is supported with SATA AHCI or Virtio-SCSI or NVMe controllers only, line %d (subtype:%s)"),
698 m_strPath.c_str(),
699 ResourceType_OtherStorageDevice,
700 i.m_iLineNumber, i.strResourceSubType.c_str() );
701 break;
702 }
703
704 case ResourceType_USBController: // 23
705 /* <Item ovf:required="false">
706 <rasd:Caption>usb</rasd:Caption>
707 <rasd:Description>USB Controller</rasd:Description>
708 <rasd:InstanceId>3</rasd:InstanceId>
709 <rasd:ResourceType>23</rasd:ResourceType>
710 <rasd:Address>0</rasd:Address>
711 <rasd:BusNumber>0</rasd:BusNumber>
712 </Item> */
713 vsys.fHasUsbController = true; // we have no additional information
714 break;
715
716 case ResourceType_SoundCard: // 35
717 /* <Item ovf:required="false">
718 <rasd:Caption>sound</rasd:Caption>
719 <rasd:Description>Sound Card</rasd:Description>
720 <rasd:InstanceId>10</rasd:InstanceId>
721 <rasd:ResourceType>35</rasd:ResourceType>
722 <rasd:ResourceSubType>ensoniq1371</rasd:ResourceSubType>
723 <rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
724 <rasd:AddressOnParent>3</rasd:AddressOnParent>
725 </Item> */
726 vsys.strSoundCardType = i.strResourceSubType;
727 break;
728
729 default:
730 {
731 /* If this unknown resource type isn't required, we simply skip it. */
732 if (i.fResourceRequired)
733 {
734 throw OVFLogicError(N_("Error reading \"%s\": Unknown resource type %d in hardware item, line %d"),
735 m_strPath.c_str(),
736 i.resourceType,
737 i.m_iLineNumber);
738 }
739 }
740 } // end switch
741 }
742
743 // now run through the items for a second time, but handle only
744 // hard disk images; otherwise the code would fail if a hard
745 // disk image appears in the OVF before its hard disk controller
746 for (itH = vsys.vecHardwareItems.begin(); itH != vsys.vecHardwareItems.end(); ++itH)
747 {
748 const VirtualHardwareItem &i = **itH;
749
750 // do some analysis
751 switch (i.resourceType)
752 {
753 case ResourceType_CDDrive: // 15
754 /* <Item ovf:required="false">
755 <rasd:Caption>cdrom1</rasd:Caption>
756 <rasd:InstanceId>7</rasd:InstanceId>
757 <rasd:ResourceType>15</rasd:ResourceType>
758 <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
759 <rasd:Parent>5</rasd:Parent>
760 <rasd:AddressOnParent>0</rasd:AddressOnParent>
761 </Item> */
762 case ResourceType_HardDisk: // 17
763 {
764 /* <Item>
765 <rasd:Caption>Harddisk 1</rasd:Caption>
766 <rasd:Description>HD</rasd:Description>
767 <rasd:ElementName>Hard Disk</rasd:ElementName>
768 <rasd:HostResource>ovf://disk/lamp</rasd:HostResource>
769 <rasd:InstanceID>5</rasd:InstanceID>
770 <rasd:Parent>4</rasd:Parent>
771 <rasd:ResourceType>17</rasd:ResourceType>
772 </Item> */
773
774 // look up the hard disk controller element whose InstanceID equals our Parent;
775 // this is how the connection is specified in OVF
776 ControllersMap::const_iterator it = vsys.mapControllers.find(i.strParent);
777 if (it == vsys.mapControllers.end())
778 throw OVFLogicError(N_("Error reading \"%s\": Disk item with instance ID \"%s\" specifies invalid parent \"%s\", line %d"),
779 m_strPath.c_str(),
780 i.strInstanceID.c_str(),
781 i.strParent.c_str(),
782 i.m_iLineNumber);
783
784 VirtualDisk vd;
785 vd.strIdController = i.strParent;
786 i.strAddressOnParent.toInt(vd.ulAddressOnParent);
787 // ovf://disk/lamp
788 // 123456789012345
789 if (i.strHostResource.startsWith("ovf://disk/"))
790 vd.strDiskId = i.strHostResource.substr(11);
791 else if (i.strHostResource.startsWith("ovf:/disk/"))
792 vd.strDiskId = i.strHostResource.substr(10);
793 else if (i.strHostResource.startsWith("/disk/"))
794 vd.strDiskId = i.strHostResource.substr(6);
795
796 //the error may be missed for CD, because CD can be empty
797 if ((vd.strDiskId.isEmpty() || (m_mapDisks.find(vd.strDiskId) == m_mapDisks.end()))
798 && i.resourceType == ResourceType_HardDisk)
799 throw OVFLogicError(N_("Error reading \"%s\": Disk item with instance ID \"%s\" specifies invalid host resource \"%s\", line %d"),
800 m_strPath.c_str(),
801 i.strInstanceID.c_str(),
802 i.strHostResource.c_str(),
803 i.m_iLineNumber);
804
805 vsys.mapVirtualDisks[vd.strDiskId] = vd;
806 break;
807 }
808 default:
809 break;
810 }
811 }
812 }
813 else if ( !strcmp(pcszElemName, "OperatingSystemSection")
814 || !strcmp(pcszTypeAttr, "ovf:OperatingSystemSection_Type")
815 )
816 {
817 uint64_t cimos64;
818 if (!(pelmThis->getAttributeValue("id", cimos64)))
819 throw OVFLogicError(N_("Error reading \"%s\": missing or invalid 'ovf:id' attribute in operating system section element, line %d"),
820 m_strPath.c_str(),
821 pelmThis->getLineNumber());
822
823 vsys.cimos = (CIMOSType_T)cimos64;
824 const xml::ElementNode *pelmCIMOSDescription;
825 if ((pelmCIMOSDescription = pelmThis->findChildElement("Description")))
826 vsys.strCimosDesc = pelmCIMOSDescription->getValueN(RT_XML_CONTENT_SMALL);
827
828 const xml::ElementNode *pelmVBoxOSType;
829 if ((pelmVBoxOSType = pelmThis->findChildElementNS("vbox", // namespace
830 "OSType"))) // element name
831 vsys.strTypeVBox = pelmVBoxOSType->getValueN(RT_XML_CONTENT_SMALL);
832 }
833 else if ( (!strcmp(pcszElemName, "AnnotationSection"))
834 || (!strcmp(pcszTypeAttr, "ovf:AnnotationSection_Type"))
835 )
836 {
837 const xml::ElementNode *pelmAnnotation;
838 if ((pelmAnnotation = pelmThis->findChildElement("Annotation")))
839 vsys.strDescription = pelmAnnotation->getValueN(RT_XML_CONTENT_SMALL);
840 }
841 }
842}
843
844void VirtualHardwareItem::fillItem(const xml::ElementNode *item)
845{
846 xml::NodesLoop loopItemChildren(*item);// all child elements
847 const xml::ElementNode *pelmItemChild;
848 while ((pelmItemChild = loopItemChildren.forAllNodes()))
849 {
850 const char *pcszItemChildName = pelmItemChild->getName();
851 if (!strcmp(pcszItemChildName, "Description"))
852 strDescription = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
853 else if (!strcmp(pcszItemChildName, "Caption"))
854 strCaption = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
855 else if (!strcmp(pcszItemChildName, "ElementName"))
856 strElementName = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
857 else if ( !strcmp(pcszItemChildName, "InstanceID")
858 || !strcmp(pcszItemChildName, "InstanceId") )
859 strInstanceID = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
860 else if (!strcmp(pcszItemChildName, "HostResource"))
861 strHostResource = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
862 else if (!strcmp(pcszItemChildName, "ResourceType"))
863 {
864 uint32_t ulType;
865 pelmItemChild->copyValue(ulType);
866 if (ulType > 0xffff)
867 ulType = 0xffff;
868 resourceType = (ResourceType_T)ulType;
869 fResourceRequired = true;
870 const char *pcszAttValue;
871 if (item->getAttributeValueN("required", pcszAttValue, RT_XML_ATTR_TINY))
872 {
873 if (!strcmp(pcszAttValue, "false"))
874 fResourceRequired = false;
875 }
876 }
877 else if (!strcmp(pcszItemChildName, "OtherResourceType"))
878 strOtherResourceType = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
879 else if (!strcmp(pcszItemChildName, "ResourceSubType"))
880 strResourceSubType = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
881 else if (!strcmp(pcszItemChildName, "AutomaticAllocation"))
882 fAutomaticAllocation = (!strcmp(pelmItemChild->getValueN(RT_XML_CONTENT_SMALL), "true")) ? true : false;
883 else if (!strcmp(pcszItemChildName, "AutomaticDeallocation"))
884 fAutomaticDeallocation = (!strcmp(pelmItemChild->getValueN(RT_XML_CONTENT_SMALL), "true")) ? true : false;
885 else if (!strcmp(pcszItemChildName, "Parent"))
886 strParent = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
887 else if (!strcmp(pcszItemChildName, "Connection"))
888 strConnection = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
889 else if (!strcmp(pcszItemChildName, "Address"))
890 {
891 strAddress = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
892 pelmItemChild->copyValue(lAddress);
893 }
894 else if (!strcmp(pcszItemChildName, "AddressOnParent"))
895 strAddressOnParent = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
896 else if (!strcmp(pcszItemChildName, "AllocationUnits"))
897 strAllocationUnits = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
898 else if (!strcmp(pcszItemChildName, "VirtualQuantity"))
899 pelmItemChild->copyValue(ullVirtualQuantity);
900 else if (!strcmp(pcszItemChildName, "Reservation"))
901 pelmItemChild->copyValue(ullReservation);
902 else if (!strcmp(pcszItemChildName, "Limit"))
903 pelmItemChild->copyValue(ullLimit);
904 else if (!strcmp(pcszItemChildName, "Weight"))
905 pelmItemChild->copyValue(ullWeight);
906 else if (!strcmp(pcszItemChildName, "ConsumerVisibility"))
907 strConsumerVisibility = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
908 else if (!strcmp(pcszItemChildName, "MappingBehavior"))
909 strMappingBehavior = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
910 else if (!strcmp(pcszItemChildName, "PoolID"))
911 strPoolID = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
912 else if (!strcmp(pcszItemChildName, "BusNumber"))
913 pelmItemChild->copyValue(ulBusNumber);
914// else if (pelmItemChild->getPrefix() == NULL
915// || strcmp(pelmItemChild->getPrefix(), "vmw"))
916// throw OVFLogicError(N_("Unknown element '%s' under Item element, line %d"),
917// pcszItemChildName,
918// m_iLineNumber);
919 }
920}
921
922void VirtualHardwareItem::_checkConsistencyAndCompliance() RT_THROW(OVFLogicError)
923{
924 RTCString name = getItemName();
925 if (resourceType == 0)
926 throw OVFLogicError(N_("Empty element ResourceType under %s element, line %d. see DMTF Schema Documentation %s"),
927 name.c_str(), m_iLineNumber, DTMF_SPECS_URI);
928
929 /* Don't be too uptight about the strInstanceID value. There are OVAs out
930 there which have InstanceID="%iid%" for memory for instance, which is
931 no good reason for not being able to process them. bugref:8997 */
932 if (strInstanceID.isEmpty())
933 {
934 if ( resourceType == ResourceType_IDEController
935 || resourceType == ResourceType_OtherStorageDevice
936 || resourceType == ResourceType_ParallelSCSIHBA
937 || resourceType == ResourceType_iSCSIHBA //??
938 || resourceType == ResourceType_IBHCA ) //??
939 throw OVFLogicError(N_("Element InstanceID is absent under %s element, line %d. see DMTF Schema Documentation %s"),
940 name.c_str(), m_iLineNumber, DTMF_SPECS_URI);
941 else
942 LogRel(("OVFREADER: Warning: Ignoring missing or invalid InstanceID under element %s, line %u\n",
943 name.c_str(), m_iLineNumber));
944 }
945}
946
947void StorageItem::fillItem(const xml::ElementNode *item)
948{
949 VirtualHardwareItem::fillItem(item);
950
951 xml::NodesLoop loopItemChildren(*item);// all child elements
952 const xml::ElementNode *pelmItemChild;
953 while ((pelmItemChild = loopItemChildren.forAllNodes()))
954 {
955 const char *pcszItemChildName = pelmItemChild->getName();
956 if (!strcmp(pcszItemChildName, "HostExtentName"))
957 strHostExtentName = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
958 else if (!strcmp(pcszItemChildName, "OtherHostExtentNameFormat"))
959 strOtherHostExtentNameFormat = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
960 else if (!strcmp(pcszItemChildName, "OtherHostExtentNameNamespace"))
961 strOtherHostExtentNameNamespace = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
962 else if (!strcmp(pcszItemChildName, "VirtualQuantityUnits"))
963 strVirtualQuantityUnits = pelmItemChild->getValueN(RT_XML_CONTENT_SMALL);
964 else if (!strcmp(pcszItemChildName, "Access"))
965 {
966 uint32_t temp;
967 pelmItemChild->copyValue(temp);
968 accessType = (StorageAccessType_T)temp;
969 }
970 else if (!strcmp(pcszItemChildName, "HostExtentNameFormat"))
971 {
972 }
973 else if (!strcmp(pcszItemChildName, "HostExtentNameNamespace"))
974 {
975 }
976 else if (!strcmp(pcszItemChildName, "HostExtentStartingAddress"))
977 {
978 }
979 else if (!strcmp(pcszItemChildName, "HostResourceBlockSize"))
980 {
981 int64_t temp;
982 pelmItemChild->copyValue(temp);
983 hostResourceBlockSize = temp;
984 }
985 else if (!strcmp(pcszItemChildName, "Limit"))
986 {
987 int64_t temp;
988 pelmItemChild->copyValue(temp);
989 limit = temp;
990 }
991 else if (!strcmp(pcszItemChildName, "Reservation"))
992 {
993 int64_t temp;
994 pelmItemChild->copyValue(temp);
995 reservation = temp;
996 }
997 else if (!strcmp(pcszItemChildName, "VirtualQuantity"))
998 {
999 int64_t temp;
1000 pelmItemChild->copyValue(temp);
1001 virtualQuantity = temp;
1002 }
1003 else if (!strcmp(pcszItemChildName, "VirtualResourceBlockSize"))
1004 {
1005 int64_t temp;
1006 pelmItemChild->copyValue(temp);
1007 virtualResourceBlockSize = temp;
1008 }
1009 }
1010}
1011
1012
1013void StorageItem::_checkConsistencyAndCompliance() RT_THROW(OVFLogicError)
1014{
1015 VirtualHardwareItem::_checkConsistencyAndCompliance();
1016
1017 RTCString name = getItemName();
1018
1019 if (accessType == StorageAccessType_Unknown)
1020 {
1021 //throw OVFLogicError(N_("Access type is unknown under %s element, line %d"),
1022 // name.c_str(), m_iLineNumber);
1023 }
1024
1025 if (hostResourceBlockSize <= 0 && reservation > 0)
1026 {
1027 throw OVFLogicError(N_("Element HostResourceBlockSize is absent under %s element, line %d. "
1028 "see DMTF Schema Documentation %s"),
1029 name.c_str(), m_iLineNumber, DTMF_SPECS_URI);
1030 }
1031
1032 if (virtualResourceBlockSize <= 0 && virtualQuantity > 0)
1033 {
1034 throw OVFLogicError(N_("Element VirtualResourceBlockSize is absent under %s element, line %d. "
1035 "see DMTF Schema Documentation %s"),
1036 name.c_str(), m_iLineNumber, DTMF_SPECS_URI);
1037 }
1038
1039 if (virtualQuantity > 0 && strVirtualQuantityUnits.isEmpty())
1040 {
1041 throw OVFLogicError(N_("Element VirtualQuantityUnits is absent under %s element, line %d. "
1042 "see DMTF Schema Documentation %s"),
1043 name.c_str(), m_iLineNumber, DTMF_SPECS_URI);
1044 }
1045
1046 if (virtualResourceBlockSize <= 1 &&
1047 strVirtualQuantityUnits.compare(RTCString("count"), RTCString::CaseInsensitive) == 0
1048 )
1049 {
1050 throw OVFLogicError(N_("Element VirtualQuantityUnits is set to \"count\" "
1051 "while VirtualResourceBlockSize is set to 1. "
1052 "under %s element, line %d. "
1053 "It's needed to change on \"byte\". "
1054 "see DMTF Schema Documentation %s"),
1055 name.c_str(), m_iLineNumber, DTMF_SPECS_URI);
1056 }
1057}
1058
1059void EthernetPortItem::fillItem(const xml::ElementNode *item)
1060{
1061 VirtualHardwareItem::fillItem(item);
1062
1063 xml::NodesLoop loopItemChildren(*item);// all child elements
1064 const xml::ElementNode *pelmItemChild;
1065 while ((pelmItemChild = loopItemChildren.forAllNodes()))
1066 {
1067 }
1068}
1069
1070void EthernetPortItem::_checkConsistencyAndCompliance() RT_THROW(OVFLogicError)
1071{
1072 VirtualHardwareItem::_checkConsistencyAndCompliance();
1073}
1074
1075////////////////////////////////////////////////////////////////////////////////
1076//
1077// Errors
1078//
1079////////////////////////////////////////////////////////////////////////////////
1080
1081OVFLogicError::OVFLogicError(const char *aFormat, ...)
1082{
1083 char *pszNewMsg;
1084 va_list args;
1085 va_start(args, aFormat);
1086 RTStrAPrintfV(&pszNewMsg, aFormat, args);
1087 setWhat(pszNewMsg);
1088 RTStrFree(pszNewMsg);
1089 va_end(args);
1090}
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette