1""" 2Utility functions for zfs 3 4These functions are for dealing with type conversion and basic execution 5 6:maintainer: Jorge Schrauwen <sjorge@blackdot.be> 7:maturity: new 8:depends: salt.utils.stringutils, salt.ext, salt.module.cmdmod 9:platform: illumos,freebsd,linux 10 11.. versionadded:: 2018.3.1 12 13""" 14 15 16import logging 17import math 18import os 19import re 20from numbers import Number 21 22import salt.modules.cmdmod 23from salt.utils.decorators import memoize as real_memoize 24from salt.utils.odict import OrderedDict 25from salt.utils.stringutils import to_num as str_to_num 26 27# Size conversion data 28re_zfs_size = re.compile(r"^(\d+|\d+(?=\d*)\.\d+)([KkMmGgTtPpEe][Bb]?)$") 29zfs_size = ["K", "M", "G", "T", "P", "E"] 30 31log = logging.getLogger(__name__) 32 33 34def _check_retcode(cmd): 35 """ 36 Simple internal wrapper for cmdmod.retcode 37 """ 38 return ( 39 salt.modules.cmdmod.retcode(cmd, output_loglevel="quiet", ignore_retcode=True) 40 == 0 41 ) 42 43 44def _exec(**kwargs): 45 """ 46 Simple internal wrapper for cmdmod.run 47 """ 48 if "ignore_retcode" not in kwargs: 49 kwargs["ignore_retcode"] = True 50 if "output_loglevel" not in kwargs: 51 kwargs["output_loglevel"] = "quiet" 52 return salt.modules.cmdmod.run_all(**kwargs) 53 54 55def _merge_last(values, merge_after, merge_with=" "): 56 """ 57 Merge values all values after X into the last value 58 """ 59 if len(values) > merge_after: 60 values = values[0 : (merge_after - 1)] + [ 61 merge_with.join(values[(merge_after - 1) :]) 62 ] 63 64 return values 65 66 67def _property_normalize_name(name): 68 """ 69 Normalizes property names 70 """ 71 if "@" in name: 72 name = name[: name.index("@") + 1] 73 return name 74 75 76def _property_detect_type(name, values): 77 """ 78 Detect the datatype of a property 79 """ 80 value_type = "str" 81 if values.startswith("on | off"): 82 value_type = "bool" 83 elif values.startswith("yes | no"): 84 value_type = "bool_alt" 85 elif values in ["<size>", "<size> | none"]: 86 value_type = "size" 87 elif values in ["<count>", "<count> | none", "<guid>"]: 88 value_type = "numeric" 89 elif name in ["sharenfs", "sharesmb", "canmount"]: 90 value_type = "bool" 91 elif name in ["version", "copies"]: 92 value_type = "numeric" 93 return value_type 94 95 96def _property_create_dict(header, data): 97 """ 98 Create a property dict 99 """ 100 prop = dict(zip(header, _merge_last(data, len(header)))) 101 prop["name"] = _property_normalize_name(prop["property"]) 102 prop["type"] = _property_detect_type(prop["name"], prop["values"]) 103 prop["edit"] = from_bool(prop["edit"]) 104 if "inherit" in prop: 105 prop["inherit"] = from_bool(prop["inherit"]) 106 del prop["property"] 107 return prop 108 109 110def _property_parse_cmd(cmd, alias=None): 111 """ 112 Parse output of zpool/zfs get command 113 """ 114 if not alias: 115 alias = {} 116 properties = {} 117 118 # NOTE: append get to command 119 if cmd[-3:] != "get": 120 cmd += " get" 121 122 # NOTE: parse output 123 prop_hdr = [] 124 for prop_data in _exec(cmd=cmd)["stderr"].split("\n"): 125 # NOTE: make the line data more manageable 126 prop_data = prop_data.lower().split() 127 128 # NOTE: skip empty lines 129 if not prop_data: 130 continue 131 # NOTE: parse header 132 elif prop_data[0] == "property": 133 prop_hdr = prop_data 134 continue 135 # NOTE: skip lines after data 136 elif not prop_hdr or prop_data[1] not in ["no", "yes"]: 137 continue 138 139 # NOTE: create property dict 140 prop = _property_create_dict(prop_hdr, prop_data) 141 142 # NOTE: add property to dict 143 properties[prop["name"]] = prop 144 if prop["name"] in alias: 145 properties[alias[prop["name"]]] = prop 146 147 # NOTE: cleanup some duplicate data 148 del prop["name"] 149 return properties 150 151 152def _auto(direction, name, value, source="auto", convert_to_human=True): 153 """ 154 Internal magic for from_auto and to_auto 155 """ 156 # NOTE: check direction 157 if direction not in ["to", "from"]: 158 return value 159 160 # NOTE: collect property data 161 props = property_data_zpool() 162 if source == "zfs": 163 props = property_data_zfs() 164 elif source == "auto": 165 props.update(property_data_zfs()) 166 167 # NOTE: figure out the conversion type 168 value_type = props[name]["type"] if name in props else "str" 169 170 # NOTE: convert 171 if value_type == "size" and direction == "to": 172 return globals()["{}_{}".format(direction, value_type)](value, convert_to_human) 173 174 return globals()["{}_{}".format(direction, value_type)](value) 175 176 177@real_memoize 178def _zfs_cmd(): 179 """ 180 Return the path of the zfs binary if present 181 """ 182 # Get the path to the zfs binary. 183 return salt.utils.path.which("zfs") 184 185 186@real_memoize 187def _zpool_cmd(): 188 """ 189 Return the path of the zpool binary if present 190 """ 191 # Get the path to the zfs binary. 192 return salt.utils.path.which("zpool") 193 194 195def _command( 196 source, 197 command, 198 flags=None, 199 opts=None, 200 property_name=None, 201 property_value=None, 202 filesystem_properties=None, 203 pool_properties=None, 204 target=None, 205): 206 """ 207 Build and properly escape a zfs command 208 209 .. note:: 210 211 Input is not considered safe and will be passed through 212 to_auto(from_auto('input_here')), you do not need to do so 213 your self first. 214 215 """ 216 # NOTE: start with the zfs binary and command 217 cmd = [_zpool_cmd() if source == "zpool" else _zfs_cmd(), command] 218 219 # NOTE: append flags if we have any 220 if flags is None: 221 flags = [] 222 for flag in flags: 223 cmd.append(flag) 224 225 # NOTE: append options 226 # we pass through 'sorted' to guarantee the same order 227 if opts is None: 228 opts = {} 229 for opt in sorted(opts): 230 if not isinstance(opts[opt], list): 231 opts[opt] = [opts[opt]] 232 for val in opts[opt]: 233 cmd.append(opt) 234 cmd.append(to_str(val)) 235 236 # NOTE: append filesystem properties (really just options with a key/value) 237 # we pass through 'sorted' to guarantee the same order 238 if filesystem_properties is None: 239 filesystem_properties = {} 240 for fsopt in sorted(filesystem_properties): 241 cmd.append("-O" if source == "zpool" else "-o") 242 cmd.append( 243 "{key}={val}".format( 244 key=fsopt, 245 val=to_auto( 246 fsopt, 247 filesystem_properties[fsopt], 248 source="zfs", 249 convert_to_human=False, 250 ), 251 ) 252 ) 253 254 # NOTE: append pool properties (really just options with a key/value) 255 # we pass through 'sorted' to guarantee the same order 256 if pool_properties is None: 257 pool_properties = {} 258 for fsopt in sorted(pool_properties): 259 cmd.append("-o") 260 cmd.append( 261 "{key}={val}".format( 262 key=fsopt, 263 val=to_auto( 264 fsopt, 265 pool_properties[fsopt], 266 source="zpool", 267 convert_to_human=False, 268 ), 269 ) 270 ) 271 272 # NOTE: append property and value 273 # the set command takes a key=value pair, we need to support this 274 if property_name is not None: 275 if property_value is not None: 276 if not isinstance(property_name, list): 277 property_name = [property_name] 278 if not isinstance(property_value, list): 279 property_value = [property_value] 280 for key, val in zip(property_name, property_value): 281 cmd.append( 282 "{key}={val}".format( 283 key=key, 284 val=to_auto(key, val, source=source, convert_to_human=False), 285 ) 286 ) 287 else: 288 cmd.append(property_name) 289 290 # NOTE: append the target(s) 291 if target is not None: 292 if not isinstance(target, list): 293 target = [target] 294 for tgt in target: 295 # NOTE: skip None list items 296 # we do not want to skip False and 0! 297 if tgt is None: 298 continue 299 cmd.append(to_str(tgt)) 300 301 return " ".join(cmd) 302 303 304def is_supported(): 305 """ 306 Check the system for ZFS support 307 """ 308 # Check for supported platforms 309 # NOTE: ZFS on Windows is in development 310 # NOTE: ZFS on NetBSD is in development 311 on_supported_platform = False 312 if salt.utils.platform.is_sunos(): 313 on_supported_platform = True 314 elif salt.utils.platform.is_freebsd() and _check_retcode("kldstat -q -m zfs"): 315 on_supported_platform = True 316 elif salt.utils.platform.is_linux() and os.path.exists("/sys/module/zfs"): 317 on_supported_platform = True 318 elif salt.utils.platform.is_linux() and salt.utils.path.which("zfs-fuse"): 319 on_supported_platform = True 320 elif ( 321 salt.utils.platform.is_darwin() 322 and os.path.exists("/Library/Extensions/zfs.kext") 323 and os.path.exists("/dev/zfs") 324 ): 325 on_supported_platform = True 326 327 # Additional check for the zpool command 328 return (salt.utils.path.which("zpool") and on_supported_platform) is True 329 330 331@real_memoize 332def has_feature_flags(): 333 """ 334 Check if zpool-features is available 335 """ 336 # get man location 337 man = salt.utils.path.which("man") 338 return _check_retcode("{man} zpool-features".format(man=man)) if man else False 339 340 341@real_memoize 342def property_data_zpool(): 343 """ 344 Return a dict of zpool properties 345 346 .. note:: 347 348 Each property will have an entry with the following info: 349 - edit : boolean - is this property editable after pool creation 350 - type : str - either bool, bool_alt, size, numeric, or string 351 - values : str - list of possible values 352 353 .. warning:: 354 355 This data is probed from the output of 'zpool get' with some supplemental 356 data that is hardcoded. There is no better way to get this information aside 357 from reading the code. 358 359 """ 360 # NOTE: man page also mentions a few short forms 361 property_data = _property_parse_cmd( 362 _zpool_cmd(), 363 { 364 "allocated": "alloc", 365 "autoexpand": "expand", 366 "autoreplace": "replace", 367 "listsnapshots": "listsnaps", 368 "fragmentation": "frag", 369 }, 370 ) 371 372 # NOTE: zpool status/iostat has a few extra fields 373 zpool_size_extra = [ 374 "capacity-alloc", 375 "capacity-free", 376 "operations-read", 377 "operations-write", 378 "bandwidth-read", 379 "bandwidth-write", 380 "read", 381 "write", 382 ] 383 zpool_numeric_extra = [ 384 "cksum", 385 "cap", 386 ] 387 388 for prop in zpool_size_extra: 389 property_data[prop] = { 390 "edit": False, 391 "type": "size", 392 "values": "<size>", 393 } 394 395 for prop in zpool_numeric_extra: 396 property_data[prop] = { 397 "edit": False, 398 "type": "numeric", 399 "values": "<count>", 400 } 401 402 return property_data 403 404 405@real_memoize 406def property_data_zfs(): 407 """ 408 Return a dict of zfs properties 409 410 .. note:: 411 412 Each property will have an entry with the following info: 413 - edit : boolean - is this property editable after pool creation 414 - inherit : boolean - is this property inheritable 415 - type : str - either bool, bool_alt, size, numeric, or string 416 - values : str - list of possible values 417 418 .. warning:: 419 420 This data is probed from the output of 'zfs get' with some supplemental 421 data that is hardcoded. There is no better way to get this information aside 422 from reading the code. 423 424 """ 425 return _property_parse_cmd( 426 _zfs_cmd(), 427 { 428 "available": "avail", 429 "logicalreferenced": "lrefer.", 430 "logicalused": "lused.", 431 "referenced": "refer", 432 "volblocksize": "volblock", 433 "compression": "compress", 434 "readonly": "rdonly", 435 "recordsize": "recsize", 436 "refreservation": "refreserv", 437 "reservation": "reserv", 438 }, 439 ) 440 441 442def from_numeric(value): 443 """ 444 Convert zfs numeric to python int 445 """ 446 if value == "none": 447 value = None 448 elif value: 449 value = str_to_num(value) 450 return value 451 452 453def to_numeric(value): 454 """ 455 Convert python int to zfs numeric 456 """ 457 value = from_numeric(value) 458 if value is None: 459 value = "none" 460 return value 461 462 463def from_bool(value): 464 """ 465 Convert zfs bool to python bool 466 """ 467 if value in ["on", "yes"]: 468 value = True 469 elif value in ["off", "no"]: 470 value = False 471 elif value == "none": 472 value = None 473 474 return value 475 476 477def from_bool_alt(value): 478 """ 479 Convert zfs bool_alt to python bool 480 """ 481 return from_bool(value) 482 483 484def to_bool(value): 485 """ 486 Convert python bool to zfs on/off bool 487 """ 488 value = from_bool(value) 489 if isinstance(value, bool): 490 value = "on" if value else "off" 491 elif value is None: 492 value = "none" 493 494 return value 495 496 497def to_bool_alt(value): 498 """ 499 Convert python to zfs yes/no value 500 """ 501 value = from_bool_alt(value) 502 if isinstance(value, bool): 503 value = "yes" if value else "no" 504 elif value is None: 505 value = "none" 506 507 return value 508 509 510def from_size(value): 511 """ 512 Convert zfs size (human readable) to python int (bytes) 513 """ 514 match_size = re_zfs_size.match(str(value)) 515 if match_size: 516 v_unit = match_size.group(2).upper()[0] 517 v_size = float(match_size.group(1)) 518 v_multiplier = math.pow(1024, zfs_size.index(v_unit) + 1) 519 value = v_size * v_multiplier 520 if int(value) == value: 521 value = int(value) 522 elif value is not None: 523 value = str(value) 524 525 return from_numeric(value) 526 527 528def to_size(value, convert_to_human=True): 529 """ 530 Convert python int (bytes) to zfs size 531 532 NOTE: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/pyzfs/common/util.py#114 533 """ 534 value = from_size(value) 535 if value is None: 536 value = "none" 537 538 if isinstance(value, Number) and value > 1024 and convert_to_human: 539 v_power = int(math.floor(math.log(value, 1024))) 540 v_multiplier = math.pow(1024, v_power) 541 542 # NOTE: zfs is a bit odd on how it does the rounding, 543 # see libzfs implementation linked above 544 v_size_float = float(value) / v_multiplier 545 if v_size_float == int(v_size_float): 546 value = "{:.0f}{}".format( 547 v_size_float, 548 zfs_size[v_power - 1], 549 ) 550 else: 551 for v_precision in ["{:.2f}{}", "{:.1f}{}", "{:.0f}{}"]: 552 v_size = v_precision.format( 553 v_size_float, 554 zfs_size[v_power - 1], 555 ) 556 if len(v_size) <= 5: 557 value = v_size 558 break 559 560 return value 561 562 563def from_str(value): 564 """ 565 Decode zfs safe string (used for name, path, ...) 566 """ 567 if value == "none": 568 value = None 569 if value: 570 value = str(value) 571 if value.startswith('"') and value.endswith('"'): 572 value = value[1:-1] 573 value = value.replace('\\"', '"') 574 575 return value 576 577 578def to_str(value): 579 """ 580 Encode zfs safe string (used for name, path, ...) 581 """ 582 value = from_str(value) 583 584 if value: 585 value = value.replace('"', '\\"') 586 if " " in value: 587 value = '"' + value + '"' 588 elif value is None: 589 value = "none" 590 591 return value 592 593 594def from_auto(name, value, source="auto"): 595 """ 596 Convert zfs value to python value 597 """ 598 return _auto("from", name, value, source) 599 600 601def to_auto(name, value, source="auto", convert_to_human=True): 602 """ 603 Convert python value to zfs value 604 """ 605 return _auto("to", name, value, source, convert_to_human) 606 607 608def from_auto_dict(values, source="auto"): 609 """ 610 Pass an entire dictionary to from_auto 611 612 .. note:: 613 The key will be passed as the name 614 615 """ 616 for name, value in values.items(): 617 values[name] = from_auto(name, value, source) 618 619 return values 620 621 622def to_auto_dict(values, source="auto", convert_to_human=True): 623 """ 624 Pass an entire dictionary to to_auto 625 626 .. note:: 627 The key will be passed as the name 628 """ 629 for name, value in values.items(): 630 values[name] = to_auto(name, value, source, convert_to_human) 631 632 return values 633 634 635def is_snapshot(name): 636 """ 637 Check if name is a valid snapshot name 638 """ 639 return from_str(name).count("@") == 1 640 641 642def is_bookmark(name): 643 """ 644 Check if name is a valid bookmark name 645 """ 646 return from_str(name).count("#") == 1 647 648 649def is_dataset(name): 650 """ 651 Check if name is a valid filesystem or volume name 652 """ 653 return not is_snapshot(name) and not is_bookmark(name) 654 655 656def zfs_command( 657 command, 658 flags=None, 659 opts=None, 660 property_name=None, 661 property_value=None, 662 filesystem_properties=None, 663 target=None, 664): 665 """ 666 Build and properly escape a zfs command 667 668 .. note:: 669 670 Input is not considered safe and will be passed through 671 to_auto(from_auto('input_here')), you do not need to do so 672 your self first. 673 674 """ 675 return _command( 676 "zfs", 677 command=command, 678 flags=flags, 679 opts=opts, 680 property_name=property_name, 681 property_value=property_value, 682 filesystem_properties=filesystem_properties, 683 pool_properties=None, 684 target=target, 685 ) 686 687 688def zpool_command( 689 command, 690 flags=None, 691 opts=None, 692 property_name=None, 693 property_value=None, 694 filesystem_properties=None, 695 pool_properties=None, 696 target=None, 697): 698 """ 699 Build and properly escape a zpool command 700 701 .. note:: 702 703 Input is not considered safe and will be passed through 704 to_auto(from_auto('input_here')), you do not need to do so 705 your self first. 706 707 """ 708 return _command( 709 "zpool", 710 command=command, 711 flags=flags, 712 opts=opts, 713 property_name=property_name, 714 property_value=property_value, 715 filesystem_properties=filesystem_properties, 716 pool_properties=pool_properties, 717 target=target, 718 ) 719 720 721def parse_command_result(res, label=None): 722 """ 723 Parse the result of a zpool/zfs command 724 725 .. note:: 726 727 Output on failure is rather predictable. 728 - retcode > 0 729 - each 'error' is a line on stderr 730 - optional 'Usage:' block under those with hits 731 732 We simple check those and return a OrderedDict were 733 we set label = True|False and error = error_messages 734 735 """ 736 ret = OrderedDict() 737 738 if label: 739 ret[label] = res["retcode"] == 0 740 741 if res["retcode"] != 0: 742 ret["error"] = [] 743 for error in res["stderr"].splitlines(): 744 if error.lower().startswith("usage:"): 745 break 746 if error.lower().startswith("use '-f'"): 747 error = error.replace("-f", "force=True") 748 if error.lower().startswith("use '-r'"): 749 error = error.replace("-r", "recursive=True") 750 ret["error"].append(error) 751 752 if ret["error"]: 753 ret["error"] = "\n".join(ret["error"]) 754 else: 755 del ret["error"] 756 757 return ret 758 759 760# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 761