1 #!/usr/bin/env python2.6 2 # 3 # CDDL HEADER START 4 # 5 # The contents of this file are subject to the terms of the 6 # Common Development and Distribution License (the "License"). 7 # You may not use this file except in compliance with the License. 8 # 9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 10 # or http://www.opensolaris.org/os/licensing. 11 # See the License for the specific language governing permissions 12 # and limitations under the License. 13 # 14 # When distributing Covered Code, include this CDDL HEADER in each 15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE. 16 # If applicable, add the following below this CDDL HEADER, with the 17 # fields enclosed by brackets "[]" replaced with your own identifying 18 # information: Portions Copyright [yyyy] [name of copyright owner] 19 # 20 # CDDL HEADER END 21 # 22 23 import subprocess 24 import re 25 import threading 26 from bisect import insort 27 28 BYTESPERMB = 1048576 29 30 # Commonly used command paths 31 PFCMD = "/usr/bin/pfexec" 32 ZFSCMD = "/usr/sbin/zfs" 33 ZPOOLCMD = "/usr/sbin/zpool" 34 35 36 class Datasets(Exception): 37 """ 38 Container class for all zfs datasets. Maintains a centralised 39 list of datasets (generated on demand) and accessor methods. 40 Also allows clients to notify when a refresh might be necessary. 41 """ 42 # Class wide instead of per-instance in order to avoid duplication 43 filesystems = None 44 volumes = None 45 snapshots = None 46 47 # Mutex locks to prevent concurrent writes to above class wide 48 # dataset lists. 49 _filesystemslock = threading.Lock() 50 _volumeslock = threading.Lock() 51 snapshotslock = threading.Lock() 52 53 def create_auto_snapshot_set(self, label, tag=None): 54 """ 55 Create a complete set of snapshots as if this were 56 for a standard zfs-auto-snapshot operation. 57 58 Keyword arguments: 59 label: 60 A label to apply to the snapshot name. Cannot be None. 61 tag: 62 A string indicating one of the standard auto-snapshot schedules 63 tags to check (eg. "frequent" for will map to the tag: 64 com.sun:auto-snapshot:frequent). If specified as a zfs property 65 on a zfs dataset, the property corresponding to the tag will 66 override the wildcard property: "com.sun:auto-snapshot" 67 Default value = None 68 """ 69 excluded = [] 70 included = [] 71 single = [] 72 recursive = [] 73 #Get auto-snap property in two passes. First with the global 74 #value, then overriding with the label/schedule specific value 75 76 cmd = [ZFSCMD, "list", "-H", "-t", "filesystem,volume", 77 "-o", "name,com.sun:auto-snapshot", "-s", "name"] 78 if tag: 79 overrideprop = "com.sun:auto-snapshot:" + tag 80 scmd = [ZFSCMD, "list", "-H", "-t", "filesystem,volume", 81 "-o", "name," + overrideprop, "-s", "name"] 82 try: 83 p = subprocess.Popen(scmd, 84 stdout=subprocess.PIPE, 85 stderr=subprocess.PIPE, 86 close_fds=True) 87 outdata,errdata = p.communicate() 88 err = p.wait() 89 except OSError, message: 90 raise RuntimeError, "%s subprocess error:\n %s" % \ 91 (cmd, str(message)) 92 if err != 0: 93 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 94 (str(cmd), err, errdata) 95 for line in outdata.rstrip().split('\n'): 96 line = line.split() 97 #Skip over unset values 98 if line[1] == "-": 99 continue 100 elif line[1] == "true": 101 included.append(line[0]) 102 elif line[1] == "false": 103 excluded.append(line[0]) 104 try: 105 p = subprocess.Popen(cmd, 106 stdout=subprocess.PIPE, 107 stderr=subprocess.PIPE, 108 close_fds=True) 109 outdata,errdat = p.communicate() 110 err = p.wait() 111 except OSError, message: 112 raise RuntimeError, "%s subprocess error:\n %s" % \ 113 (cmd, str(message)) 114 if err != 0: 115 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 116 (str(cmd), err, errdata) 117 for line in outdata.rstrip().split('\n'): 118 line = line.split() 119 #Only set values that aren't already set. Don't override 120 try: 121 included.index(line[0]) 122 continue 123 except ValueError: 124 try: 125 excluded.index(line[0]) 126 continue 127 except ValueError: 128 #Dataset is not listed in either list. 129 if line[1] == "-": 130 continue 131 elif line[1] == "true": 132 included.append(line[0]) 133 elif line[1] == "false": 134 excluded.append(line[0]) 135 136 #Now figure out what can be recursively snapshotted and what 137 #must be singly snapshotted. Single snapshots restrictions apply 138 #to those datasets who have a child in the excluded list. 139 for datasetname in included: 140 excludedchild = False 141 dataset = ReadWritableDataset(datasetname) 142 children = dataset.list_children() 143 for child in children: 144 try: 145 excluded.index(child) 146 excludedchild = True 147 single.append(datasetname) 148 break 149 except ValueError: 150 pass 151 152 if excludedchild == False: 153 recursive.append(datasetname) 154 155 finalrecursive = [] 156 for datasetname in recursive: 157 parts = datasetname.rsplit('/', 1) 158 parent = parts[0] 159 if parent == datasetname: 160 #toplevel node. Skip 161 continue 162 if parent in recursive: 163 continue 164 else: 165 finalrecursive.append(datasetname) 166 167 for name in finalrecursive: 168 dataset = ReadWritableDataset(name) 169 dataset.create_snapshot(label, True) 170 for name in single: 171 dataset = ReadWritableDataset(name) 172 dataset.create_snapshot(label, False) 173 174 def list_filesystems(self, pattern = None): 175 """ 176 List pattern matching filesystems sorted by name. 177 178 Keyword arguments: 179 pattern -- Filter according to pattern (default None) 180 """ 181 filesystems = [] 182 # Need to first ensure no other thread is trying to 183 # build this list at the same time. 184 Datasets._filesystemslock.acquire() 185 if Datasets.filesystems == None: 186 Datasets.filesystems = [] 187 cmd = [ZFSCMD, "list", "-H", "-t", "filesystem", \ 188 "-o", "name,mountpoint", "-s", "name"] 189 try: 190 p = subprocess.Popen(cmd, 191 stdout=subprocess.PIPE, 192 stderr=subprocess.PIPE, 193 close_fds=True) 194 outdata,errdata = p.communicate() 195 err = p.wait() 196 except OSError, message: 197 raise RuntimeError, "%s subprocess error:\n %s" % \ 198 (cmd, str(message)) 199 if err != 0: 200 Datasets._filesystemslock.release() 201 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 202 (str(cmd), err, errdata) 203 for line in outdata.rstrip().split('\n'): 204 line = line.rstrip().split() 205 Datasets.filesystems.append([line[0], line[1]]) 206 Datasets._filesystemslock.release() 207 208 if pattern == None: 209 filesystems = Datasets.filesystems[:] 210 else: 211 # Regular expression pattern to match "pattern" parameter. 212 regexpattern = ".*%s.*" % pattern 213 patternobj = re.compile(regexpattern) 214 215 for fsname,fsmountpoint in Datasets.filesystems: 216 patternmatchobj = re.match(patternobj, fsname) 217 if patternmatchobj != None: 218 filesystems.append(fsname, fsmountpoint) 219 return filesystems 220 221 def list_volumes(self, pattern = None): 222 """ 223 List pattern matching volumes sorted by name. 224 225 Keyword arguments: 226 pattern -- Filter according to pattern (default None) 227 """ 228 volumes = [] 229 Datasets._volumeslock.acquire() 230 if Datasets.volumes == None: 231 Datasets.volumes = [] 232 cmd = [ZFSCMD, "list", "-H", "-t", "volume", \ 233 "-o", "name", "-s", "name"] 234 try: 235 p = subprocess.Popen(cmd, 236 stdout=subprocess.PIPE, 237 stderr=subprocess.PIPE, 238 close_fds=True) 239 outdata,errdata = p.communicate() 240 err = p.wait() 241 except OSError, message: 242 raise RuntimeError, "%s subprocess error:\n %s" % \ 243 (cmd, str(message)) 244 if err != 0: 245 Datasets._volumeslock.release() 246 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 247 (str(cmd), err, errdata) 248 for line in outdata.rstrip().split('\n'): 249 Datasets.volumes.append(line.rstrip()) 250 Datasets._volumeslock.release() 251 252 if pattern == None: 253 volumes = Datasets.volumes[:] 254 else: 255 # Regular expression pattern to match "pattern" parameter. 256 regexpattern = ".*%s.*" % pattern 257 patternobj = re.compile(regexpattern) 258 259 for volname in Datasets.volumes: 260 patternmatchobj = re.match(patternobj, volname) 261 if patternmatchobj != None: 262 volumes.append(volname) 263 return volumes 264 265 def list_snapshots(self, pattern = None): 266 """ 267 List pattern matching snapshots sorted by creation date. 268 Oldest listed first 269 270 Keyword arguments: 271 pattern -- Filter according to pattern (default None) 272 """ 273 snapshots = [] 274 Datasets.snapshotslock.acquire() 275 if Datasets.snapshots == None: 276 Datasets.snapshots = [] 277 snaps = [] 278 cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value,name", "creation"] 279 try: 280 p = subprocess.Popen(cmd, 281 stdout=subprocess.PIPE, 282 stderr=subprocess.PIPE, 283 close_fds=True) 284 outdata,errdata = p.communicate() 285 err= p.wait() 286 except OSError, message: 287 Datasets.snapshotslock.release() 288 raise RuntimeError, "%s subprocess error:\n %s" % \ 289 (cmd, str(message)) 290 if err != 0: 291 Datasets.snapshotslock.release() 292 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 293 (str(cmd), err, errdata) 294 for dataset in outdata.rstrip().split('\n'): 295 if re.search("@", dataset): 296 insort(snaps, dataset.split()) 297 for snap in snaps: 298 Datasets.snapshots.append([snap[1], long(snap[0])]) 299 if pattern == None: 300 snapshots = Datasets.snapshots[:] 301 else: 302 # Regular expression pattern to match "pattern" parameter. 303 regexpattern = ".*@.*%s" % pattern 304 patternobj = re.compile(regexpattern) 305 306 for snapname,snaptime in Datasets.snapshots: 307 patternmatchobj = re.match(patternobj, snapname) 308 if patternmatchobj != None: 309 snapshots.append([snapname, snaptime]) 310 Datasets.snapshotslock.release() 311 return snapshots 312 313 def list_cloned_snapshots(self): 314 """ 315 Returns a list of snapshots that have cloned filesystems 316 dependent on them. 317 Snapshots with cloned filesystems can not be destroyed 318 unless dependent cloned filesystems are first destroyed. 319 """ 320 cmd = [ZFSCMD, "list", "-H", "-o", "origin"] 321 try: 322 p = subprocess.Popen(cmd, 323 stdout=subprocess.PIPE, 324 stderr=subprocess.PIPE, 325 close_fds=True) 326 outdata,errdata = p.communicate() 327 err = p.wait() 328 except OSError, message: 329 raise RuntimeError, "%s subprocess error:\n %s" % \ 330 (cmd, str(message)) 331 if err != 0: 332 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 333 (str(cmd), err, errdata) 334 result = [] 335 for line in outdata.rstrip().split('\n'): 336 details = line.rstrip() 337 if details != "-": 338 try: 339 result.index(details) 340 except ValueError: 341 result.append(details) 342 return result 343 344 def refresh_snapshots(self): 345 """ 346 Should be called when snapshots have been created or deleted 347 and a rescan should be performed. Rescan gets deferred until 348 next invocation of zfs.Dataset.list_snapshots() 349 """ 350 # FIXME in future. 351 # This is a little sub-optimal because we should be able to modify 352 # the snapshot list in place in some situations and regenerate the 353 # snapshot list without calling out to zfs(1m). But on the 354 # pro side, we will pick up any new snapshots since the last 355 # scan that we would be otherwise unaware of. 356 Datasets.snapshotslock.acquire() 357 Datasets.snapshots = None 358 Datasets.snapshotslock.release() 359 360 361 class ZPool(Exception): 362 """ 363 Base class for ZFS storage pool objects 364 """ 365 def __init__(self, name): 366 self.name = name 367 self.health = self.__get_health() 368 self.__datasets = Datasets() 369 self.__filesystems = None 370 self.__volumes = None 371 self.__snapshots = None 372 373 def __get_health(self): 374 """ 375 Returns pool health status: 'ONLINE', 'DEGRADED' or 'FAULTED' 376 """ 377 cmd = [ZPOOLCMD, "list", "-H", "-o", "health", self.name] 378 try: 379 p = subprocess.Popen(cmd, 380 stdout=subprocess.PIPE, 381 stderr=subprocess.PIPE, 382 close_fds=True) 383 outdata,errdata = p.communicate() 384 err = p.wait() 385 except OSError, message: 386 raise RuntimeError, "%s subprocess error:\n %s" % \ 387 (cmd, str(message)) 388 if err != 0: 389 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 390 (str(cmd), err, errdata) 391 result = outdata.rstrip() 392 return result 393 394 def get_capacity(self): 395 """ 396 Returns the percentage of total pool storage in use. 397 Calculated based on the "used" and "available" properties 398 of the pool's top-level filesystem because the values account 399 for reservations and quotas of children in their calculations, 400 giving a more practical indication of how much capacity is used 401 up on the pool. 402 """ 403 if self.health == "FAULTED": 404 raise "PoolFaulted" 405 406 cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value", \ 407 "used,available", self.name] 408 try: 409 p = subprocess.Popen(cmd, 410 stdout=subprocess.PIPE, 411 stderr=subprocess.PIPE, 412 close_fds=True) 413 outdata,errdata = p.communicate() 414 err = p.wait() 415 except OSError, message: 416 raise RuntimeError, "%s subprocess error:\n %s" % \ 417 (cmd, str(message)) 418 if err != 0: 419 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 420 (str(cmd), err, errdata) 421 422 _used,_available = outdata.rstrip().split('\n') 423 used = float(_used) 424 available = float(_available) 425 return 100.0 * used/(used + available) 426 427 def get_available_size(self): 428 """ 429 How much unused space is available for use on this Zpool. 430 Answer in bytes. 431 """ 432 # zpool(1) doesn't report available space in 433 # units suitable for calulations but zfs(1) 434 # can so use it to find the value for the 435 # filesystem matching the pool. 436 # The root filesystem of the pool is simply 437 # the pool name. 438 poolfs = Filesystem(self.name) 439 avail = poolfs.get_available_size() 440 return avail 441 442 def get_used_size(self): 443 """ 444 How much space is in use on this Zpool. 445 Answer in bytes 446 """ 447 # Same as ZPool.get_available_size(): zpool(1) 448 # doesn't generate suitable out put so use 449 # zfs(1) on the toplevel filesystem 450 if self.health == "FAULTED": 451 raise "PoolFaulted" 452 poolfs = Filesystem(self.name) 453 used = poolfs.get_used_size() 454 return used 455 456 def list_filesystems(self): 457 """ 458 Return a list of filesystems on this Zpool. 459 List is sorted by name. 460 """ 461 if self.__filesystems == None: 462 result = [] 463 # Provides pre-sorted filesystem list 464 for fsname,fsmountpoint in self.__datasets.list_filesystems(): 465 if re.match(self.name, fsname): 466 result.append([fsname, fsmountpoint]) 467 self.__filesystems = result 468 return self.__filesystems 469 470 def list_volumes(self): 471 """ 472 Return a list of volumes (zvol) on this Zpool 473 List is sorted by name 474 """ 475 if self.__volumes == None: 476 result = [] 477 regexpattern = "^%s" % self.name 478 patternobj = re.compile(regexpattern) 479 for volname in self.__datasets.list_volumes(): 480 patternmatchobj = re.match(patternobj, volname) 481 if patternmatchobj != None: 482 result.append(volname) 483 result.sort() 484 self.__volumes = result 485 return self.__volumes 486 487 def list_snapshots(self, pattern = None): 488 """ 489 List pattern matching snapshots sorted by creation date. 490 Oldest listed first 491 492 Keyword arguments: 493 pattern -- Filter according to pattern (default None) 494 """ 495 # If there isn't a list of snapshots for this dataset 496 # already, create it now and store it in order to save 497 # time later for potential future invocations. 498 Datasets.snapshotslock.acquire() 499 if Datasets.snapshots == None: 500 self.__snapshots = None 501 Datasets.snapshotslock.release() 502 if self.__snapshots == None: 503 result = [] 504 regexpattern = "^%s.*@" % self.name 505 patternobj = re.compile(regexpattern) 506 for snapname,snaptime in self.__datasets.list_snapshots(): 507 patternmatchobj = re.match(patternobj, snapname) 508 if patternmatchobj != None: 509 result.append([snapname, snaptime]) 510 # Results already sorted by creation time 511 self.__snapshots = result 512 if pattern == None: 513 return self.__snapshots 514 else: 515 snapshots = [] 516 regexpattern = "^%s.*@.*%s" % (self.name, pattern) 517 patternobj = re.compile(regexpattern) 518 for snapname,snaptime in self.__snapshots: 519 patternmatchobj = re.match(patternobj, snapname) 520 if patternmatchobj != None: 521 snapshots.append([snapname, snaptime]) 522 return snapshots 523 524 def __str__(self): 525 return_string = "ZPool name: " + self.name 526 return_string = return_string + "\n\tHealth: " + self.health 527 try: 528 return_string = return_string + \ 529 "\n\tUsed: " + \ 530 str(self.get_used_size()/BYTESPERMB) + "Mb" 531 return_string = return_string + \ 532 "\n\tAvailable: " + \ 533 str(self.get_available_size()/BYTESPERMB) + "Mb" 534 return_string = return_string + \ 535 "\n\tCapacity: " + \ 536 str(self.get_capacity()) + "%" 537 except "PoolFaulted": 538 pass 539 return return_string 540 541 542 class ReadableDataset: 543 """ 544 Base class for Filesystem, Volume and Snapshot classes 545 Provides methods for read only operations common to all. 546 """ 547 def __init__(self, name, creation = None): 548 self.name = name 549 self.__creationTime = creation 550 self.datasets = Datasets() 551 552 def __str__(self): 553 return_string = "ReadableDataset name: " + self.name + "\n" 554 return return_string 555 556 def get_creation_time(self): 557 if self.__creationTime == None: 558 cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value", "creation", 559 self.name] 560 try: 561 p = subprocess.Popen(cmd, 562 stdout=subprocess.PIPE, 563 stderr=subprocess.PIPE, 564 close_fds=True) 565 outdata,errdata = p.communicate() 566 err = p.wait() 567 except OSError, message: 568 raise RuntimeError, "%s subprocess error:\n %s" % \ 569 (cmd, str(message)) 570 if err != 0: 571 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 572 (str(cmd), err, errdata) 573 self.__creationTime = long(outdata.rstrip()) 574 return self.__creationTime 575 576 def exists(self): 577 """ 578 Returns True if the dataset is still existent on the system. 579 False otherwise 580 """ 581 # Test existance of the dataset by checking the output of a 582 # simple zfs get command on the snapshot 583 cmd = [ZFSCMD, "get", "-H", "-o", "name", "type", self.name] 584 try: 585 p = subprocess.Popen(cmd, 586 stdout=subprocess.PIPE, 587 stderr=subprocess.PIPE, 588 close_fds=True) 589 outdata,errdata = p.communicate() 590 err = p.wait() 591 except OSError, message: 592 raise RuntimeError, "%s subprocess error:\n %s" % \ 593 (cmd, str(message)) 594 if err != 0: 595 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 596 (str(cmd), err, errdata) 597 result = outdata.rstrip() 598 if result == self.name: 599 return True 600 else: 601 return False 602 603 def get_used_size(self): 604 cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value", "used", self.name] 605 try: 606 p = subprocess.Popen(cmd, 607 stdout=subprocess.PIPE, 608 stderr=subprocess.PIPE, 609 close_fds=True) 610 outdata,errdata = p.communicate() 611 err = p.wait() 612 except OSError, message: 613 raise RuntimeError, "%s subprocess error:\n %s" % \ 614 (cmd, str(message)) 615 if err != 0: 616 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 617 (str(cmd), err, errdata) 618 return long(outdata.rstrip()) 619 620 621 class Snapshot(ReadableDataset, Exception): 622 """ 623 ZFS Snapshot object class. 624 Provides information and operations specfic to ZFS snapshots 625 """ 626 def __init__(self, name, creation = None): 627 """ 628 Keyword arguments: 629 name -- Name of the ZFS snapshot 630 creation -- Creation time of the snapshot if known (Default None) 631 """ 632 ReadableDataset.__init__(self, name, creation) 633 self.fsname, self.snaplabel = self.__split_snapshot_name() 634 self.poolname = self.__get_pool_name() 635 636 def __get_pool_name(self): 637 name = self.fsname.split("/", 1) 638 return name[0] 639 640 def __split_snapshot_name(self): 641 name = self.name.split("@", 1) 642 # Make sure this is really a snapshot and not a 643 # filesystem otherwise a filesystem could get 644 # destroyed instead of a snapshot. That would be 645 # really really bad. 646 if name[0] == self.name: 647 raise 'SnapshotError', "%s is not a valid snapshot name\n" % (name) 648 return name[0],name[1] 649 650 def get_referenced_size(self): 651 """ 652 How much unique storage space is used by this snapshot. 653 Answer in bytes 654 """ 655 cmd = [ZFSCMD, "get", "-H", "-p", \ 656 "-o", "value", "referenced", \ 657 self.name] 658 try: 659 p = subprocess.Popen(cmd, 660 stdout=subprocess.PIPE, 661 stderr=subprocess.PIPE, 662 close_fds=True) 663 outdata,errdata = p.communicate() 664 err = p.wait() 665 except OSError, message: 666 raise RuntimeError, "%s subprocess error:\n %s" % \ 667 (cmd, str(message)) 668 if err != 0: 669 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 670 (str(cmd), err, errdata) 671 return long(outdata.rstrip()) 672 673 def list_children(self): 674 """Returns a recursive list of child snapshots of this snapshot""" 675 cmd = [ZFSCMD, 676 "list", "-t", "snapshot", "-H", "-r", "-o", "name", 677 self.fsname] 678 try: 679 p = subprocess.Popen(cmd, 680 stdout=subprocess.PIPE, 681 stderr=subprocess.PIPE, 682 close_fds=True) 683 outdata,errdata = p.communicate() 684 err = p.wait() 685 except OSError, message: 686 raise RuntimeError, "%s subprocess error:\n %s" % \ 687 (cmd, str(message)) 688 if err != 0: 689 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 690 (str(cmd), err, errdata) 691 result = [] 692 for line in outdata.rstrip().split('\n'): 693 if re.search("@%s" % (self.snaplabel), line) and \ 694 line != self.name: 695 result.append(line) 696 return result 697 698 def has_clones(self): 699 """Returns True if the snapshot has any dependent clones""" 700 cmd = [ZFSCMD, "list", "-H", "-o", "origin,name"] 701 try: 702 p = subprocess.Popen(cmd, 703 stdout=subprocess.PIPE, 704 stderr=subprocess.PIPE, 705 close_fds=True) 706 outdata,errdata = p.communicate() 707 err = p.wait() 708 except OSError, message: 709 raise RuntimeError, "%s subprocess error:\n %s" % \ 710 (cmd, str(message)) 711 if err != 0: 712 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 713 (str(cmd), err, errdata) 714 for line in outdata.rstrip().split('\n'): 715 details = line.rstrip().split() 716 if details[0] == self.name and \ 717 details[1] != '-': 718 return True 719 return False 720 721 def destroy_snapshot(self, recursive=False): 722 """Permanently remove this snapshot from the filesystem""" 723 # Be sure it genuninely exists before trying to destroy it 724 if self.exists() == False: 725 return 726 cmd = [PFCMD, ZFSCMD, "destroy", self.name] 727 try: 728 p = subprocess.Popen(cmd, 729 stdout=subprocess.PIPE, 730 stderr=subprocess.PIPE, 731 close_fds=True) 732 outdata,errdata = p.communicate() 733 err = p.wait() 734 except OSError, message: 735 raise RuntimeError, "%s subprocess error:\n %s" % \ 736 (cmd, str(message)) 737 if err != 0: 738 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 739 (str(cmd), err, errdata) 740 # Clear the global snapshot cache so that a rescan will be 741 # triggered on the next call to Datasets.list_snapshots() 742 self.datasets.refresh_snapshots() 743 744 def __str__(self): 745 return_string = "Snapshot name: " + self.name 746 return_string = return_string + "\n\tCreation time: " \ 747 + str(self.get_creation_time()) 748 return_string = return_string + "\n\tUsed Size: " \ 749 + str(self.get_used_size()) 750 return_string = return_string + "\n\tReferenced Size: " \ 751 + str(self.get_referenced_size()) 752 return return_string 753 754 755 class ReadWritableDataset(ReadableDataset): 756 """ 757 Base class for ZFS filesystems and volumes. 758 Provides methods for operations and properties 759 common to both filesystems and volumes. 760 """ 761 def __init__(self, name, creation = None): 762 ReadableDataset.__init__(self, name, creation) 763 self.__snapshots = None 764 765 def __str__(self): 766 return_string = "ReadWritableDataset name: " + self.name + "\n" 767 return return_string 768 769 def get_auto_snap(self, schedule = None): 770 if schedule: 771 cmd = [ZFSCMD, "get", "-H", "-o", "value", \ 772 "com.sun:auto-snapshot", self.name] 773 cmd = [ZFSCMD, "get", "-H", "-o", "value", \ 774 "com.sun:auto-snapshot", self.name] 775 try: 776 p = subprocess.Popen(cmd, 777 stdout=subprocess.PIPE, 778 stderr=subprocess.PIPE, 779 close_fds=True) 780 outdata,errdata = p.communicate() 781 err = p.wait() 782 except OSError, message: 783 raise RuntimeError, "%s subprocess error:\n %s" % \ 784 (cmd, str(message)) 785 if err != 0: 786 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 787 (str(cmd), err, errdata) 788 if outdata.rstrip() == "true": 789 return True 790 else: 791 return False 792 793 def get_available_size(self): 794 cmd = [ZFSCMD, "get", "-H", "-p", "-o", "value", "available", \ 795 self.name] 796 try: 797 p = subprocess.Popen(cmd, 798 stdout=subprocess.PIPE, 799 stderr=subprocess.PIPE, 800 close_fds=True) 801 outdata,errdata = p.communicate() 802 err = p.wait() 803 except OSError, message: 804 raise RuntimeError, "%s subprocess error:\n %s" % \ 805 (cmd, str(message)) 806 if err != 0: 807 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 808 (str(cmd), err, errdata) 809 return long(outdata.rstrip()) 810 811 def create_snapshot(self, snaplabel, recursive = False): 812 """ 813 Create a snapshot for the ReadWritable dataset using the supplied 814 snapshot label. 815 816 Keyword Arguments: 817 snaplabel: 818 A string to use as the snapshot label. 819 The bit that comes after the "@" part of the snapshot 820 name. 821 recursive: 822 Recursively snapshot childfren of this dataset. 823 Default = False 824 """ 825 cmd = [PFCMD, ZFSCMD, "snapshot"] 826 if recursive == True: 827 cmd.append("-r") 828 cmd.append("%s@%s" % (self.name, snaplabel)) 829 try: 830 p = subprocess.Popen(cmd, 831 stdout=subprocess.PIPE, 832 stderr=subprocess.PIPE, 833 close_fds=True) 834 outdata,errdata = p.communicate() 835 err = p.wait() 836 except OSError, message: 837 raise RuntimeError, "%s subprocess error:\n %s" % \ 838 (cmd, str(message)) 839 if err != 0: 840 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 841 (str(cmd), err, errdata) 842 self.datasets.refresh_snapshots() 843 844 def list_children(self): 845 846 # Note, if more dataset types ever come around they will 847 # need to be added to the filsystem,volume args below. 848 # Not for the forseeable future though. 849 cmd = [ZFSCMD, "list", "-H", "-r", "-t", "filesystem,volume", 850 "-o", "name", self.name] 851 try: 852 p = subprocess.Popen(cmd, 853 stdout=subprocess.PIPE, 854 stderr=subprocess.PIPE, 855 close_fds=True) 856 outdata,errdata = p.communicate() 857 err = p.wait() 858 except OSError, message: 859 raise RuntimeError, "%s subprocess error:\n %s" % \ 860 (cmd, str(message)) 861 if err != 0: 862 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 863 (str(cmd), err, errdata) 864 result = [] 865 for line in outdata.rstrip().split('\n'): 866 if line.rstrip() != self.name: 867 result.append(line.rstrip()) 868 return result 869 870 871 def list_snapshots(self, pattern = None): 872 """ 873 List pattern matching snapshots sorted by creation date. 874 Oldest listed first 875 876 Keyword arguments: 877 pattern -- Filter according to pattern (default None) 878 """ 879 # If there isn't a list of snapshots for this dataset 880 # already, create it now and store it in order to save 881 # time later for potential future invocations. 882 Datasets.snapshotslock.acquire() 883 if Datasets.snapshots == None: 884 self.__snapshots = None 885 Datasets.snapshotslock.release() 886 if self.__snapshots == None: 887 result = [] 888 regexpattern = "^%s@" % self.name 889 patternobj = re.compile(regexpattern) 890 for snapname,snaptime in self.datasets.list_snapshots(): 891 patternmatchobj = re.match(patternobj, snapname) 892 if patternmatchobj != None: 893 result.append([snapname, snaptime]) 894 # Results already sorted by creation time 895 self.__snapshots = result 896 if pattern == None: 897 return self.__snapshots 898 else: 899 snapshots = [] 900 regexpattern = "^%s@.*%s" % (self.name, pattern) 901 patternobj = re.compile(regexpattern) 902 for snapname,snaptime in self.__snapshots: 903 patternmatchobj = re.match(patternobj, snapname) 904 if patternmatchobj != None: 905 snapshots.append(snapname) 906 return snapshots 907 908 def set_auto_snap(self, include, inherit = False): 909 if inherit == True: 910 cmd = [PFCMD, ZFSCMD, "inherit", "com.sun:auto-snapshot", \ 911 self.name] 912 elif include == True: 913 cmd = [PFCMD, ZFSCMD, "set", "com.sun:auto-snapshot=true", \ 914 self.name] 915 else: 916 cmd = [PFCMD, ZFSCMD, "set", "com.sun:auto-snapshot=false", \ 917 self.name] 918 try: 919 p = subprocess.Popen(cmd, 920 stdout=subprocess.PIPE, 921 stderr=subprocess.PIPE, 922 close_fds=True) 923 outdata,errdata = p.communicate() 924 err = p.wait() 925 except OSError, message: 926 raise RuntimeError, "%s subprocess error:\n %s" % \ 927 (cmd, str(message)) 928 if err != 0: 929 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 930 (str(cmd), err, errdata) 931 return 932 933 934 class Filesystem(ReadWritableDataset): 935 """ZFS Filesystem class""" 936 def __init__(self, name, mountpoint = None): 937 ReadWritableDataset.__init__(self, name) 938 self.__mountpoint = mountpoint 939 940 def __str__(self): 941 return_string = "Filesystem name: " + self.name + \ 942 "\n\tMountpoint: " + self.get_mountpoint() + \ 943 "\n\tAuto snap: " 944 if self.get_auto_snap(): 945 return_string = return_string + "TRUE" 946 else: 947 return_string = return_string + "FALSE" 948 return_string = return_string + "\n" 949 return return_string 950 951 def get_mountpoint(self): 952 if (self.__mountpoint == None): 953 cmd = [ZFSCMD, "get", "-H", "-o", "value", "mountpoint", \ 954 self.name] 955 try: 956 p = subprocess.Popen(cmd, 957 stdout=subprocess.PIPE, 958 stderr=subprocess.PIPE, 959 close_fds=True) 960 outdata,errdata = p.communicate() 961 err = p.wait() 962 except OSError, message: 963 raise RuntimeError, "%s subprocess error:\n %s" % \ 964 (cmd, str(message)) 965 if err != 0: 966 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 967 (str(cmd), err, errdata) 968 result = outdata.rstrip() 969 self.__mountpoint = result 970 return self.__mountpoint 971 972 def list_children(self): 973 cmd = [ZFSCMD, "list", "-H", "-r", "-t", "filesystem", "-o", "name", 974 self.name] 975 try: 976 p = subprocess.Popen(cmd, 977 stdout=subprocess.PIPE, 978 stderr=subprocess.PIPE, 979 close_fds=True) 980 outdata,errdata = p.communicate() 981 err = p.wait() 982 except OSError, message: 983 raise RuntimeError, "%s subprocess error:\n %s" % \ 984 (cmd, str(message)) 985 if err != 0: 986 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 987 (str(cmd), err, errdata) 988 result = [] 989 for line in outdata.rstrip().split('\n'): 990 if line.rstrip() != self.name: 991 result.append(line.rstrip()) 992 return result 993 994 995 class Volume(ReadWritableDataset): 996 """ 997 ZFS Volume Class 998 This is basically just a stub and does nothing 999 unique from ReadWritableDataset parent class. 1000 """ 1001 def __init__(self, name): 1002 ReadWritableDataset.__init__(self, name) 1003 1004 def __str__(self): 1005 return_string = "Volume name: " + self.name + "\n" 1006 return return_string 1007 1008 1009 def list_zpools(): 1010 """Returns a list of all zpools on the system""" 1011 result = [] 1012 cmd = [ZPOOLCMD, "list", "-H", "-o", "name"] 1013 try: 1014 p = subprocess.Popen(cmd, 1015 stdout=subprocess.PIPE, 1016 stderr=subprocess.PIPE, 1017 close_fds = 0) 1018 outdata,errdata = p.communicate() 1019 err = p.wait() 1020 except OSError, message: 1021 raise RuntimeError, "%s subprocess error:\n %s" % \ 1022 (cmd, str(message)) 1023 1024 if err != 0: 1025 raise RuntimeError, '%s failed with exit code %d\n%s' % \ 1026 (str(cmd), err, errdata) 1027 for line in outdata.rstrip().split('\n'): 1028 result.append(line.rstrip()) 1029 return result 1030 1031 1032 if __name__ == "__main__": 1033 for zpool in list_zpools(): 1034 pool = ZPool(zpool) 1035 for filesys,mountpoint in pool.list_filesystems(): 1036 fs = Filesystem(filesys, mountpoint) 1037 print fs 1038 print "\tSnapshots:" 1039 for snapshot, snaptime in fs.list_snapshots(): 1040 snap = Snapshot(snapshot, snaptime) 1041 print "\t" + snap.name 1042 print "\n" 1043 for volname in pool.list_volumes(): 1044 vol = Volume(volname) 1045 print vol 1046 print "\tSnapshots:" 1047 for snapshot, snaptime in vol.list_snapshots(): 1048 snap = Snapshot(snapshot, snaptime) 1049 print "\t" + snap.name 1050 print "\n" 1051 1052