1""" 2Manage VMware distributed virtual switches (DVSs) and their distributed virtual 3portgroups (DVportgroups). 4 5:codeauthor: `Alexandru Bleotu <alexandru.bleotu@morganstaley.com>` 6 7Examples 8======== 9 10Several settings can be changed for DVSs and DVporgroups. Here are two examples 11covering all of the settings. Fewer settings can be used 12 13DVS 14--- 15 16.. code-block:: python 17 18 'name': 'dvs1', 19 'max_mtu': 1000, 20 'uplink_names': [ 21 'dvUplink1', 22 'dvUplink2', 23 'dvUplink3' 24 ], 25 'capability': { 26 'portgroup_operation_supported': false, 27 'operation_supported': true, 28 'port_operation_supported': false 29 }, 30 'lacp_api_version': 'multipleLag', 31 'contact_email': 'foo@email.com', 32 'product_info': { 33 'version': 34 '6.0.0', 35 'vendor': 36 'VMware, 37 Inc.', 38 'name': 39 'DVS' 40 }, 41 'network_resource_management_enabled': true, 42 'contact_name': 'me@email.com', 43 'infrastructure_traffic_resource_pools': [ 44 { 45 'reservation': 0, 46 'limit': 1000, 47 'share_level': 'high', 48 'key': 'management', 49 'num_shares': 100 50 }, 51 { 52 'reservation': 0, 53 'limit': -1, 54 'share_level': 'normal', 55 'key': 'faultTolerance', 56 'num_shares': 50 57 }, 58 { 59 'reservation': 0, 60 'limit': 32000, 61 'share_level': 'normal', 62 'key': 'vmotion', 63 'num_shares': 50 64 }, 65 { 66 'reservation': 10000, 67 'limit': -1, 68 'share_level': 'normal', 69 'key': 'virtualMachine', 70 'num_shares': 50 71 }, 72 { 73 'reservation': 0, 74 'limit': -1, 75 'share_level': 'custom', 76 'key': 'iSCSI', 77 'num_shares': 75 78 }, 79 { 80 'reservation': 0, 81 'limit': -1, 82 'share_level': 'normal', 83 'key': 'nfs', 84 'num_shares': 50 85 }, 86 { 87 'reservation': 0, 88 'limit': -1, 89 'share_level': 'normal', 90 'key': 'hbr', 91 'num_shares': 50 92 }, 93 { 94 'reservation': 8750, 95 'limit': 15000, 96 'share_level': 'high', 97 'key': 'vsan', 98 'num_shares': 100 99 }, 100 { 101 'reservation': 0, 102 'limit': -1, 103 'share_level': 'normal', 104 'key': 'vdp', 105 'num_shares': 50 106 } 107 ], 108 'link_discovery_protocol': { 109 'operation': 110 'listen', 111 'protocol': 112 'cdp' 113 }, 114 'network_resource_control_version': 'version3', 115 'description': 'Managed by Salt. Random settings.' 116 117Note: The mandatory attribute is: ``name``. 118 119Portgroup 120--------- 121 122.. code-block:: python 123 124 'security_policy': { 125 'allow_promiscuous': true, 126 'mac_changes': false, 127 'forged_transmits': true 128 }, 129 'name': 'vmotion-v702', 130 'out_shaping': { 131 'enabled': true, 132 'average_bandwidth': 1500, 133 'burst_size': 4096, 134 'peak_bandwidth': 1500 135 }, 136 'num_ports': 128, 137 'teaming': { 138 'port_order': { 139 'active': [ 140 'dvUplink2' 141 ], 142 'standby': [ 143 'dvUplink1' 144 ] 145 }, 146 'notify_switches': false, 147 'reverse_policy': true, 148 'rolling_order': false, 149 'policy': 'failover_explicit', 150 'failure_criteria': { 151 'check_error_percent': true, 152 'full_duplex': false, 153 'check_duplex': false, 154 'percentage': 50, 155 'check_speed': 'minimum', 156 'speed': 20, 157 'check_beacon': true 158 } 159 }, 160 'type': 'earlyBinding', 161 'vlan_id': 100, 162 'description': 'Managed by Salt. Random settings.' 163 164Note: The mandatory attributes are: ``name``, ``type``. 165 166Dependencies 167============ 168 169- pyVmomi Python Module 170 171 172pyVmomi 173------- 174 175PyVmomi can be installed via pip: 176 177.. code-block:: bash 178 179 pip install pyVmomi 180 181.. note:: 182 183 Version 6.0 of pyVmomi has some problems with SSL error handling on certain 184 versions of Python. If using version 6.0 of pyVmomi, Python 2.7.9, 185 or newer must be present. This is due to an upstream dependency 186 in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the 187 version of Python is not in the supported range, you will need to install an 188 earlier version of pyVmomi. See `Issue #29537`_ for more information. 189 190.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 191 192Based on the note above, to install an earlier version of pyVmomi than the 193version currently listed in PyPi, run the following: 194 195.. code-block:: bash 196 197 pip install pyVmomi==5.5.0.2014.1.1 198 199The 5.5.0.2014.1.1 is a known stable version that this original ESXi State 200Module was developed against. 201""" 202 203 204import logging 205import sys 206 207import salt.exceptions 208 209try: 210 from pyVmomi import VmomiSupport 211 212 HAS_PYVMOMI = True 213except ImportError: 214 HAS_PYVMOMI = False 215 216# Get Logging Started 217log = logging.getLogger(__name__) 218 219 220def __virtual__(): 221 if not HAS_PYVMOMI: 222 return False, "State module did not load: pyVmomi not found" 223 224 # We check the supported vim versions to infer the pyVmomi version 225 if ( 226 "vim25/6.0" in VmomiSupport.versionMap 227 and sys.version_info > (2, 7) 228 and sys.version_info < (2, 7, 9) 229 ): 230 231 return ( 232 False, 233 "State module did not load: Incompatible versions " 234 "of Python and pyVmomi present. See Issue #29537.", 235 ) 236 return "dvs" 237 238 239def mod_init(low): 240 """ 241 Init function 242 """ 243 return True 244 245 246def _get_datacenter_name(): 247 """ 248 Returns the datacenter name configured on the proxy 249 250 Supported proxies: esxcluster, esxdatacenter 251 """ 252 253 proxy_type = __salt__["vsphere.get_proxy_type"]() 254 details = None 255 if proxy_type == "esxcluster": 256 details = __salt__["esxcluster.get_details"]() 257 elif proxy_type == "esxdatacenter": 258 details = __salt__["esxdatacenter.get_details"]() 259 if not details: 260 raise salt.exceptions.CommandExecutionError( 261 "details for proxy type '{}' not loaded".format(proxy_type) 262 ) 263 return details["datacenter"] 264 265 266def dvs_configured(name, dvs): 267 """ 268 Configures a DVS. 269 270 Creates a new DVS, if it doesn't exist in the provided datacenter or 271 reconfigures it if configured differently. 272 273 dvs 274 DVS dict representations (see module sysdocs) 275 """ 276 datacenter_name = _get_datacenter_name() 277 dvs_name = dvs["name"] if dvs.get("name") else name 278 log.info( 279 "Running state %s for DVS '%s' in datacenter '%s'", 280 name, 281 dvs_name, 282 datacenter_name, 283 ) 284 changes_required = False 285 ret = {"name": name, "changes": {}, "result": None, "comment": None} 286 comments = [] 287 changes = {} 288 changes_required = False 289 290 try: 291 # TODO dvs validation 292 si = __salt__["vsphere.get_service_instance_via_proxy"]() 293 dvss = __salt__["vsphere.list_dvss"](dvs_names=[dvs_name], service_instance=si) 294 if not dvss: 295 changes_required = True 296 if __opts__["test"]: 297 comments.append( 298 "State {} will create a new DVS '{}' in datacenter '{}'".format( 299 name, dvs_name, datacenter_name 300 ) 301 ) 302 log.info(comments[-1]) 303 else: 304 dvs["name"] = dvs_name 305 __salt__["vsphere.create_dvs"]( 306 dvs_dict=dvs, dvs_name=dvs_name, service_instance=si 307 ) 308 comments.append( 309 "Created a new DVS '{}' in datacenter '{}'".format( 310 dvs_name, datacenter_name 311 ) 312 ) 313 log.info(comments[-1]) 314 changes.update({"dvs": {"new": dvs}}) 315 else: 316 # DVS already exists. Checking various aspects of the config 317 props = [ 318 "description", 319 "contact_email", 320 "contact_name", 321 "lacp_api_version", 322 "link_discovery_protocol", 323 "max_mtu", 324 "network_resource_control_version", 325 "network_resource_management_enabled", 326 ] 327 log.trace( 328 "DVS '%s' found in datacenter '%s'. Checking for any updates in %s", 329 dvs_name, 330 datacenter_name, 331 props, 332 ) 333 props_to_original_values = {} 334 props_to_updated_values = {} 335 current_dvs = dvss[0] 336 for prop in props: 337 if prop in dvs and dvs[prop] != current_dvs.get(prop): 338 props_to_original_values[prop] = current_dvs.get(prop) 339 props_to_updated_values[prop] = dvs[prop] 340 341 # Simple infrastructure traffic resource control compare doesn't 342 # work because num_shares is optional if share_level is not custom 343 # We need to do a dedicated compare for this property 344 infra_prop = "infrastructure_traffic_resource_pools" 345 original_infra_res_pools = [] 346 updated_infra_res_pools = [] 347 if infra_prop in dvs: 348 if not current_dvs.get(infra_prop): 349 updated_infra_res_pools = dvs[infra_prop] 350 else: 351 for idx in range(len(dvs[infra_prop])): 352 if ( 353 "num_shares" not in dvs[infra_prop][idx] 354 and current_dvs[infra_prop][idx]["share_level"] != "custom" 355 and "num_shares" in current_dvs[infra_prop][idx] 356 ): 357 358 del current_dvs[infra_prop][idx]["num_shares"] 359 if dvs[infra_prop][idx] != current_dvs[infra_prop][idx]: 360 361 original_infra_res_pools.append( 362 current_dvs[infra_prop][idx] 363 ) 364 updated_infra_res_pools.append(dict(dvs[infra_prop][idx])) 365 if updated_infra_res_pools: 366 props_to_original_values[ 367 "infrastructure_traffic_resource_pools" 368 ] = original_infra_res_pools 369 props_to_updated_values[ 370 "infrastructure_traffic_resource_pools" 371 ] = updated_infra_res_pools 372 if props_to_updated_values: 373 if __opts__["test"]: 374 changes_string = "" 375 for p in props_to_updated_values: 376 if p == "infrastructure_traffic_resource_pools": 377 changes_string += ( 378 "\tinfrastructure_traffic_resource_pools:\n" 379 ) 380 for idx in range(len(props_to_updated_values[p])): 381 d = props_to_updated_values[p][idx] 382 s = props_to_original_values[p][idx] 383 changes_string += "\t\t{} from '{}' to '{}'\n".format( 384 d["key"], s, d 385 ) 386 else: 387 changes_string += "\t{} from '{}' to '{}'\n".format( 388 p, 389 props_to_original_values[p], 390 props_to_updated_values[p], 391 ) 392 comments.append( 393 "State dvs_configured will update DVS '{}' " 394 "in datacenter '{}':\n{}" 395 "".format(dvs_name, datacenter_name, changes_string) 396 ) 397 log.info(comments[-1]) 398 else: 399 __salt__["vsphere.update_dvs"]( 400 dvs_dict=props_to_updated_values, 401 dvs=dvs_name, 402 service_instance=si, 403 ) 404 comments.append( 405 "Updated DVS '{}' in datacenter '{}'".format( 406 dvs_name, datacenter_name 407 ) 408 ) 409 log.info(comments[-1]) 410 changes.update( 411 { 412 "dvs": { 413 "new": props_to_updated_values, 414 "old": props_to_original_values, 415 } 416 } 417 ) 418 __salt__["vsphere.disconnect"](si) 419 except salt.exceptions.CommandExecutionError as exc: 420 log.error("Error: %s", exc, exc_info=True) 421 if si: 422 __salt__["vsphere.disconnect"](si) 423 if not __opts__["test"]: 424 ret["result"] = False 425 ret.update( 426 {"comment": str(exc), "result": False if not __opts__["test"] else None} 427 ) 428 return ret 429 if not comments: 430 # We have no changes 431 ret.update( 432 { 433 "comment": ( 434 "DVS '{}' in datacenter '{}' is " 435 "correctly configured. Nothing to be done." 436 "".format(dvs_name, datacenter_name) 437 ), 438 "result": True, 439 } 440 ) 441 else: 442 ret.update( 443 { 444 "comment": "\n".join(comments), 445 "changes": changes, 446 "result": None if __opts__["test"] else True, 447 } 448 ) 449 return ret 450 451 452def _get_diff_dict(dict1, dict2): 453 """ 454 Returns a dictionary with the diffs between two dictionaries 455 456 It will ignore any key that doesn't exist in dict2 457 """ 458 ret_dict = {} 459 for p in dict2.keys(): 460 if p not in dict1: 461 ret_dict.update({p: {"val1": None, "val2": dict2[p]}}) 462 elif dict1[p] != dict2[p]: 463 if isinstance(dict1[p], dict) and isinstance(dict2[p], dict): 464 sub_diff_dict = _get_diff_dict(dict1[p], dict2[p]) 465 if sub_diff_dict: 466 ret_dict.update({p: sub_diff_dict}) 467 else: 468 ret_dict.update({p: {"val1": dict1[p], "val2": dict2[p]}}) 469 return ret_dict 470 471 472def _get_val2_dict_from_diff_dict(diff_dict): 473 """ 474 Returns a dictionaries with the values stored in val2 of a diff dict. 475 """ 476 ret_dict = {} 477 for p in diff_dict.keys(): 478 if not isinstance(diff_dict[p], dict): 479 raise ValueError("Unexpected diff difct '{}'".format(diff_dict)) 480 if "val2" in diff_dict[p].keys(): 481 ret_dict.update({p: diff_dict[p]["val2"]}) 482 else: 483 ret_dict.update({p: _get_val2_dict_from_diff_dict(diff_dict[p])}) 484 return ret_dict 485 486 487def _get_val1_dict_from_diff_dict(diff_dict): 488 """ 489 Returns a dictionaries with the values stored in val1 of a diff dict. 490 """ 491 ret_dict = {} 492 for p in diff_dict.keys(): 493 if not isinstance(diff_dict[p], dict): 494 raise ValueError("Unexpected diff difct '{}'".format(diff_dict)) 495 if "val1" in diff_dict[p].keys(): 496 ret_dict.update({p: diff_dict[p]["val1"]}) 497 else: 498 ret_dict.update({p: _get_val1_dict_from_diff_dict(diff_dict[p])}) 499 return ret_dict 500 501 502def _get_changes_from_diff_dict(diff_dict): 503 """ 504 Returns a list of string message of the differences in a diff dict. 505 506 Each inner message is tabulated one tab deeper 507 """ 508 changes_strings = [] 509 for p in diff_dict.keys(): 510 if not isinstance(diff_dict[p], dict): 511 raise ValueError("Unexpected diff difct '{}'".format(diff_dict)) 512 if sorted(diff_dict[p].keys()) == ["val1", "val2"]: 513 # Some string formatting 514 from_str = diff_dict[p]["val1"] 515 if isinstance(diff_dict[p]["val1"], str): 516 from_str = "'{}'".format(diff_dict[p]["val1"]) 517 elif isinstance(diff_dict[p]["val1"], list): 518 from_str = "'{}'".format(", ".join(diff_dict[p]["val1"])) 519 to_str = diff_dict[p]["val2"] 520 if isinstance(diff_dict[p]["val2"], str): 521 to_str = "'{}'".format(diff_dict[p]["val2"]) 522 elif isinstance(diff_dict[p]["val2"], list): 523 to_str = "'{}'".format(", ".join(diff_dict[p]["val2"])) 524 changes_strings.append("{} from {} to {}".format(p, from_str, to_str)) 525 else: 526 sub_changes = _get_changes_from_diff_dict(diff_dict[p]) 527 if sub_changes: 528 changes_strings.append("{}:".format(p)) 529 changes_strings.extend(["\t{}".format(c) for c in sub_changes]) 530 return changes_strings 531 532 533def portgroups_configured(name, dvs, portgroups): 534 """ 535 Configures portgroups on a DVS. 536 537 Creates/updates/removes portgroups in a provided DVS 538 539 dvs 540 Name of the DVS 541 542 portgroups 543 Portgroup dict representations (see module sysdocs) 544 """ 545 datacenter = _get_datacenter_name() 546 log.info("Running state %s on DVS '%s', datacenter '%s'", name, dvs, datacenter) 547 changes_required = False 548 ret = {"name": name, "changes": {}, "result": None, "comment": None} 549 comments = [] 550 changes = {} 551 changes_required = False 552 553 try: 554 # TODO portroups validation 555 si = __salt__["vsphere.get_service_instance_via_proxy"]() 556 current_pgs = __salt__["vsphere.list_dvportgroups"]( 557 dvs=dvs, service_instance=si 558 ) 559 expected_pg_names = [] 560 for pg in portgroups: 561 pg_name = pg["name"] 562 expected_pg_names.append(pg_name) 563 del pg["name"] 564 log.info("Checking pg '%s'", pg_name) 565 filtered_current_pgs = [p for p in current_pgs if p.get("name") == pg_name] 566 if not filtered_current_pgs: 567 changes_required = True 568 if __opts__["test"]: 569 comments.append( 570 "State {} will create a new portgroup " 571 "'{}' in DVS '{}', datacenter " 572 "'{}'".format(name, pg_name, dvs, datacenter) 573 ) 574 else: 575 __salt__["vsphere.create_dvportgroup"]( 576 portgroup_dict=pg, 577 portgroup_name=pg_name, 578 dvs=dvs, 579 service_instance=si, 580 ) 581 comments.append( 582 "Created a new portgroup '{}' in DVS " 583 "'{}', datacenter '{}'" 584 "".format(pg_name, dvs, datacenter) 585 ) 586 log.info(comments[-1]) 587 changes.update({pg_name: {"new": pg}}) 588 else: 589 # Porgroup already exists. Checking the config 590 log.trace( 591 "Portgroup '%s' found in DVS '%s', datacenter '%s'. Checking for any updates.", 592 pg_name, 593 dvs, 594 datacenter, 595 ) 596 current_pg = filtered_current_pgs[0] 597 diff_dict = _get_diff_dict(current_pg, pg) 598 599 if diff_dict: 600 changes_required = True 601 if __opts__["test"]: 602 changes_strings = _get_changes_from_diff_dict(diff_dict) 603 log.trace("changes_strings = %s", changes_strings) 604 comments.append( 605 "State {} will update portgroup '{}' in " 606 "DVS '{}', datacenter '{}':\n{}" 607 "".format( 608 name, 609 pg_name, 610 dvs, 611 datacenter, 612 "\n".join(["\t{}".format(c) for c in changes_strings]), 613 ) 614 ) 615 else: 616 __salt__["vsphere.update_dvportgroup"]( 617 portgroup_dict=pg, 618 portgroup=pg_name, 619 dvs=dvs, 620 service_instance=si, 621 ) 622 comments.append( 623 "Updated portgroup '{}' in DVS " 624 "'{}', datacenter '{}'" 625 "".format(pg_name, dvs, datacenter) 626 ) 627 log.info(comments[-1]) 628 changes.update( 629 { 630 pg_name: { 631 "new": _get_val2_dict_from_diff_dict(diff_dict), 632 "old": _get_val1_dict_from_diff_dict(diff_dict), 633 } 634 } 635 ) 636 # Add the uplink portgroup to the expected pg names 637 uplink_pg = __salt__["vsphere.list_uplink_dvportgroup"]( 638 dvs=dvs, service_instance=si 639 ) 640 expected_pg_names.append(uplink_pg["name"]) 641 # Remove any extra portgroups 642 for current_pg in current_pgs: 643 if current_pg["name"] not in expected_pg_names: 644 changes_required = True 645 if __opts__["test"]: 646 comments.append( 647 "State {} will remove " 648 "the portgroup '{}' from DVS '{}', " 649 "datacenter '{}'" 650 "".format(name, current_pg["name"], dvs, datacenter) 651 ) 652 else: 653 __salt__["vsphere.remove_dvportgroup"]( 654 portgroup=current_pg["name"], dvs=dvs, service_instance=si 655 ) 656 comments.append( 657 "Removed the portgroup '{}' from DVS " 658 "'{}', datacenter '{}'" 659 "".format(current_pg["name"], dvs, datacenter) 660 ) 661 log.info(comments[-1]) 662 changes.update({current_pg["name"]: {"old": current_pg}}) 663 __salt__["vsphere.disconnect"](si) 664 except salt.exceptions.CommandExecutionError as exc: 665 log.error("Error: %s", exc, exc_info=True) 666 if si: 667 __salt__["vsphere.disconnect"](si) 668 if not __opts__["test"]: 669 ret["result"] = False 670 ret.update( 671 {"comment": exc.strerror, "result": False if not __opts__["test"] else None} 672 ) 673 return ret 674 if not changes_required: 675 # We have no changes 676 ret.update( 677 { 678 "comment": ( 679 "All portgroups in DVS '{}', datacenter " 680 "'{}' exist and are correctly configured. " 681 "Nothing to be done.".format(dvs, datacenter) 682 ), 683 "result": True, 684 } 685 ) 686 else: 687 ret.update( 688 { 689 "comment": "\n".join(comments), 690 "changes": changes, 691 "result": None if __opts__["test"] else True, 692 } 693 ) 694 return ret 695 696 697def uplink_portgroup_configured(name, dvs, uplink_portgroup): 698 """ 699 Configures the uplink portgroup on a DVS. The state assumes there is only 700 one uplink portgroup. 701 702 dvs 703 Name of the DVS 704 705 upling_portgroup 706 Uplink portgroup dict representations (see module sysdocs) 707 708 """ 709 datacenter = _get_datacenter_name() 710 log.info("Running %s on DVS '%s', datacenter '%s'", name, dvs, datacenter) 711 changes_required = False 712 ret = {"name": name, "changes": {}, "result": None, "comment": None} 713 comments = [] 714 changes = {} 715 changes_required = False 716 717 try: 718 # TODO portroups validation 719 si = __salt__["vsphere.get_service_instance_via_proxy"]() 720 current_uplink_portgroup = __salt__["vsphere.list_uplink_dvportgroup"]( 721 dvs=dvs, service_instance=si 722 ) 723 log.trace("current_uplink_portgroup = %s", current_uplink_portgroup) 724 diff_dict = _get_diff_dict(current_uplink_portgroup, uplink_portgroup) 725 if diff_dict: 726 changes_required = True 727 if __opts__["test"]: 728 changes_strings = _get_changes_from_diff_dict(diff_dict) 729 log.trace("changes_strings = %s", changes_strings) 730 comments.append( 731 "State {} will update the " 732 "uplink portgroup in DVS '{}', datacenter " 733 "'{}':\n{}" 734 "".format( 735 name, 736 dvs, 737 datacenter, 738 "\n".join(["\t{}".format(c) for c in changes_strings]), 739 ) 740 ) 741 else: 742 __salt__["vsphere.update_dvportgroup"]( 743 portgroup_dict=uplink_portgroup, 744 portgroup=current_uplink_portgroup["name"], 745 dvs=dvs, 746 service_instance=si, 747 ) 748 comments.append( 749 "Updated the uplink portgroup in DVS '{}', datacenter '{}'".format( 750 dvs, datacenter 751 ) 752 ) 753 log.info(comments[-1]) 754 changes.update( 755 { 756 "uplink_portgroup": { 757 "new": _get_val2_dict_from_diff_dict(diff_dict), 758 "old": _get_val1_dict_from_diff_dict(diff_dict), 759 } 760 } 761 ) 762 __salt__["vsphere.disconnect"](si) 763 except salt.exceptions.CommandExecutionError as exc: 764 log.error("Error: %s", exc, exc_info=True) 765 if si: 766 __salt__["vsphere.disconnect"](si) 767 if not __opts__["test"]: 768 ret["result"] = False 769 ret.update( 770 {"comment": exc.strerror, "result": False if not __opts__["test"] else None} 771 ) 772 return ret 773 if not changes_required: 774 # We have no changes 775 ret.update( 776 { 777 "comment": ( 778 "Uplink portgroup in DVS '{}', datacenter " 779 "'{}' is correctly configured. " 780 "Nothing to be done.".format(dvs, datacenter) 781 ), 782 "result": True, 783 } 784 ) 785 else: 786 ret.update( 787 { 788 "comment": "\n".join(comments), 789 "changes": changes, 790 "result": None if __opts__["test"] else True, 791 } 792 ) 793 return ret 794