VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/tests/storage/storagecfg.py@ 79592

Last change on this file since 79592 was 79591, checked in by vboxsync, 6 years ago

ValidationKit/tests/storage/tdStorageBenchmark1: Allow static configurations where we are just given a directory for testing and have no control over the disk devices

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 21.9 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: storagecfg.py 79591 2019-07-08 12:25:46Z vboxsync $
3
4"""
5VirtualBox Validation Kit - Storage test configuration API.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2016-2019 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.virtualbox.org. This file is free software;
14you can redistribute it and/or modify it under the terms of the GNU
15General Public License (GPL) as published by the Free Software
16Foundation, in version 2 as it comes in the "COPYING" file of the
17VirtualBox OSE distribution. VirtualBox OSE is distributed in the
18hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
19
20The contents of this file may alternatively be used under the terms
21of the Common Development and Distribution License Version 1.0
22(CDDL) only, as it comes in the "COPYING.CDDL" file of the
23VirtualBox OSE distribution, in which case the provisions of the
24CDDL are applicable instead of those of the GPL.
25
26You may elect to license modified versions of this file under the
27terms and conditions of either the GPL or the CDDL or both.
28"""
29__version__ = "$Revision: 79591 $"
30
31# Standard Python imports.
32import os;
33import re;
34
35# Validation Kit imports.
36from common import utils;
37
38
39class StorageDisk(object):
40 """
41 Class representing a disk for testing.
42 """
43
44 def __init__(self, sPath, fRamDisk = False):
45 self.sPath = sPath;
46 self.fUsed = False;
47 self.fRamDisk = fRamDisk;
48
49 def getPath(self):
50 """
51 Return the disk path.
52 """
53 return self.sPath;
54
55 def isUsed(self):
56 """
57 Returns whether the disk is currently in use.
58 """
59 return self.fUsed;
60
61 def isRamDisk(self):
62 """
63 Returns whether the disk objecthas a RAM backing.
64 """
65 return self.fRamDisk;
66
67 def setUsed(self, fUsed):
68 """
69 Sets the used flag for the disk.
70 """
71 if fUsed:
72 if self.fUsed:
73 return False;
74
75 self.fUsed = True;
76 else:
77 self.fUsed = fUsed;
78
79 return True;
80
81class StorageConfigOs(object):
82 """
83 Base class for a single hosts OS storage configuration.
84 """
85
86 def _getDisksMatchingRegExpWithPath(self, sPath, sRegExp):
87 """
88 Adds new disks to the config matching the given regular expression.
89 """
90
91 lstDisks = [];
92 oRegExp = re.compile(sRegExp);
93 asFiles = os.listdir(sPath);
94 for sFile in asFiles:
95 if oRegExp.match(os.path.basename(sFile)) and os.path.exists(sPath + '/' + sFile):
96 lstDisks.append(StorageDisk(sPath + '/' + sFile));
97
98 return lstDisks;
99
100class StorageConfigOsSolaris(StorageConfigOs):
101 """
102 Class implementing the Solaris specifics for a storage configuration.
103 """
104
105 def __init__(self):
106 StorageConfigOs.__init__(self);
107 self.idxRamDisk = 0;
108
109 def _getActivePoolsStartingWith(self, oExec, sPoolIdStart):
110 """
111 Returns a list of pools starting with the given ID or None on failure.
112 """
113 lstPools = None;
114 fRc, sOutput, _ = oExec.execBinary('zpool', ('list', '-H'));
115 if fRc:
116 lstPools = [];
117 asPools = sOutput.splitlines();
118 for sPool in asPools:
119 if sPool.startswith(sPoolIdStart):
120 # Extract the whole name and add it to the list.
121 asItems = sPool.split('\t');
122 lstPools.append(asItems[0]);
123 return lstPools;
124
125 def _getActiveVolumesInPoolStartingWith(self, oExec, sPool, sVolumeIdStart):
126 """
127 Returns a list of active volumes for the given pool starting with the given
128 identifier or None on failure.
129 """
130 lstVolumes = None;
131 fRc, sOutput, _ = oExec.execBinary('zfs', ('list', '-H'));
132 if fRc:
133 lstVolumes = [];
134 asVolumes = sOutput.splitlines();
135 for sVolume in asVolumes:
136 if sVolume.startswith(sPool + '/' + sVolumeIdStart):
137 # Extract the whole name and add it to the list.
138 asItems = sVolume.split('\t');
139 lstVolumes.append(asItems[0]);
140 return lstVolumes;
141
142 def getDisksMatchingRegExp(self, sRegExp):
143 """
144 Returns a list of disks matching the regular expression.
145 """
146 return self._getDisksMatchingRegExpWithPath('/dev/dsk', sRegExp);
147
148 def getMntBase(self):
149 """
150 Returns the mountpoint base for the host.
151 """
152 return '/pools';
153
154 def createStoragePool(self, oExec, sPool, asDisks, sRaidLvl):
155 """
156 Creates a new storage pool with the given disks and the given RAID level.
157 """
158 sZPoolRaid = None;
159 if len(asDisks) > 1 and (sRaidLvl == 'raid5' or sRaidLvl is None):
160 sZPoolRaid = 'raidz';
161
162 fRc = True;
163 if sZPoolRaid is not None:
164 fRc = oExec.execBinaryNoStdOut('zpool', ('create', '-f', sPool, sZPoolRaid,) + tuple(asDisks));
165 else:
166 fRc = oExec.execBinaryNoStdOut('zpool', ('create', '-f', sPool,) + tuple(asDisks));
167
168 return fRc;
169
170 def createVolume(self, oExec, sPool, sVol, sMountPoint, cbVol = None):
171 """
172 Creates and mounts a filesystem at the given mountpoint using the
173 given pool and volume IDs.
174 """
175 fRc = True;
176 if cbVol is not None:
177 fRc = oExec.execBinaryNoStdOut('zfs', ('create', '-o', 'mountpoint='+sMountPoint, '-V', cbVol, sPool + '/' + sVol));
178 else:
179 fRc = oExec.execBinaryNoStdOut('zfs', ('create', '-o', 'mountpoint='+sMountPoint, sPool + '/' + sVol));
180
181 return fRc;
182
183 def destroyVolume(self, oExec, sPool, sVol):
184 """
185 Destroys the given volume.
186 """
187 fRc = oExec.execBinaryNoStdOut('zfs', ('destroy', sPool + '/' + sVol));
188 return fRc;
189
190 def destroyPool(self, oExec, sPool):
191 """
192 Destroys the given storage pool.
193 """
194 fRc = oExec.execBinaryNoStdOut('zpool', ('destroy', sPool));
195 return fRc;
196
197 def cleanupPoolsAndVolumes(self, oExec, sPoolIdStart, sVolIdStart):
198 """
199 Cleans up any pools and volumes starting with the name in the given
200 parameters.
201 """
202 fRc = True;
203 lstPools = self._getActivePoolsStartingWith(oExec, sPoolIdStart);
204 if lstPools is not None:
205 for sPool in lstPools:
206 lstVolumes = self._getActiveVolumesInPoolStartingWith(oExec, sPool, sVolIdStart);
207 if lstVolumes is not None:
208 # Destroy all the volumes first
209 for sVolume in lstVolumes:
210 fRc2 = oExec.execBinaryNoStdOut('zfs', ('destroy', sVolume));
211 if not fRc2:
212 fRc = fRc2;
213
214 # Destroy the pool
215 fRc2 = self.destroyPool(oExec, sPool);
216 if not fRc2:
217 fRc = fRc2;
218 else:
219 fRc = False;
220 else:
221 fRc = False;
222
223 return fRc;
224
225 def createRamDisk(self, oExec, cbRamDisk):
226 """
227 Creates a RAM backed disk with the given size.
228 """
229 oDisk = None;
230 sRamDiskName = 'ramdisk%u' % (self.idxRamDisk,);
231 fRc, _ , _ = oExec.execBinary('ramdiskadm', ('-a', sRamDiskName, str(cbRamDisk)));
232 if fRc:
233 self.idxRamDisk += 1;
234 oDisk = StorageDisk('/dev/ramdisk/%s' % (sRamDiskName, ), True);
235
236 return oDisk;
237
238 def destroyRamDisk(self, oExec, oDisk):
239 """
240 Destroys the given ramdisk object.
241 """
242 sRamDiskName = os.path.basename(oDisk.getPath());
243 return oExec.execBinaryNoStdOut('ramdiskadm', ('-d', sRamDiskName));
244
245class StorageConfigOsLinux(StorageConfigOs):
246 """
247 Class implementing the Linux specifics for a storage configuration.
248 """
249
250 def __init__(self):
251 StorageConfigOs.__init__(self);
252 self.dSimplePools = { }; # Simple storage pools which don't use lvm (just one partition)
253 self.dMounts = { }; # Pool/Volume to mountpoint mapping.
254
255 def _getDmRaidLevelFromLvl(self, sRaidLvl):
256 """
257 Converts our raid level indicators to something mdadm can understand.
258 """
259 if sRaidLvl is None or sRaidLvl == 'raid0':
260 return 'stripe';
261 if sRaidLvl == 'raid5':
262 return '5';
263 if sRaidLvl == 'raid1':
264 return 'mirror';
265 return 'stripe';
266
267 def getDisksMatchingRegExp(self, sRegExp):
268 """
269 Returns a list of disks matching the regular expression.
270 """
271 return self._getDisksMatchingRegExpWithPath('/dev/', sRegExp);
272
273 def getMntBase(self):
274 """
275 Returns the mountpoint base for the host.
276 """
277 return '/mnt';
278
279 def createStoragePool(self, oExec, sPool, asDisks, sRaidLvl):
280 """
281 Creates a new storage pool with the given disks and the given RAID level.
282 """
283 fRc = True;
284 if len(asDisks) == 1 and sRaidLvl is None:
285 # Doesn't require LVM, put into the simple pools dictionary so we can
286 # use it when creating a volume later.
287 self.dSimplePools[sPool] = asDisks[0];
288 else:
289 # If a RAID is required use dm-raid first to create one.
290 asLvmPvDisks = asDisks;
291 fRc = oExec.execBinaryNoStdOut('mdadm', ('--create', '/dev/md0', '--assume-clean',
292 '--level=' + self._getDmRaidLevelFromLvl(sRaidLvl),
293 '--raid-devices=' + str(len(asDisks))) + tuple(asDisks));
294 if fRc:
295 # /dev/md0 is the only block device to use for our volume group.
296 asLvmPvDisks = [ '/dev/md0' ];
297
298 # Create a physical volume on every disk first.
299 for sLvmPvDisk in asLvmPvDisks:
300 fRc = oExec.execBinaryNoStdOut('pvcreate', (sLvmPvDisk, ));
301 if not fRc:
302 break;
303
304 if fRc:
305 # Create volume group with all physical volumes included
306 fRc = oExec.execBinaryNoStdOut('vgcreate', (sPool, ) + tuple(asLvmPvDisks));
307 return fRc;
308
309 def createVolume(self, oExec, sPool, sVol, sMountPoint, cbVol = None):
310 """
311 Creates and mounts a filesystem at the given mountpoint using the
312 given pool and volume IDs.
313 """
314 fRc = True;
315 sBlkDev = None;
316 if sPool in self.dSimplePools:
317 sDiskPath = self.dSimplePools.get(sPool);
318 if sDiskPath.find('zram') != -1:
319 sBlkDev = sDiskPath;
320 else:
321 # Create a partition with the requested size
322 sFdiskScript = ';\n'; # Single partition filling everything
323 if cbVol is not None:
324 sFdiskScript = ',' + str(cbVol // 512) + '\n'; # Get number of sectors
325 fRc = oExec.execBinaryNoStdOut('sfdisk', ('--no-reread', '--wipe', 'always', '-q', '-f', sDiskPath), \
326 sFdiskScript);
327 if fRc:
328 if sDiskPath.find('nvme') != -1:
329 sBlkDev = sDiskPath + 'p1';
330 else:
331 sBlkDev = sDiskPath + '1';
332 else:
333 if cbVol is None:
334 fRc = oExec.execBinaryNoStdOut('lvcreate', ('-l', '100%FREE', '-n', sVol, sPool));
335 else:
336 fRc = oExec.execBinaryNoStdOut('lvcreate', ('-L', str(cbVol), '-n', sVol, sPool));
337 if fRc:
338 sBlkDev = '/dev/mapper' + sPool + '-' + sVol;
339
340 if fRc is True and sBlkDev is not None:
341 # Create a filesystem and mount it
342 fRc = oExec.execBinaryNoStdOut('mkfs.ext4', ('-F', '-F', sBlkDev,));
343 fRc = fRc and oExec.mkDir(sMountPoint);
344 fRc = fRc and oExec.execBinaryNoStdOut('mount', (sBlkDev, sMountPoint));
345 if fRc:
346 self.dMounts[sPool + '/' + sVol] = sMountPoint;
347 return fRc;
348
349 def destroyVolume(self, oExec, sPool, sVol):
350 """
351 Destroys the given volume.
352 """
353 # Unmount first
354 sMountPoint = self.dMounts[sPool + '/' + sVol];
355 fRc = oExec.execBinaryNoStdOut('umount', (sMountPoint,));
356 self.dMounts.pop(sPool + '/' + sVol);
357 oExec.rmDir(sMountPoint);
358 if sPool in self.dSimplePools:
359 # Wipe partition table
360 sDiskPath = self.dSimplePools.get(sPool);
361 if sDiskPath.find('zram') == -1:
362 fRc = oExec.execBinaryNoStdOut('sfdisk', ('--no-reread', '--wipe', 'always', '-q', '-f', '--delete', \
363 sDiskPath));
364 else:
365 fRc = oExec.execBinaryNoStdOut('lvremove', (sPool + '/' + sVol,));
366 return fRc;
367
368 def destroyPool(self, oExec, sPool):
369 """
370 Destroys the given storage pool.
371 """
372 fRc = True;
373 if sPool in self.dSimplePools:
374 self.dSimplePools.pop(sPool);
375 else:
376 fRc = oExec.execBinaryNoStdOut('vgremove', (sPool,));
377 return fRc;
378
379 def cleanupPoolsAndVolumes(self, oExec, sPoolIdStart, sVolIdStart):
380 """
381 Cleans up any pools and volumes starting with the name in the given
382 parameters.
383 """
384 # @todo: Needs implementation, for LVM based configs a similar approach can be used
385 # as for Solaris.
386 _ = oExec;
387 _ = sPoolIdStart;
388 _ = sVolIdStart;
389 return True;
390
391 def createRamDisk(self, oExec, cbRamDisk):
392 """
393 Creates a RAM backed disk with the given size.
394 """
395 # Make sure the ZRAM module is loaded.
396 oDisk = None;
397 fRc = oExec.execBinaryNoStdOut('modprobe', ('zram',));
398 if fRc:
399 fRc, sOut, _ = oExec.execBinary('zramctl', ('--raw', '-f', '-s', str(cbRamDisk)));
400 if fRc:
401 oDisk = StorageDisk(sOut.rstrip(), True);
402
403 return oDisk;
404
405 def destroyRamDisk(self, oExec, oDisk):
406 """
407 Destroys the given ramdisk object.
408 """
409 return oExec.execBinaryNoStdOut('zramctl', ('-r', oDisk.getPath()));
410
411## @name Host disk config types.
412## @{
413g_ksDiskCfgStatic = 'StaticDir';
414g_ksDiskCfgRegExp = 'RegExp';
415g_ksDiskCfgList = 'DiskList';
416## @}
417
418class DiskCfg(object):
419 """
420 Host disk configuration.
421 """
422
423 def __init__(self, sTargetOs, sCfgType, oDisks):
424 self.sTargetOs = sTargetOs;
425 self.sCfgType = sCfgType;
426 self.oDisks = oDisks;
427
428 def getTargetOs(self):
429 return self.sTargetOs;
430
431 def getCfgType(self):
432 return self.sCfgType;
433
434 def isCfgStaticDir(self):
435 return self.sCfgType == g_ksDiskCfgStatic;
436
437 def isCfgRegExp(self):
438 return self.sCfgType == g_ksDiskCfgRegExp;
439
440 def isCfgList(self):
441 return self.sCfgType == g_ksDiskCfgList;
442
443 def getDisks(self):
444 return self.oDisks;
445
446class StorageCfg(object):
447 """
448 Storage configuration helper class taking care of the different host OS.
449 """
450
451 def __init__(self, oExec, oDiskCfg):
452 self.oExec = oExec;
453 self.lstDisks = [ ]; # List of disks present in the system.
454 self.dPools = { }; # Dictionary of storage pools.
455 self.dVols = { }; # Dictionary of volumes.
456 self.iPoolId = 0;
457 self.iVolId = 0;
458 self.oDiskCfg = oDiskCfg;
459
460 fRc = True;
461 oStorOs = None;
462 if oDiskCfg.getTargetOs() == 'solaris':
463 oStorOs = StorageConfigOsSolaris();
464 elif oDiskCfg.getTargetOs() == 'linux':
465 oStorOs = StorageConfigOsLinux(); # pylint: disable=redefined-variable-type
466 elif not oDiskCfg.isCfgStaticDir(): # For unknown hosts we only a static testing directory we don't care about setting up.
467 fRc = False;
468
469 if fRc:
470 self.oStorOs = oStorOs;
471 if oDiskCfg.isCfgRegExp():
472 self.lstDisks = oStorOs.getDisksMatchingRegExp(oDiskCfg.getDisks());
473 elif oDiskCfg.isCfgList():
474 # Assume a list of of disks and add.
475 for sDisk in oDiskCfg.getDisks():
476 self.lstDisks.append(StorageDisk(sDisk));
477
478 def __del__(self):
479 self.cleanup();
480
481 def cleanup(self):
482 """
483 Cleans up any created storage configs.
484 """
485
486 if not self.oDiskCfg.isCfgStaticDir():
487 # Destroy all volumes first.
488 for sMountPoint in self.dVols.keys(): # pylint: disable=consider-iterating-dictionary
489 self.destroyVolume(sMountPoint);
490
491 # Destroy all pools.
492 for sPool in self.dPools.keys(): # pylint: disable=consider-iterating-dictionary
493 self.destroyStoragePool(sPool);
494
495 self.dVols.clear();
496 self.dPools.clear();
497 self.oDiskCfg = None;
498 self.iPoolId = 0;
499 self.iVolId = 0;
500
501 def getRawDisk(self):
502 """
503 Returns a raw disk device from the list of free devices for use.
504 """
505
506 for oDisk in self.lstDisks:
507 if oDisk.isUsed() is False:
508 oDisk.setUsed(True);
509 return oDisk.getPath();
510
511 return None;
512
513 def getUnusedDiskCount(self):
514 """
515 Returns the number of unused disks.
516 """
517
518 cDisksUnused = 0;
519 for oDisk in self.lstDisks:
520 if not oDisk.isUsed():
521 cDisksUnused += 1;
522
523 return cDisksUnused;
524
525 def createStoragePool(self, cDisks = 0, sRaidLvl = None,
526 cbPool = None, fRamDisk = False):
527 """
528 Create a new storage pool
529 """
530 lstDisks = [ ];
531 fRc = True;
532 sPool = None;
533
534 if not self.oDiskCfg.isCfgStaticDir():
535 if fRamDisk:
536 oDisk = self.oStorOs.createRamDisk(self.oExec, cbPool);
537 if oDisk is not None:
538 lstDisks.append(oDisk);
539 cDisks = 1;
540 else:
541 if cDisks == 0:
542 cDisks = self.getUnusedDiskCount();
543
544 for oDisk in self.lstDisks:
545 if not oDisk.isUsed():
546 oDisk.setUsed(True);
547 lstDisks.append(oDisk);
548 if len(lstDisks) == cDisks:
549 break;
550
551 # Enough drives to satisfy the request?
552 if len(lstDisks) == cDisks:
553 # Create a list of all device paths
554 lstDiskPaths = [ ];
555 for oDisk in lstDisks:
556 lstDiskPaths.append(oDisk.getPath());
557
558 # Find a name for the pool
559 sPool = 'pool' + str(self.iPoolId);
560 self.iPoolId += 1;
561
562 fRc = self.oStorOs.createStoragePool(self.oExec, sPool, lstDiskPaths, sRaidLvl);
563 if fRc:
564 self.dPools[sPool] = lstDisks;
565 else:
566 self.iPoolId -= 1;
567 else:
568 fRc = False;
569
570 # Cleanup in case of error.
571 if not fRc:
572 for oDisk in lstDisks:
573 oDisk.setUsed(False);
574 if oDisk.isRamDisk():
575 self.oStorOs.destroyRamDisk(self.oExec, oDisk);
576 else:
577 sPool = 'StaticDummy';
578
579 return fRc, sPool;
580
581 def destroyStoragePool(self, sPool):
582 """
583 Destroys the storage pool with the given ID.
584 """
585
586 fRc = True;
587
588 if not self.oDiskCfg.isCfgStaticDir():
589 lstDisks = self.dPools.get(sPool);
590 if lstDisks is not None:
591 fRc = self.oStorOs.destroyPool(self.oExec, sPool);
592 if fRc:
593 # Mark disks as unused
594 self.dPools.pop(sPool);
595 for oDisk in lstDisks:
596 oDisk.setUsed(False);
597 if oDisk.isRamDisk():
598 self.oStorOs.destroyRamDisk(self.oExec, oDisk);
599 else:
600 fRc = False;
601
602 return fRc;
603
604 def createVolume(self, sPool, cbVol = None):
605 """
606 Creates a new volume from the given pool returning the mountpoint.
607 """
608
609 fRc = True;
610 sMountPoint = None;
611 if not self.oDiskCfg.isCfgStaticDir():
612 if sPool in self.dPools:
613 sVol = 'vol' + str(self.iVolId);
614 sMountPoint = self.oStorOs.getMntBase() + '/' + sVol;
615 self.iVolId += 1;
616 fRc = self.oStorOs.createVolume(self.oExec, sPool, sVol, sMountPoint, cbVol);
617 if fRc:
618 self.dVols[sMountPoint] = (sVol, sPool);
619 else:
620 self.iVolId -= 1;
621 else:
622 fRc = False;
623 else:
624 sMountPoint = self.oDiskCfg.getDisks();
625
626 return fRc, sMountPoint;
627
628 def destroyVolume(self, sMountPoint):
629 """
630 Destroy the volume at the given mount point.
631 """
632
633 fRc = True;
634 if not self.oDiskCfg.isCfgStaticDir():
635 sVol, sPool = self.dVols.get(sMountPoint);
636 if sVol is not None:
637 fRc = self.oStorOs.destroyVolume(self.oExec, sPool, sVol);
638 if fRc:
639 self.dVols.pop(sMountPoint);
640 else:
641 fRc = False;
642
643 return fRc;
644
645 def mkDirOnVolume(self, sMountPoint, sDir, fMode = 0o700):
646 """
647 Creates a new directory on the volume pointed to by the given mount point.
648 """
649 return self.oExec.mkDir(sMountPoint + '/' + sDir, fMode);
650
651 def cleanupLeftovers(self):
652 """
653 Tries to cleanup any leftover pools and volumes from a failed previous run.
654 """
655 if not self.oDiskCfg.isCfgStaticDir():
656 return self.oStorOs.cleanupPoolsAndVolumes(self.oExec, 'pool', 'vol');
657 else:
658 fRc = True;
659 for sEntry in os.listdir(self.oDiskCfg.getDisks()):
660 fRc = fRc and self.oExec.rmTree(os.path.join(self.oDiskCfg.getDisks(), sEntry));
661
662 return fRc;
Note: See TracBrowser for help on using the repository browser.

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