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