diff --git a/salt/config/schemas/vcenter.py b/salt/config/schemas/vcenter.py index 4867923f27ac..1d76fb43a511 100644 --- a/salt/config/schemas/vcenter.py +++ b/salt/config/schemas/vcenter.py @@ -14,6 +14,8 @@ # Import Salt libs from salt.utils.schema import (Schema, + ArrayItem, + IntegerItem, StringItem) @@ -31,3 +33,25 @@ class VCenterEntitySchema(Schema): vcenter = StringItem(title='vCenter', description='Specifies the vcenter hostname', required=True) + + +class VCenterProxySchema(Schema): + ''' + Schema for the configuration for the proxy to connect to a VCenter. + ''' + title = 'VCenter Proxy Connection Schema' + description = 'Schema that describes the connection to a VCenter' + additional_properties = False + proxytype = StringItem(required=True, + enum=['vcenter']) + vcenter = StringItem(required=True, pattern=r'[^\s]+') + mechanism = StringItem(required=True, enum=['userpass', 'sspi']) + username = StringItem() + passwords = ArrayItem(min_items=1, + items=StringItem(), + unique_items=True) + + domain = StringItem() + principal = StringItem(default='host') + protocol = StringItem(default='https') + port = IntegerItem(minimum=1) diff --git a/salt/modules/vcenter.py b/salt/modules/vcenter.py new file mode 100644 index 000000000000..bac3c674b49a --- /dev/null +++ b/salt/modules/vcenter.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +''' +Module used to access the vcenter proxy connection methods +''' +from __future__ import absolute_import + +# Import python libs +import logging +import salt.utils + + +log = logging.getLogger(__name__) + +__proxyenabled__ = ['vcenter'] +# Define the module's virtual name +__virtualname__ = 'vcenter' + + +def __virtual__(): + ''' + Only work on proxy + ''' + if salt.utils.is_proxy(): + return __virtualname__ + return False + + +def get_details(): + return __proxy__['vcenter.get_details']() diff --git a/salt/modules/vsphere.py b/salt/modules/vsphere.py index bde7c9c98e15..0c923858042d 100644 --- a/salt/modules/vsphere.py +++ b/salt/modules/vsphere.py @@ -177,6 +177,7 @@ import salt.utils.path import salt.utils.vmware import salt.utils.vsan +import salt.utils.pbm from salt.exceptions import CommandExecutionError, VMwareSaltError, \ ArgumentValueError, InvalidConfigError, VMwareObjectRetrievalError, \ VMwareApiError, InvalidEntityError @@ -193,7 +194,7 @@ HAS_JSONSCHEMA = False try: - from pyVmomi import vim, vmodl, VmomiSupport + from pyVmomi import vim, vmodl, pbm, VmomiSupport HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False @@ -207,7 +208,7 @@ log = logging.getLogger(__name__) __virtualname__ = 'vsphere' -__proxyenabled__ = ['esxi', 'esxcluster', 'esxdatacenter'] +__proxyenabled__ = ['esxi', 'esxcluster', 'esxdatacenter', 'vcenter'] def __virtual__(): @@ -254,6 +255,8 @@ def _get_proxy_connection_details(): details = __salt__['esxcluster.get_details']() elif proxytype == 'esxdatacenter': details = __salt__['esxdatacenter.get_details']() + elif proxytype == 'vcenter': + details = __salt__['vcenter.get_details']() else: raise CommandExecutionError('\'{0}\' proxy is not supported' ''.format(proxytype)) @@ -379,7 +382,7 @@ def _gets_service_instance_via_proxy(*args, **kwargs): @depends(HAS_PYVMOMI) -@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter', 'vcenter') def get_service_instance_via_proxy(service_instance=None): ''' Returns a service instance to the proxied endpoint (vCenter/ESXi host). @@ -399,7 +402,7 @@ def get_service_instance_via_proxy(service_instance=None): @depends(HAS_PYVMOMI) -@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter', 'vcenter') def disconnect(service_instance): ''' Disconnects from a vCenter or ESXi host @@ -1934,7 +1937,7 @@ def get_vsan_eligible_disks(host, username, password, protocol=None, port=None, @depends(HAS_PYVMOMI) -@supports_proxies('esxi', 'esxcluster', 'esxdatacenter') +@supports_proxies('esxi', 'esxcluster', 'esxdatacenter', 'vcenter') @gets_service_instance_via_proxy def test_vcenter_connection(service_instance=None): ''' @@ -4608,8 +4611,344 @@ def remove_dvportgroup(portgroup, dvs, service_instance=None): return True +def _get_policy_dict(policy): + '''Returns a dictionary representation of a policy''' + profile_dict = {'name': policy.name, + 'description': policy.description, + 'resource_type': policy.resourceType.resourceType} + subprofile_dicts = [] + if isinstance(policy, pbm.profile.CapabilityBasedProfile) and \ + isinstance(policy.constraints, + pbm.profile.SubProfileCapabilityConstraints): + + for subprofile in policy.constraints.subProfiles: + subprofile_dict = {'name': subprofile.name, + 'force_provision': subprofile.forceProvision} + cap_dicts = [] + for cap in subprofile.capability: + cap_dict = {'namespace': cap.id.namespace, + 'id': cap.id.id} + # We assume there is one constraint with one value set + val = cap.constraint[0].propertyInstance[0].value + if isinstance(val, pbm.capability.types.Range): + val_dict = {'type': 'range', + 'min': val.min, + 'max': val.max} + elif isinstance(val, pbm.capability.types.DiscreteSet): + val_dict = {'type': 'set', + 'values': val.values} + else: + val_dict = {'type': 'scalar', + 'value': val} + cap_dict['setting'] = val_dict + cap_dicts.append(cap_dict) + subprofile_dict['capabilities'] = cap_dicts + subprofile_dicts.append(subprofile_dict) + profile_dict['subprofiles'] = subprofile_dicts + return profile_dict + + @depends(HAS_PYVMOMI) -@supports_proxies('esxdatacenter', 'esxcluster') +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def list_storage_policies(policy_names=None, service_instance=None): + ''' + Returns a list of storage policies. + + policy_names + Names of policies to list. If None, all policies are listed. + Default is None. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.list_storage_policies + + salt '*' vsphere.list_storage_policy policy_names=[policy_name] + ''' + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + if not policy_names: + policies = salt.utils.pbm.get_storage_policies(profile_manager, + get_all_policies=True) + else: + policies = salt.utils.pbm.get_storage_policies(profile_manager, + policy_names) + return [_get_policy_dict(p) for p in policies] + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def list_default_vsan_policy(service_instance=None): + ''' + Returns the default vsan storage policy. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.list_storage_policies + + salt '*' vsphere.list_storage_policy policy_names=[policy_name] + ''' + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + policies = salt.utils.pbm.get_storage_policies(profile_manager, + get_all_policies=True) + def_policies = [p for p in policies + if p.systemCreatedProfileType == 'VsanDefaultProfile'] + if not def_policies: + raise VMwareObjectRetrievalError('Default VSAN policy was not ' + 'retrieved') + return _get_policy_dict(def_policies[0]) + + +def _get_capability_definition_dict(cap_metadata): + # We assume each capability definition has one property with the same id + # as the capability so we display its type as belonging to the capability + # The object model permits multiple properties + return {'namespace': cap_metadata.id.namespace, + 'id': cap_metadata.id.id, + 'mandatory': cap_metadata.mandatory, + 'description': cap_metadata.summary.summary, + 'type': cap_metadata.propertyMetadata[0].type.typeName} + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def list_capability_definitions(service_instance=None): + ''' + Returns a list of the metadata of all capabilities in the vCenter. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.list_capabilities + ''' + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + ret_list = [_get_capability_definition_dict(c) for c in + salt.utils.pbm.get_capability_definitions(profile_manager)] + return ret_list + + +def _apply_policy_config(policy_spec, policy_dict): + '''Applies a policy dictionary to a policy spec''' + log.trace('policy_dict = {0}'.format(policy_dict)) + if policy_dict.get('name'): + policy_spec.name = policy_dict['name'] + if policy_dict.get('description'): + policy_spec.description = policy_dict['description'] + if policy_dict.get('subprofiles'): + # Incremental changes to subprofiles and capabilities are not + # supported because they would complicate updates too much + # The whole configuration of all sub-profiles is expected and applied + policy_spec.constraints = pbm.profile.SubProfileCapabilityConstraints() + subprofiles = [] + for subprofile_dict in policy_dict['subprofiles']: + subprofile_spec = \ + pbm.profile.SubProfileCapabilityConstraints.SubProfile( + name=subprofile_dict['name']) + cap_specs = [] + if subprofile_dict.get('force_provision'): + subprofile_spec.forceProvision = \ + subprofile_dict['force_provision'] + for cap_dict in subprofile_dict['capabilities']: + prop_inst_spec = pbm.capability.PropertyInstance( + id=cap_dict['id'] + ) + setting_type = cap_dict['setting']['type'] + if setting_type == 'set': + prop_inst_spec.value = pbm.capability.types.DiscreteSet() + prop_inst_spec.value.values = cap_dict['setting']['values'] + elif setting_type == 'range': + prop_inst_spec.value = pbm.capability.types.Range() + prop_inst_spec.value.max = cap_dict['setting']['max'] + prop_inst_spec.value.min = cap_dict['setting']['min'] + elif setting_type == 'scalar': + prop_inst_spec.value = cap_dict['setting']['value'] + cap_spec = pbm.capability.CapabilityInstance( + id=pbm.capability.CapabilityMetadata.UniqueId( + id=cap_dict['id'], + namespace=cap_dict['namespace']), + constraint=[pbm.capability.ConstraintInstance( + propertyInstance=[prop_inst_spec])]) + cap_specs.append(cap_spec) + subprofile_spec.capability = cap_specs + subprofiles.append(subprofile_spec) + policy_spec.constraints.subProfiles = subprofiles + log.trace('updated policy_spec = {0}'.format(policy_spec)) + return policy_spec + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def create_storage_policy(policy_name, policy_dict, service_instance=None): + ''' + Creates a storage policy. + + Supported capability types: scalar, set, range. + + policy_name + Name of the policy to create. + The value of the argument will override any existing name in + ``policy_dict``. + + policy_dict + Dictionary containing the changes to apply to the policy. + (exmaple in salt.states.pbm) + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.create_storage_policy policy_name='policy name' + policy_dict="$policy_dict" + ''' + log.trace('create storage policy \'{0}\', dict = {1}' + ''.format(policy_name, policy_dict)) + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + policy_create_spec = pbm.profile.CapabilityBasedProfileCreateSpec() + # Hardcode the storage profile resource type + policy_create_spec.resourceType = pbm.profile.ResourceType( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + # Set name argument + policy_dict['name'] = policy_name + log.trace('Setting policy values in policy_update_spec') + _apply_policy_config(policy_create_spec, policy_dict) + salt.utils.pbm.create_storage_policy(profile_manager, policy_create_spec) + return {'create_storage_policy': True} + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def update_storage_policy(policy, policy_dict, service_instance=None): + ''' + Updates a storage policy. + + Supported capability types: scalar, set, range. + + policy + Name of the policy to update. + + policy_dict + Dictionary containing the changes to apply to the policy. + (exmaple in salt.states.pbm) + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.update_storage_policy policy='policy name' + policy_dict="$policy_dict" + ''' + log.trace('updating storage policy, dict = {0}'.format(policy_dict)) + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + policies = salt.utils.pbm.get_storage_policies(profile_manager, [policy]) + if not policies: + raise VMwareObjectRetrievalError('Policy \'{0}\' was not found' + ''.format(policy)) + policy_ref = policies[0] + policy_update_spec = pbm.profile.CapabilityBasedProfileUpdateSpec() + log.trace('Setting policy values in policy_update_spec') + for prop in ['description', 'constraints']: + setattr(policy_update_spec, prop, getattr(policy_ref, prop)) + _apply_policy_config(policy_update_spec, policy_dict) + salt.utils.pbm.update_storage_policy(profile_manager, policy_ref, + policy_update_spec) + return {'update_storage_policy': True} + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxcluster', 'esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def list_default_storage_policy_of_datastore(datastore, service_instance=None): + ''' + Returns a list of datastores assign the the storage policies. + + datastore + Name of the datastore to assign. + The datastore needs to be visible to the VMware entity the proxy + points to. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.list_default_storage_policy_of_datastore datastore=ds1 + ''' + log.trace('Listing the default storage policy of datastore \'{0}\'' + ''.format(datastore)) + # Find datastore + target_ref = _get_proxy_target(service_instance) + ds_refs = salt.utils.vmware.get_datastores(service_instance, target_ref, + datastore_names=[datastore]) + if not ds_refs: + raise VMwareObjectRetrievalError('Datastore \'{0}\' was not ' + 'found'.format(datastore)) + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + policy = salt.utils.pbm.get_default_storage_policy_of_datastore( + profile_manager, ds_refs[0]) + return _get_policy_dict(policy) + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxcluster', 'esxdatacenter', 'vcenter') +@gets_service_instance_via_proxy +def assign_default_storage_policy_to_datastore(policy, datastore, + service_instance=None): + ''' + Assigns a storage policy as the default policy to a datastore. + + policy + Name of the policy to assign. + + datastore + Name of the datastore to assign. + The datastore needs to be visible to the VMware entity the proxy + points to. + + service_instance + Service instance (vim.ServiceInstance) of the vCenter. + Default is None. + + .. code-block:: bash + salt '*' vsphere.assign_storage_policy_to_datastore + policy='policy name' datastore=ds1 + ''' + log.trace('Assigning policy {0} to datastore {1}' + ''.format(policy, datastore)) + profile_manager = salt.utils.pbm.get_profile_manager(service_instance) + # Find policy + policies = salt.utils.pbm.get_storage_policies(profile_manager, [policy]) + if not policies: + raise VMwareObjectRetrievalError('Policy \'{0}\' was not found' + ''.format(policy)) + policy_ref = policies[0] + # Find datastore + target_ref = _get_proxy_target(service_instance) + ds_refs = salt.utils.vmware.get_datastores(service_instance, target_ref, + datastore_names=[datastore]) + if not ds_refs: + raise VMwareObjectRetrievalError('Datastore \'{0}\' was not ' + 'found'.format(datastore)) + ds_ref = ds_refs[0] + salt.utils.pbm.assign_default_storage_policy_to_datastore( + profile_manager, policy_ref, ds_ref) + return True + + +@depends(HAS_PYVMOMI) +@supports_proxies('esxdatacenter', 'esxcluster', 'vcenter') @gets_service_instance_via_proxy def list_datacenters_via_proxy(datacenter_names=None, service_instance=None): ''' @@ -4647,7 +4986,7 @@ def list_datacenters_via_proxy(datacenter_names=None, service_instance=None): @depends(HAS_PYVMOMI) -@supports_proxies('esxdatacenter') +@supports_proxies('esxdatacenter', 'vcenter') @gets_service_instance_via_proxy def create_datacenter(datacenter_name, service_instance=None): ''' @@ -6102,7 +6441,7 @@ def add_host_to_dvs(host, username, password, vmknic_name, vmnic_name, @depends(HAS_PYVMOMI) -@supports_proxies('esxcluster', 'esxdatacenter') +@supports_proxies('esxcluster', 'esxdatacenter', 'vcenter') def _get_proxy_target(service_instance): ''' Returns the target object of a proxy. @@ -6130,6 +6469,9 @@ def _get_proxy_target(service_instance): reference = salt.utils.vmware.get_datacenter(service_instance, datacenter) + elif proxy_type == 'vcenter': + # vcenter proxy - the target is the root folder + reference = salt.utils.vmware.get_root_folder(service_instance) log.trace('reference = {0}'.format(reference)) return reference diff --git a/salt/proxy/vcenter.py b/salt/proxy/vcenter.py new file mode 100644 index 000000000000..5c5ad797d191 --- /dev/null +++ b/salt/proxy/vcenter.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +''' +Proxy Minion interface module for managing VMWare vCenters. + +:codeauthor: :email:`Rod McKenzie (roderick.mckenzie@morganstanley.com)` +:codeauthor: :email:`Alexandru Bleotu (alexandru.bleotu@morganstanley.com)` + +Dependencies +============ + +- pyVmomi Python Module + +pyVmomi +------- + +PyVmomi can be installed via pip: + +.. code-block:: bash + + pip install pyVmomi + +.. note:: + + Version 6.0 of pyVmomi has some problems with SSL error handling on certain + versions of Python. If using version 6.0 of pyVmomi, Python 2.6, + Python 2.7.9, or newer must be present. This is due to an upstream dependency + in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the + version of Python is not in the supported range, you will need to install an + earlier version of pyVmomi. See `Issue #29537`_ for more information. + +.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 + +Based on the note above, to install an earlier version of pyVmomi than the +version currently listed in PyPi, run the following: + +.. code-block:: bash + + pip install pyVmomi==5.5.0.2014.1.1 + +The 5.5.0.2014.1.1 is a known stable version that this original ESXi State +Module was developed against. + + +Configuration +============= +To use this proxy module, please use on of the following configurations: + + +.. code-block:: yaml + + proxy: + proxytype: vcenter + vcenter: + username: + mechanism: userpass + passwords: + - first_password + - second_password + - third_password + + proxy: + proxytype: vcenter + vcenter: + username: + domain: + mechanism: sspi + principal: + +proxytype +^^^^^^^^^ +The ``proxytype`` key and value pair is critical, as it tells Salt which +interface to load from the ``proxy`` directory in Salt's install hierarchy, +or from ``/srv/salt/_proxy`` on the Salt Master (if you have created your +own proxy module, for example). To use this Proxy Module, set this to +``vcenter``. + +vcenter +^^^^^^^ +The location of the VMware vCenter server (host of ip). Required + +username +^^^^^^^^ +The username used to login to the vcenter, such as ``root``. +Required only for userpass. + +mechanism +^^^^^^^^ +The mechanism used to connect to the vCenter server. Supported values are +``userpass`` and ``sspi``. Required. + +passwords +^^^^^^^^^ +A list of passwords to be used to try and login to the vCenter server. At least +one password in this list is required if mechanism is ``userpass`` + +The proxy integration will try the passwords listed in order. + +domain +^^^^^^ +User domain. Required if mechanism is ``sspi`` + +principal +^^^^^^^^ +Kerberos principal. Rquired if mechanism is ``sspi`` + +protocol +^^^^^^^^ +If the vCenter is not using the default protocol, set this value to an +alternate protocol. Default is ``https``. + +port +^^^^ +If the ESXi host is not using the default port, set this value to an +alternate port. Default is ``443``. + + +Salt Proxy +---------- + +After your pillar is in place, you can test the proxy. The proxy can run on +any machine that has network connectivity to your Salt Master and to the +vCenter server in the pillar. SaltStack recommends that the machine running the +salt-proxy process also run a regular minion, though it is not strictly +necessary. + +On the machine that will run the proxy, make sure there is an ``/etc/salt/proxy`` +file with at least the following in it: + +.. code-block:: yaml + + master: + +You can then start the salt-proxy process with: + +.. code-block:: bash + + salt-proxy --proxyid + +You may want to add ``-l debug`` to run the above in the foreground in +debug mode just to make sure everything is OK. + +Next, accept the key for the proxy on your salt-master, just like you +would for a regular minion: + +.. code-block:: bash + + salt-key -a + +You can confirm that the pillar data is in place for the proxy: + +.. code-block:: bash + + salt pillar.items + +And now you should be able to ping the ESXi host to make sure it is +responding: + +.. code-block:: bash + + salt test.ping + +At this point you can execute one-off commands against the vcenter. For +example, you can get if the proxy can actually connect to the vCenter: + +.. code-block:: bash + + salt vsphere.test_vcenter_connection + +Note that you don't need to provide credentials or an ip/hostname. Salt +knows to use the credentials you stored in Pillar. + +It's important to understand how this particular proxy works. +:mod:`Salt.modules.vsphere ` is a +standard Salt execution module. + + If you pull up the docs for it you'll see +that almost every function in the module takes credentials and a targets either +a vcenter or a host. When credentials and a host aren't passed, Salt runs commands +through ``pyVmomi`` against the local machine. If you wanted, you could run +functions from this module on any host where an appropriate version of +``pyVmomi`` is installed, and that host would reach out over the network +and communicate with the ESXi host. +''' + +# Import Python Libs +from __future__ import absolute_import +import logging +import os + +# Import Salt Libs +import salt.exceptions +from salt.config.schemas.vcenter import VCenterProxySchema +from salt.utils.dictupdate import merge + +# This must be present or the Salt loader won't load this module. +__proxyenabled__ = ['vcenter'] + +# External libraries +try: + import jsonschema + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False + +# Variables are scoped to this module so we can have persistent data +# across calls to fns in here. +DETAILS = {} + + +# Set up logging +log = logging.getLogger(__name__) +# Define the module's virtual name +__virtualname__ = 'vcenter' + + +def __virtual__(): + ''' + Only load if the vsphere execution module is available. + ''' + if HAS_JSONSCHEMA: + return __virtualname__ + + return False, 'The vcenter proxy module did not load.' + + +def init(opts): + ''' + This function gets called when the proxy starts up. + For login the protocol and port are cached. + ''' + log.info('Initting vcenter proxy module in process {0}' + ''.format(os.getpid())) + log.trace('VCenter Proxy Validating vcenter proxy input') + schema = VCenterProxySchema.serialize() + log.trace('schema = {}'.format(schema)) + proxy_conf = merge(opts.get('proxy', {}), __pillar__.get('proxy', {})) + log.trace('proxy_conf = {0}'.format(proxy_conf)) + try: + jsonschema.validate(proxy_conf, schema) + except jsonschema.exceptions.ValidationError as exc: + raise salt.exceptions.InvalidConfigError(exc) + + # Save mandatory fields in cache + for key in ('vcenter', 'mechanism'): + DETAILS[key] = proxy_conf[key] + + # Additional validation + if DETAILS['mechanism'] == 'userpass': + if 'username' not in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'userpass\' , but no ' + '\'username\' key found in proxy config') + if 'passwords' not in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'userpass\' , but no ' + '\'passwords\' key found in proxy config') + for key in ('username', 'passwords'): + DETAILS[key] = proxy_conf[key] + else: + if 'domain' not in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'sspi\' , but no ' + '\'domain\' key found in proxy config') + if 'principal' not in proxy_conf: + raise salt.exceptions.InvalidConfigError( + 'Mechanism is set to \'sspi\' , but no ' + '\'principal\' key found in proxy config') + for key in ('domain', 'principal'): + DETAILS[key] = proxy_conf[key] + + # Save optional + DETAILS['protocol'] = proxy_conf.get('protocol') + DETAILS['port'] = proxy_conf.get('port') + + # Test connection + if DETAILS['mechanism'] == 'userpass': + # Get the correct login details + log.info('Retrieving credentials and testing vCenter connection for ' + 'mehchanism \'userpass\'') + try: + username, password = find_credentials() + DETAILS['password'] = password + except salt.exceptions.SaltSystemExit as err: + log.critical('Error: {0}'.format(err)) + return False + return True + + +def ping(): + ''' + Returns True. + + CLI Example: + + .. code-block:: bash + + salt vcenter test.ping + ''' + return True + + +def shutdown(): + ''' + Shutdown the connection to the proxy device. For this proxy, + shutdown is a no-op. + ''' + log.debug('VCenter proxy shutdown() called...') + + +def find_credentials(): + ''' + Cycle through all the possible credentials and return the first one that + works. + ''' + + # if the username and password were already found don't fo though the + # connection process again + if 'username' in DETAILS and 'password' in DETAILS: + return DETAILS['username'], DETAILS['password'] + + passwords = __pillar__['proxy']['passwords'] + for password in passwords: + DETAILS['password'] = password + if not __salt__['vsphere.test_vcenter_connection'](): + # We are unable to authenticate + continue + # If we have data returned from above, we've successfully authenticated. + return DETAILS['username'], password + # We've reached the end of the list without successfully authenticating. + raise salt.exceptions.VMwareConnectionError('Cannot complete login due to ' + 'incorrect credentials.') + + +def get_details(): + ''' + Function that returns the cached details + ''' + return DETAILS diff --git a/salt/states/pbm.py b/salt/states/pbm.py new file mode 100644 index 000000000000..00945fc65cf6 --- /dev/null +++ b/salt/states/pbm.py @@ -0,0 +1,501 @@ +# -*- coding: utf-8 -*- +''' +Manages VMware storage policies +(called pbm because the vCenter endpoint is /pbm) + +Examples +======== + +Storage policy +-------------- + +.. code-block:: python + +{ + "name": "salt_storage_policy" + "description": "Managed by Salt. Random capability values.", + "resource_type": "STORAGE", + "subprofiles": [ + { + "capabilities": [ + { + "setting": { + "type": "scalar", + "value": 2 + }, + "namespace": "VSAN", + "id": "hostFailuresToTolerate" + }, + { + "setting": { + "type": "scalar", + "value": 2 + }, + "namespace": "VSAN", + "id": "stripeWidth" + }, + { + "setting": { + "type": "scalar", + "value": true + }, + "namespace": "VSAN", + "id": "forceProvisioning" + }, + { + "setting": { + "type": "scalar", + "value": 50 + }, + "namespace": "VSAN", + "id": "proportionalCapacity" + }, + { + "setting": { + "type": "scalar", + "value": 0 + }, + "namespace": "VSAN", + "id": "cacheReservation" + } + ], + "name": "Rule-Set 1: VSAN", + "force_provision": null + } + ], +} + +Dependencies +============ + + +- pyVmomi Python Module + + +pyVmomi +------- + +PyVmomi can be installed via pip: + +.. code-block:: bash + + pip install pyVmomi + +.. note:: + + Version 6.0 of pyVmomi has some problems with SSL error handling on certain + versions of Python. If using version 6.0 of pyVmomi, Python 2.6, + Python 2.7.9, or newer must be present. This is due to an upstream dependency + in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the + version of Python is not in the supported range, you will need to install an + earlier version of pyVmomi. See `Issue #29537`_ for more information. + +.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 +''' + +# Import Python Libs +from __future__ import absolute_import +import logging +import copy +import sys + +# Import Salt Libs +from salt.exceptions import CommandExecutionError, ArgumentValueError +from salt.utils.dictdiffer import recursive_diff +from salt.utils.listdiffer import list_diff + +# External libraries +try: + from pyVmomi import VmomiSupport + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + +# Get Logging Started +log = logging.getLogger(__name__) + + +def __virtual__(): + if not HAS_PYVMOMI: + return False, 'State module did not load: pyVmomi not found' + + # We check the supported vim versions to infer the pyVmomi version + if 'vim25/6.0' in VmomiSupport.versionMap and \ + sys.version_info > (2, 7) and sys.version_info < (2, 7, 9): + + return False, ('State module did not load: Incompatible versions ' + 'of Python and pyVmomi present. See Issue #29537.') + return True + + +def mod_init(low): + ''' + Init function + ''' + return True + + +def default_vsan_policy_configured(name, policy): + ''' + Configures the default VSAN policy on a vCenter. + The state assumes there is only one default VSAN policy on a vCenter. + + policy + Dict representation of a policy + ''' + # TODO Refactor when recurse_differ supports list_differ + # It's going to make the whole thing much easier + policy_copy = copy.deepcopy(policy) + proxy_type = __salt__['vsphere.get_proxy_type']() + log.trace('proxy_type = {0}'.format(proxy_type)) + # All allowed proxies have a shim execution module with the same + # name which implementes a get_details function + # All allowed proxies have a vcenter detail + vcenter = __salt__['{0}.get_details'.format(proxy_type)]()['vcenter'] + log.info('Running {0} on vCenter ' + '\'{1}\''.format(name, vcenter)) + log.trace('policy = {0}'.format(policy)) + changes_required = False + ret = {'name': name, 'changes': {}, 'result': None, 'comment': None, + 'pchanges': {}} + comments = [] + changes = {} + changes_required = False + si = None + + try: + #TODO policy schema validation + si = __salt__['vsphere.get_service_instance_via_proxy']() + current_policy = __salt__['vsphere.list_default_vsan_policy'](si) + log.trace('current_policy = {0}'.format(current_policy)) + # Building all diffs between the current and expected policy + # XXX We simplify the comparison by assuming we have at most 1 + # sub_profile + if policy.get('subprofiles'): + if len(policy['subprofiles']) > 1: + raise ArgumentValueError('Multiple sub_profiles ({0}) are not ' + 'supported in the input policy') + subprofile = policy['subprofiles'][0] + current_subprofile = current_policy['subprofiles'][0] + capabilities_differ = list_diff(current_subprofile['capabilities'], + subprofile.get('capabilities', []), + key='id') + del policy['subprofiles'] + if subprofile.get('capabilities'): + del subprofile['capabilities'] + del current_subprofile['capabilities'] + # Get the subprofile diffs without the capability keys + subprofile_differ = recursive_diff(current_subprofile, + dict(subprofile)) + + del current_policy['subprofiles'] + policy_differ = recursive_diff(current_policy, policy) + if policy_differ.diffs or capabilities_differ.diffs or \ + subprofile_differ.diffs: + + if 'name' in policy_differ.new_values or \ + 'description' in policy_differ.new_values: + + raise ArgumentValueError( + '\'name\' and \'description\' of the default VSAN policy ' + 'cannot be updated') + changes_required = True + if __opts__['test']: + str_changes = [] + if policy_differ.diffs: + str_changes.extend([change for change in + policy_differ.changes_str.split('\n')]) + if subprofile_differ.diffs or capabilities_differ.diffs: + str_changes.append('subprofiles:') + if subprofile_differ.diffs: + str_changes.extend( + [' {0}'.format(change) for change in + subprofile_differ.changes_str.split('\n')]) + if capabilities_differ.diffs: + str_changes.append(' capabilities:') + str_changes.extend( + [' {0}'.format(change) for change in + capabilities_differ.changes_str2.split('\n')]) + comments.append( + 'State {0} will update the default VSAN policy on ' + 'vCenter \'{1}\':\n{2}' + ''.format(name, vcenter, '\n'.join(str_changes))) + else: + __salt__['vsphere.update_storage_policy']( + policy=current_policy['name'], + policy_dict=policy_copy, + service_instance=si) + comments.append('Updated the default VSAN policy in vCenter ' + '\'{0}\''.format(vcenter)) + log.info(comments[-1]) + + new_values = policy_differ.new_values + new_values['subprofiles'] = [subprofile_differ.new_values] + new_values['subprofiles'][0]['capabilities'] = \ + capabilities_differ.new_values + if not new_values['subprofiles'][0]['capabilities']: + del new_values['subprofiles'][0]['capabilities'] + if not new_values['subprofiles'][0]: + del new_values['subprofiles'] + old_values = policy_differ.old_values + old_values['subprofiles'] = [subprofile_differ.old_values] + old_values['subprofiles'][0]['capabilities'] = \ + capabilities_differ.old_values + if not old_values['subprofiles'][0]['capabilities']: + del old_values['subprofiles'][0]['capabilities'] + if not old_values['subprofiles'][0]: + del old_values['subprofiles'] + changes.update({'default_vsan_policy': + {'new': new_values, + 'old': old_values}}) + log.trace(changes) + __salt__['vsphere.disconnect'](si) + except CommandExecutionError as exc: + log.error('Error: {}'.format(exc)) + if si: + __salt__['vsphere.disconnect'](si) + if not __opts__['test']: + ret['result'] = False + ret.update({'comment': exc.strerror, + 'result': False if not __opts__['test'] else None}) + return ret + if not changes_required: + # We have no changes + ret.update({'comment': ('Default VSAN policy in vCenter ' + '\'{0}\' is correctly configured. ' + 'Nothing to be done.'.format(vcenter)), + 'result': True}) + else: + ret.update({'comment': '\n'.join(comments)}) + if __opts__['test']: + ret.update({'pchanges': changes, + 'result': None}) + else: + ret.update({'changes': changes, + 'result': True}) + return ret + + +def storage_policies_configured(name, policies): + ''' + Configures storage policies on a vCenter. + + policies + List of dict representation of the required storage policies + ''' + comments = [] + changes = [] + changes_required = False + ret = {'name': name, 'changes': {}, 'result': None, 'comment': None, + 'pchanges': {}} + log.trace('policies = {0}'.format(policies)) + si = None + try: + proxy_type = __salt__['vsphere.get_proxy_type']() + log.trace('proxy_type = {0}'.format(proxy_type)) + # All allowed proxies have a shim execution module with the same + # name which implementes a get_details function + # All allowed proxies have a vcenter detail + vcenter = __salt__['{0}.get_details'.format(proxy_type)]()['vcenter'] + log.info('Running state \'{0}\' on vCenter ' + '\'{1}\''.format(name, vcenter)) + si = __salt__['vsphere.get_service_instance_via_proxy']() + current_policies = __salt__['vsphere.list_storage_policies']( + policy_names=[policy['name'] for policy in policies], + service_instance=si) + log.trace('current_policies = {0}'.format(current_policies)) + # TODO Refactor when recurse_differ supports list_differ + # It's going to make the whole thing much easier + for policy in policies: + policy_copy = copy.deepcopy(policy) + filtered_policies = [p for p in current_policies + if p['name'] == policy['name']] + current_policy = filtered_policies[0] \ + if filtered_policies else None + + if not current_policy: + changes_required = True + if __opts__['test']: + comments.append('State {0} will create the storage policy ' + '\'{1}\' on vCenter \'{2}\'' + ''.format(name, policy['name'], vcenter)) + else: + __salt__['vsphere.create_storage_policy']( + policy['name'], policy, service_instance=si) + comments.append('Created storage policy \'{0}\' on ' + 'vCenter \'{1}\''.format(policy['name'], + vcenter)) + changes.append({'new': policy, 'old': None}) + log.trace(comments[-1]) + # Continue with next + continue + + # Building all diffs between the current and expected policy + # XXX We simplify the comparison by assuming we have at most 1 + # sub_profile + if policy.get('subprofiles'): + if len(policy['subprofiles']) > 1: + raise ArgumentValueError('Multiple sub_profiles ({0}) are not ' + 'supported in the input policy') + subprofile = policy['subprofiles'][0] + current_subprofile = current_policy['subprofiles'][0] + capabilities_differ = list_diff(current_subprofile['capabilities'], + subprofile.get('capabilities', []), + key='id') + del policy['subprofiles'] + if subprofile.get('capabilities'): + del subprofile['capabilities'] + del current_subprofile['capabilities'] + # Get the subprofile diffs without the capability keys + subprofile_differ = recursive_diff(current_subprofile, + dict(subprofile)) + + del current_policy['subprofiles'] + policy_differ = recursive_diff(current_policy, policy) + if policy_differ.diffs or capabilities_differ.diffs or \ + subprofile_differ.diffs: + + changes_required = True + if __opts__['test']: + str_changes = [] + if policy_differ.diffs: + str_changes.extend( + [change for change in + policy_differ.changes_str.split('\n')]) + if subprofile_differ.diffs or \ + capabilities_differ.diffs: + + str_changes.append('subprofiles:') + if subprofile_differ.diffs: + str_changes.extend( + [' {0}'.format(change) for change in + subprofile_differ.changes_str.split('\n')]) + if capabilities_differ.diffs: + str_changes.append(' capabilities:') + str_changes.extend( + [' {0}'.format(change) for change in + capabilities_differ.changes_str2.split('\n')]) + comments.append( + 'State {0} will update the storage policy \'{1}\'' + ' on vCenter \'{2}\':\n{3}' + ''.format(name, policy['name'], vcenter, + '\n'.join(str_changes))) + else: + __salt__['vsphere.update_storage_policy']( + policy=current_policy['name'], + policy_dict=policy_copy, + service_instance=si) + comments.append('Updated the storage policy \'{0}\'' + 'in vCenter \'{1}\'' + ''.format(policy['name'], vcenter)) + log.info(comments[-1]) + + # Build new/old values to report what was changed + new_values = policy_differ.new_values + new_values['subprofiles'] = [subprofile_differ.new_values] + new_values['subprofiles'][0]['capabilities'] = \ + capabilities_differ.new_values + if not new_values['subprofiles'][0]['capabilities']: + del new_values['subprofiles'][0]['capabilities'] + if not new_values['subprofiles'][0]: + del new_values['subprofiles'] + old_values = policy_differ.old_values + old_values['subprofiles'] = [subprofile_differ.old_values] + old_values['subprofiles'][0]['capabilities'] = \ + capabilities_differ.old_values + if not old_values['subprofiles'][0]['capabilities']: + del old_values['subprofiles'][0]['capabilities'] + if not old_values['subprofiles'][0]: + del old_values['subprofiles'] + changes.append({'new': new_values, + 'old': old_values}) + else: + # No diffs found - no updates required + comments.append('Storage policy \'{0}\' is up to date. ' + 'Nothing to be done.'.format(policy['name'])) + __salt__['vsphere.disconnect'](si) + except CommandExecutionError as exc: + log.error('Error: {0}'.format(exc)) + if si: + __salt__['vsphere.disconnect'](si) + if not __opts__['test']: + ret['result'] = False + ret.update({'comment': exc.strerror, + 'result': False if not __opts__['test'] else None}) + return ret + if not changes_required: + # We have no changes + ret.update({'comment': ('All storage policy in vCenter ' + '\'{0}\' is correctly configured. ' + 'Nothing to be done.'.format(vcenter)), + 'result': True}) + else: + ret.update({'comment': '\n'.join(comments)}) + if __opts__['test']: + ret.update({'pchanges': {'storage_policies': changes}, + 'result': None}) + else: + ret.update({'changes': {'storage_policies': changes}, + 'result': True}) + return ret + + +def default_storage_policy_assigned(name, policy, datastore): + ''' + Assigns a default storage policy to a datastore + + policy + Name of storage policy + + datastore + Name of datastore + ''' + log.info('Running state {0} for policy \'{1}\', datastore \'{2}\'.' + ''.format(name, policy, datastore)) + changes = {} + changes_required = False + ret = {'name': name, 'changes': {}, 'result': None, 'comment': None, + 'pchanges': {}} + si = None + try: + si = __salt__['vsphere.get_service_instance_via_proxy']() + existing_policy = \ + __salt__['vsphere.list_default_storage_policy_of_datastore']( + datastore=datastore, service_instance=si) + if existing_policy['name'] == policy: + comment = ('Storage policy \'{0}\' is already assigned to ' + 'datastore \'{1}\'. Nothing to be done.' + ''.format(policy, datastore)) + else: + changes_required = True + changes = { + 'default_storage_policy': {'old': existing_policy['name'], + 'new': policy}} + if __opts__['test']: + comment = ('State {0} will assign storage policy \'{1}\' to ' + 'datastore \'{2}\'.').format(name, policy, + datastore) + else: + __salt__['vsphere.assign_default_storage_policy_to_datastore']( + policy=policy, datastore=datastore, service_instance=si) + comment = ('Storage policy \'{0} was assigned to datastore ' + '\'{1}\'.').format(policy, name) + log.info(comment) + except CommandExecutionError as exc: + log.error('Error: {}'.format(exc)) + if si: + __salt__['vsphere.disconnect'](si) + ret.update({'comment': exc.strerror, + 'result': False if not __opts__['test'] else None}) + return ret + ret['comment'] = comment + if changes_required: + if __opts__['test']: + ret.update({'result': None, + 'pchanges': changes}) + else: + ret.update({'result': True, + 'changes': changes}) + else: + ret['result'] = True + return ret diff --git a/salt/utils/pbm.py b/salt/utils/pbm.py new file mode 100644 index 000000000000..c7fa43eaa4bb --- /dev/null +++ b/salt/utils/pbm.py @@ -0,0 +1,329 @@ +# -*- coding: utf-8 -*- +''' +Library for VMware Storage Policy management (via the pbm endpoint) + +This library is used to manage the various policies available in VMware + +:codeauthor: Alexandru Bleotu + +Dependencies +~~~~~~~~~~~~ + +- pyVmomi Python Module + +pyVmomi +------- + +PyVmomi can be installed via pip: + +.. code-block:: bash + + pip install pyVmomi + +.. note:: + + versions of Python. If using version 6.0 of pyVmomi, Python 2.6, + Python 2.7.9, or newer must be present. This is due to an upstream dependency + in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the + version of Python is not in the supported range, you will need to install an + earlier version of pyVmomi. See `Issue #29537`_ for more information. + +.. _Issue #29537: https://github.com/saltstack/salt/issues/29537 + +Based on the note above, to install an earlier version of pyVmomi than the +version currently listed in PyPi, run the following: + +.. code-block:: bash + + pip install pyVmomi==5.5.0.2014.1.1 +''' + +# Import Python Libs +from __future__ import absolute_import +import logging + +# Import Salt Libs +import salt.utils.vmware +from salt.exceptions import VMwareApiError, VMwareRuntimeError, \ + VMwareObjectRetrievalError + + +try: + from pyVmomi import pbm, vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +# Get Logging Started +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only load if PyVmomi is installed. + ''' + if HAS_PYVMOMI: + return True + else: + return False, 'Missing dependency: The salt.utils.pbm module ' \ + 'requires the pyvmomi library' + + +def get_profile_manager(service_instance): + ''' + Returns a profile manager + + service_instance + Service instance to the host or vCenter + ''' + stub = salt.utils.vmware.get_new_service_instance_stub( + service_instance, ns='pbm/2.0', path='/pbm/sdk') + pbm_si = pbm.ServiceInstance('ServiceInstance', stub) + try: + profile_manager = pbm_si.RetrieveContent().profileManager + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + return profile_manager + + +def get_placement_solver(service_instance): + ''' + Returns a placement solver + + service_instance + Service instance to the host or vCenter + ''' + stub = salt.utils.vmware.get_new_service_instance_stub( + service_instance, ns='pbm/2.0', path='/pbm/sdk') + pbm_si = pbm.ServiceInstance('ServiceInstance', stub) + try: + profile_manager = pbm_si.RetrieveContent().placementSolver + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + return profile_manager + + +def get_capability_definitions(profile_manager): + ''' + Returns a list of all capability definitions. + + profile_manager + Reference to the profile manager. + ''' + res_type = pbm.profile.ResourceType( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + try: + cap_categories = profile_manager.FetchCapabilityMetadata(res_type) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + cap_definitions = [] + for cat in cap_categories: + cap_definitions.extend(cat.capabilityMetadata) + return cap_definitions + + +def get_policies_by_id(profile_manager, policy_ids): + ''' + Returns a list of policies with the specified ids. + + profile_manager + Reference to the profile manager. + + policy_ids + List of policy ids to retrieve. + ''' + try: + return profile_manager.RetrieveContent(policy_ids) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + + +def get_storage_policies(profile_manager, policy_names=None, + get_all_policies=False): + ''' + Returns a list of the storage policies, filtered by name. + + profile_manager + Reference to the profile manager. + + policy_names + List of policy names to filter by. + Default is None. + + get_all_policies + Flag specifying to return all policies, regardless of the specified + filter. + ''' + res_type = pbm.profile.ResourceType( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + try: + policy_ids = profile_manager.QueryProfile(res_type) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + log.trace('policy_ids = {0}'.format(policy_ids)) + # More policies are returned so we need to filter again + policies = [p for p in get_policies_by_id(profile_manager, policy_ids) + if p.resourceType.resourceType == + pbm.profile.ResourceTypeEnum.STORAGE] + if get_all_policies: + return policies + if not policy_names: + policy_names = [] + return [p for p in policies if p.name in policy_names] + + +def create_storage_policy(profile_manager, policy_spec): + ''' + Creates a storage policy. + + profile_manager + Reference to the profile manager. + + policy_spec + Policy update spec. + ''' + try: + profile_manager.Create(policy_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + + +def update_storage_policy(profile_manager, policy, policy_spec): + ''' + Updates a storage policy. + + profile_manager + Reference to the profile manager. + + policy + Reference to the policy to be updated. + + policy_spec + Policy update spec. + ''' + try: + profile_manager.Update(policy.profileId, policy_spec) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + + +def get_default_storage_policy_of_datastore(profile_manager, datastore): + ''' + Returns the default storage policy reference assigned to a datastore. + + profile_manager + Reference to the profile manager. + + datastore + Reference to the datastore. + ''' + # Retrieve all datastores visible + hub = pbm.placement.PlacementHub( + hubId=datastore._moId, hubType='Datastore') + log.trace('placement_hub = {0}'.format(hub)) + try: + policy_id = profile_manager.QueryDefaultRequirementProfile(hub) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) + policy_refs = get_policies_by_id(profile_manager, [policy_id]) + if not policy_refs: + raise VMwareObjectRetrievalError('Storage policy with id \'{0}\' was ' + 'not found'.format(policy_id)) + return policy_refs[0] + + +def assign_default_storage_policy_to_datastore(profile_manager, policy, + datastore): + ''' + Assigns a storage policy as the default policy to a datastore. + + profile_manager + Reference to the profile manager. + + policy + Reference to the policy to assigned. + + datastore + Reference to the datastore. + ''' + placement_hub = pbm.placement.PlacementHub( + hubId=datastore._moId, hubType='Datastore') + log.trace('placement_hub = {0}'.format(placement_hub)) + try: + profile_manager.AssignDefaultRequirementProfile(policy.profileId, + [placement_hub]) + except vim.fault.NoPermission as exc: + log.exception(exc) + raise VMwareApiError('Not enough permissions. Required privilege: ' + '{0}'.format(exc.privilegeId)) + except vim.fault.VimFault as exc: + log.exception(exc) + raise VMwareApiError(exc.msg) + except vmodl.RuntimeFault as exc: + log.exception(exc) + raise VMwareRuntimeError(exc.msg) diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index d54dbced0421..b0552996e32d 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -79,6 +79,8 @@ import errno import logging import time +import sys +import ssl # Import Salt Libs import salt.exceptions @@ -92,8 +94,9 @@ from salt.ext import six from salt.ext.six.moves.http_client import BadStatusLine # pylint: disable=E0611 try: - from pyVim.connect import GetSi, SmartConnect, Disconnect, GetStub - from pyVmomi import vim, vmodl + from pyVim.connect import GetSi, SmartConnect, Disconnect, GetStub, \ + SoapStubAdapter + from pyVmomi import vim, vmodl, VmomiSupport HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False @@ -405,6 +408,49 @@ def get_service_instance(host, username=None, password=None, protocol=None, return service_instance +def get_new_service_instance_stub(service_instance, path, ns=None, + version=None): + ''' + Returns a stub that points to a different path, + created from an existing connection. + + service_instance + The Service Instance. + + path + Path of the new stub. + + ns + Namespace of the new stub. + Default value is None + + version + Version of the new stub. + Default value is None. + ''' + #For python 2.7.9 and later, the defaul SSL conext has more strict + #connection handshaking rule. We may need turn of the hostname checking + #and client side cert verification + context = None + if sys.version_info[:3] > (2, 7, 8): + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + stub = service_instance._stub + hostname = stub.host.split(':')[0] + session_cookie = stub.cookie.split('"')[1] + VmomiSupport.GetRequestContext()['vcSessionCookie'] = session_cookie + new_stub = SoapStubAdapter(host=hostname, + ns=ns, + path=path, + version=version, + poolSize=0, + sslContext=context) + new_stub.cookie = stub.cookie + return new_stub + + def get_service_instance_from_managed_object(mo_ref, name=''): ''' Retrieves the service instance from a managed object. diff --git a/tests/unit/modules/test_vsphere.py b/tests/unit/modules/test_vsphere.py index 56669b900e80..ed043f272832 100644 --- a/tests/unit/modules/test_vsphere.py +++ b/tests/unit/modules/test_vsphere.py @@ -639,6 +639,14 @@ def setUp(self): 'mechanism': 'fake_mechanism', 'principal': 'fake_principal', 'domain': 'fake_domain'} + self.vcenter_details = {'vcenter': 'fake_vcenter', + 'username': 'fake_username', + 'password': 'fake_password', + 'protocol': 'fake_protocol', + 'port': 'fake_port', + 'mechanism': 'fake_mechanism', + 'principal': 'fake_principal', + 'domain': 'fake_domain'} def tearDown(self): for attrname in ('esxi_host_details', 'esxi_vcenter_details', @@ -693,6 +701,17 @@ def test_esxi_proxy_vcenter_details(self): 'fake_protocol', 'fake_port', 'fake_mechanism', 'fake_principal', 'fake_domain'), ret) + def test_vcenter_proxy_details(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='vcenter')): + with patch.dict(vsphere.__salt__, + {'vcenter.get_details': MagicMock( + return_value=self.vcenter_details)}): + ret = vsphere._get_proxy_connection_details() + self.assertEqual(('fake_vcenter', 'fake_username', 'fake_password', + 'fake_protocol', 'fake_port', 'fake_mechanism', + 'fake_principal', 'fake_domain'), ret) + def test_unsupported_proxy_details(self): with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value='unsupported')): @@ -890,7 +909,7 @@ def setup_loader_modules(self): } def test_supported_proxies(self): - supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -933,7 +952,7 @@ def setup_loader_modules(self): } def test_supported_proxies(self): - supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -974,7 +993,7 @@ def setup_loader_modules(self): } def test_supported_proxies(self): - supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter'] + supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -1049,7 +1068,7 @@ def setup_loader_modules(self): } def test_supported_proxies(self): - supported_proxies = ['esxcluster', 'esxdatacenter'] + supported_proxies = ['esxcluster', 'esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -1127,7 +1146,7 @@ def setup_loader_modules(self): } def test_supported_proxies(self): - supported_proxies = ['esxdatacenter'] + supported_proxies = ['esxdatacenter', 'vcenter'] for proxy_type in supported_proxies: with patch('salt.modules.vsphere.get_proxy_type', MagicMock(return_value=proxy_type)): @@ -1339,12 +1358,15 @@ def setup_loader_modules(self): def setUp(self): attrs = (('mock_si', MagicMock()), ('mock_dc', MagicMock()), - ('mock_cl', MagicMock())) + ('mock_cl', MagicMock()), + ('mock_root', MagicMock())) for attr, mock_obj in attrs: setattr(self, attr, mock_obj) self.addCleanup(delattr, self, attr) attrs = (('mock_get_datacenter', MagicMock(return_value=self.mock_dc)), - ('mock_get_cluster', MagicMock(return_value=self.mock_cl))) + ('mock_get_cluster', MagicMock(return_value=self.mock_cl)), + ('mock_get_root_folder', + MagicMock(return_value=self.mock_root))) for attr, mock_obj in attrs: setattr(self, attr, mock_obj) self.addCleanup(delattr, self, attr) @@ -1360,7 +1382,8 @@ def setUp(self): MagicMock(return_value=(None, None, None, None, None, None, None, None, 'datacenter'))), ('salt.utils.vmware.get_datacenter', self.mock_get_datacenter), - ('salt.utils.vmware.get_cluster', self.mock_get_cluster)) + ('salt.utils.vmware.get_cluster', self.mock_get_cluster), + ('salt.utils.vmware.get_root_folder', self.mock_get_root_folder)) for module, mock_obj in patches: patcher = patch(module, mock_obj) patcher.start() @@ -1409,3 +1432,10 @@ def test_esxdatacenter_proxy_return(self): MagicMock(return_value='esxdatacenter')): ret = vsphere._get_proxy_target(self.mock_si) self.assertEqual(ret, self.mock_dc) + + def test_vcenter_proxy_return(self): + with patch('salt.modules.vsphere.get_proxy_type', + MagicMock(return_value='vcenter')): + ret = vsphere._get_proxy_target(self.mock_si) + self.mock_get_root_folder.assert_called_once_with(self.mock_si) + self.assertEqual(ret, self.mock_root) diff --git a/tests/unit/utils/test_pbm.py b/tests/unit/utils/test_pbm.py new file mode 100644 index 000000000000..6c2be0f9b581 --- /dev/null +++ b/tests/unit/utils/test_pbm.py @@ -0,0 +1,664 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Alexandru Bleotu ` + + Tests functions in salt.utils.vsan +''' + +# Import python libraries +from __future__ import absolute_import +import logging + +# Import Salt testing libraries +from tests.support.unit import TestCase, skipIf +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, \ + PropertyMock + +# Import Salt libraries +from salt.exceptions import VMwareApiError, VMwareRuntimeError, \ + VMwareObjectRetrievalError +from salt.ext.six.moves import range +import salt.utils.pbm + +try: + from pyVmomi import vim, vmodl, pbm + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +# Get Logging Started +log = logging.getLogger(__name__) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetProfileManagerTestCase(TestCase): + '''Tests for salt.utils.pbm.get_profile_manager''' + def setUp(self): + self.mock_si = MagicMock() + self.mock_stub = MagicMock() + self.mock_prof_mgr = MagicMock() + self.mock_content = MagicMock() + self.mock_pbm_si = MagicMock( + RetrieveContent=MagicMock(return_value=self.mock_content)) + type(self.mock_content).profileManager = \ + PropertyMock(return_value=self.mock_prof_mgr) + patches = ( + ('salt.utils.vmware.get_new_service_instance_stub', + MagicMock(return_value=self.mock_stub)), + ('salt.utils.pbm.pbm.ServiceInstance', + MagicMock(return_value=self.mock_pbm_si))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_stub', 'mock_content', + 'mock_pbm_si', 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_new_service_stub(self): + mock_get_new_service_stub = MagicMock() + with patch('salt.utils.vmware.get_new_service_instance_stub', + mock_get_new_service_stub): + salt.utils.pbm.get_profile_manager(self.mock_si) + mock_get_new_service_stub.assert_called_once_with( + self.mock_si, ns='pbm/2.0', path='/pbm/sdk') + + def test_pbm_si(self): + mock_get_pbm_si = MagicMock() + with patch('salt.utils.pbm.pbm.ServiceInstance', + mock_get_pbm_si): + salt.utils.pbm.get_profile_manager(self.mock_si) + mock_get_pbm_si.assert_called_once_with('ServiceInstance', + self.mock_stub) + + def test_return_profile_manager(self): + ret = salt.utils.pbm.get_profile_manager(self.mock_si) + self.assertEqual(ret, self.mock_prof_mgr) + + def test_profile_manager_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_content).profileManager = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_profile_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_profile_manager_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_content).profileManager = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_profile_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_profile_manager_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_content).profileManager = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_profile_manager(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetPlacementSolverTestCase(TestCase): + '''Tests for salt.utils.pbm.get_placement_solver''' + def setUp(self): + self.mock_si = MagicMock() + self.mock_stub = MagicMock() + self.mock_prof_mgr = MagicMock() + self.mock_content = MagicMock() + self.mock_pbm_si = MagicMock( + RetrieveContent=MagicMock(return_value=self.mock_content)) + type(self.mock_content).placementSolver = \ + PropertyMock(return_value=self.mock_prof_mgr) + patches = ( + ('salt.utils.vmware.get_new_service_instance_stub', + MagicMock(return_value=self.mock_stub)), + ('salt.utils.pbm.pbm.ServiceInstance', + MagicMock(return_value=self.mock_pbm_si))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_si', 'mock_stub', 'mock_content', + 'mock_pbm_si', 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_new_service_stub(self): + mock_get_new_service_stub = MagicMock() + with patch('salt.utils.vmware.get_new_service_instance_stub', + mock_get_new_service_stub): + salt.utils.pbm.get_placement_solver(self.mock_si) + mock_get_new_service_stub.assert_called_once_with( + self.mock_si, ns='pbm/2.0', path='/pbm/sdk') + + def test_pbm_si(self): + mock_get_pbm_si = MagicMock() + with patch('salt.utils.pbm.pbm.ServiceInstance', + mock_get_pbm_si): + salt.utils.pbm.get_placement_solver(self.mock_si) + mock_get_pbm_si.assert_called_once_with('ServiceInstance', + self.mock_stub) + + def test_return_profile_manager(self): + ret = salt.utils.pbm.get_placement_solver(self.mock_si) + self.assertEqual(ret, self.mock_prof_mgr) + + def test_placement_solver_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + type(self.mock_content).placementSolver = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_placement_solver(self.mock_si) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_placement_solver_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + type(self.mock_content).placementSolver = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_placement_solver(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_placement_solver_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + type(self.mock_content).placementSolver = PropertyMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_placement_solver(self.mock_si) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetCapabilityDefinitionsTestCase(TestCase): + '''Tests for salt.utils.pbm.get_capability_definitions''' + def setUp(self): + self.mock_res_type = MagicMock() + self.mock_cap_cats = [MagicMock(capabilityMetadata=['fake_cap_meta1', + 'fake_cap_meta2']), + MagicMock(capabilityMetadata=['fake_cap_meta3'])] + self.mock_prof_mgr = MagicMock( + FetchCapabilityMetadata=MagicMock(return_value=self.mock_cap_cats)) + patches = ( + ('salt.utils.pbm.pbm.profile.ResourceType', + MagicMock(return_value=self.mock_res_type)),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_res_type', 'mock_cap_cats', 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_res_type(self): + mock_get_res_type = MagicMock() + with patch('salt.utils.pbm.pbm.profile.ResourceType', + mock_get_res_type): + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + mock_get_res_type.assert_called_once_with( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + + def test_fetch_capabilities(self): + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.mock_prof_mgr.FetchCapabilityMetadata.assert_called_once_with( + self.mock_res_type) + + def test_fetch_capabilities_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.FetchCapabilityMetadata = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_fetch_capabilities_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.FetchCapabilityMetadata = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_fetch_capabilities_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.FetchCapabilityMetadata = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_return_cap_definitions(self): + ret = salt.utils.pbm.get_capability_definitions(self.mock_prof_mgr) + self.assertEqual(ret, ['fake_cap_meta1', 'fake_cap_meta2', + 'fake_cap_meta3']) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetPoliciesByIdTestCase(TestCase): + '''Tests for salt.utils.pbm.get_policies_by_id''' + def setUp(self): + self.policy_ids = MagicMock() + self.mock_policies = MagicMock() + self.mock_prof_mgr = MagicMock( + RetrieveContent=MagicMock(return_value=self.mock_policies)) + + def tearDown(self): + for attr in ('policy_ids', 'mock_policies', 'mock_prof_mgr'): + delattr(self, attr) + + def test_retrieve_policies(self): + salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.mock_prof_mgr.RetrieveContent.assert_called_once_with( + self.policy_ids) + + def test_retrieve_policies_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.RetrieveContent = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_retrieve_policies_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.RetrieveContent = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_retrieve_policies_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.RetrieveContent = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_return_policies(self): + ret = salt.utils.pbm.get_policies_by_id(self.mock_prof_mgr, self.policy_ids) + self.assertEqual(ret, self.mock_policies) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetStoragePoliciesTestCase(TestCase): + '''Tests for salt.utils.pbm.get_storage_policies''' + def setUp(self): + self.mock_res_type = MagicMock() + self.mock_policy_ids = MagicMock() + self.mock_prof_mgr = MagicMock( + QueryProfile=MagicMock(return_value=self.mock_policy_ids)) + # Policies + self.mock_policies = [] + for i in range(4): + mock_obj = MagicMock(resourceType=MagicMock( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE)) + mock_obj.name = 'fake_policy{0}'.format(i) + self.mock_policies.append(mock_obj) + patches = ( + ('salt.utils.pbm.pbm.profile.ResourceType', + MagicMock(return_value=self.mock_res_type)), + ('salt.utils.pbm.get_policies_by_id', + MagicMock(return_value=self.mock_policies))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_res_type', 'mock_policy_ids', 'mock_policies', + 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_res_type(self): + mock_get_res_type = MagicMock() + with patch('salt.utils.pbm.pbm.profile.ResourceType', + mock_get_res_type): + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + mock_get_res_type.assert_called_once_with( + resourceType=pbm.profile.ResourceTypeEnum.STORAGE) + + def test_retrieve_policy_ids(self): + mock_retrieve_policy_ids = MagicMock(return_value=self.mock_policy_ids) + self.mock_prof_mgr.QueryProfile = mock_retrieve_policy_ids + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + mock_retrieve_policy_ids.assert_called_once_with(self.mock_res_type) + + def test_retrieve_policy_ids_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.QueryProfile = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_retrieve_policy_ids_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.QueryProfile = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_retrieve_policy_ids_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.QueryProfile = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_get_policies_by_id(self): + mock_get_policies_by_id = MagicMock(return_value=self.mock_policies) + with patch('salt.utils.pbm.get_policies_by_id', + mock_get_policies_by_id): + salt.utils.pbm.get_storage_policies(self.mock_prof_mgr) + mock_get_policies_by_id.assert_called_once_with( + self.mock_prof_mgr, self.mock_policy_ids) + + def test_return_all_policies(self): + ret = salt.utils.pbm.get_storage_policies(self.mock_prof_mgr, + get_all_policies=True) + self.assertEqual(ret, self.mock_policies) + + def test_return_filtered_policies(self): + ret = salt.utils.pbm.get_storage_policies( + self.mock_prof_mgr, policy_names=['fake_policy1', 'fake_policy3']) + self.assertEqual(ret, [self.mock_policies[1], self.mock_policies[3]]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class CreateStoragePolicyTestCase(TestCase): + '''Tests for salt.utils.pbm.create_storage_policy''' + def setUp(self): + self.mock_policy_spec = MagicMock() + self.mock_prof_mgr = MagicMock() + + def tearDown(self): + for attr in ('mock_policy_spec', 'mock_prof_mgr'): + delattr(self, attr) + + def test_create_policy(self): + salt.utils.pbm.create_storage_policy(self.mock_prof_mgr, + self.mock_policy_spec) + self.mock_prof_mgr.Create.assert_called_once_with( + self.mock_policy_spec) + + def test_create_policy_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.Create = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.create_storage_policy(self.mock_prof_mgr, + self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_create_policy_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.Create = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.create_storage_policy(self.mock_prof_mgr, + self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_create_policy_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.Create = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.create_storage_policy(self.mock_prof_mgr, + self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class UpdateStoragePolicyTestCase(TestCase): + '''Tests for salt.utils.pbm.update_storage_policy''' + def setUp(self): + self.mock_policy_spec = MagicMock() + self.mock_policy = MagicMock() + self.mock_prof_mgr = MagicMock() + + def tearDown(self): + for attr in ('mock_policy_spec', 'mock_policy', 'mock_prof_mgr'): + delattr(self, attr) + + def test_create_policy(self): + salt.utils.pbm.update_storage_policy( + self.mock_prof_mgr, self.mock_policy, self.mock_policy_spec) + self.mock_prof_mgr.Update.assert_called_once_with( + self.mock_policy.profileId, self.mock_policy_spec) + + def test_create_policy_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.Update = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.update_storage_policy( + self.mock_prof_mgr, self.mock_policy, self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_create_policy_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.Update = MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.update_storage_policy( + self.mock_prof_mgr, self.mock_policy, self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_create_policy_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.Update = MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.update_storage_policy( + self.mock_prof_mgr, self.mock_policy, self.mock_policy_spec) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetDefaultStoragePolicyOfDatastoreTestCase(TestCase): + '''Tests for salt.utils.pbm.get_default_storage_policy_of_datastore''' + def setUp(self): + self.mock_ds = MagicMock(_moId='fake_ds_moid') + self.mock_hub = MagicMock() + self.mock_policy_id = 'fake_policy_id' + self.mock_prof_mgr = MagicMock( + QueryDefaultRequirementProfile=MagicMock( + return_value=self.mock_policy_id)) + self.mock_policy_refs = [MagicMock()] + patches = ( + ('salt.utils.pbm.pbm.placement.PlacementHub', + MagicMock(return_value=self.mock_hub)), + ('salt.utils.pbm.get_policies_by_id', + MagicMock(return_value=self.mock_policy_refs))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_ds', 'mock_hub', 'mock_policy_id', 'mock_prof_mgr', + 'mock_policy_refs'): + delattr(self, attr) + + def test_get_placement_hub(self): + mock_get_placement_hub = MagicMock() + with patch('salt.utils.pbm.pbm.placement.PlacementHub', + mock_get_placement_hub): + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + mock_get_placement_hub.assert_called_once_with( + hubId='fake_ds_moid', hubType='Datastore') + + def test_query_default_requirement_profile(self): + mock_query_prof = MagicMock(return_value=self.mock_policy_id) + self.mock_prof_mgr.QueryDefaultRequirementProfile = \ + mock_query_prof + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + mock_query_prof.assert_called_once_with(self.mock_hub) + + def test_query_default_requirement_profile_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.QueryDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_query_default_requirement_profile_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.QueryDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_query_default_requirement_profile_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.QueryDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') + + def test_get_policies_by_id(self): + mock_get_policies_by_id = MagicMock() + with patch('salt.utils.pbm.get_policies_by_id', + mock_get_policies_by_id): + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + mock_get_policies_by_id.assert_called_once_with( + self.mock_prof_mgr, [self.mock_policy_id]) + + def test_no_policy_refs(self): + mock_get_policies_by_id = MagicMock() + with patch('salt.utils.pbm.get_policies_by_id', + MagicMock(return_value=None)): + with self.assertRaises(VMwareObjectRetrievalError) as excinfo: + salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, + 'Storage policy with id \'fake_policy_id\' was not ' + 'found') + + def test_return_policy_ref(self): + mock_get_policies_by_id = MagicMock() + ret = salt.utils.pbm.get_default_storage_policy_of_datastore( + self.mock_prof_mgr, self.mock_ds) + self.assertEqual(ret, self.mock_policy_refs[0]) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class AssignDefaultStoragePolicyToDatastoreTestCase(TestCase): + '''Tests for salt.utils.pbm.assign_default_storage_policy_to_datastore''' + def setUp(self): + self.mock_ds = MagicMock(_moId='fake_ds_moid') + self.mock_policy = MagicMock() + self.mock_hub = MagicMock() + self.mock_prof_mgr = MagicMock() + patches = ( + ('salt.utils.pbm.pbm.placement.PlacementHub', + MagicMock(return_value=self.mock_hub)),) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + def tearDown(self): + for attr in ('mock_ds', 'mock_hub', 'mock_policy', 'mock_prof_mgr'): + delattr(self, attr) + + def test_get_placement_hub(self): + mock_get_placement_hub = MagicMock() + with patch('salt.utils.pbm.pbm.placement.PlacementHub', + mock_get_placement_hub): + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + mock_get_placement_hub.assert_called_once_with( + hubId='fake_ds_moid', hubType='Datastore') + + def test_assign_default_requirement_profile(self): + mock_assign_prof = MagicMock() + self.mock_prof_mgr.AssignDefaultRequirementProfile = \ + mock_assign_prof + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + mock_assign_prof.assert_called_once_with( + self.mock_policy.profileId, [self.mock_hub]) + + def test_assign_default_requirement_profile_raises_no_permissions(self): + exc = vim.fault.NoPermission() + exc.privilegeId = 'Fake privilege' + self.mock_prof_mgr.AssignDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, + 'Not enough permissions. Required privilege: ' + 'Fake privilege') + + def test_assign_default_requirement_profile_raises_vim_fault(self): + exc = vim.fault.VimFault() + exc.msg = 'VimFault msg' + self.mock_prof_mgr.AssignDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareApiError) as excinfo: + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, 'VimFault msg') + + def test_assign_default_requirement_profile_raises_runtime_fault(self): + exc = vmodl.RuntimeFault() + exc.msg = 'RuntimeFault msg' + self.mock_prof_mgr.AssignDefaultRequirementProfile = \ + MagicMock(side_effect=exc) + with self.assertRaises(VMwareRuntimeError) as excinfo: + salt.utils.pbm.assign_default_storage_policy_to_datastore( + self.mock_prof_mgr, self.mock_policy, self.mock_ds) + self.assertEqual(excinfo.exception.strerror, 'RuntimeFault msg') diff --git a/tests/unit/utils/vmware/test_connection.py b/tests/unit/utils/vmware/test_connection.py index 4a95e9b67fc2..d8afbb0504c1 100644 --- a/tests/unit/utils/vmware/test_connection.py +++ b/tests/unit/utils/vmware/test_connection.py @@ -13,6 +13,7 @@ import sys # Import Salt testing libraries +from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import TestCase, skipIf from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock, call, \ PropertyMock @@ -852,6 +853,96 @@ def test_connected_to_invalid_entity(self): excinfo.exception.strerror) +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') +class GetNewServiceInstanceStub(TestCase, LoaderModuleMockMixin): + '''Tests for salt.utils.vmware.get_new_service_instance_stub''' + def setup_loader_modules(self): + return {salt.utils.vmware: { + '__virtual__': MagicMock(return_value='vmware'), + 'sys': MagicMock(), + 'ssl': MagicMock()}} + + def setUp(self): + self.mock_stub = MagicMock( + host='fake_host:1000', + cookie='ignore"fake_cookie') + self.mock_si = MagicMock( + _stub=self.mock_stub) + self.mock_ret = MagicMock() + self.mock_new_stub = MagicMock() + self.context_dict = {} + patches = (('salt.utils.vmware.VmomiSupport.GetRequestContext', + MagicMock( + return_value=self.context_dict)), + ('salt.utils.vmware.SoapStubAdapter', + MagicMock(return_value=self.mock_new_stub))) + for mod, mock in patches: + patcher = patch(mod, mock) + patcher.start() + self.addCleanup(patcher.stop) + + type(salt.utils.vmware.sys).version_info = \ + PropertyMock(return_value=(2, 7, 9)) + self.mock_context = MagicMock() + self.mock_create_default_context = \ + MagicMock(return_value=self.mock_context) + salt.utils.vmware.ssl.create_default_context = \ + self.mock_create_default_context + + def tearDown(self): + for attr in ('mock_stub', 'mock_si', 'mock_ret', 'mock_new_stub', + 'context_dict', 'mock_context', + 'mock_create_default_context'): + delattr(self, attr) + + def test_ssl_default_context_loaded(self): + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path') + self.mock_create_default_context.assert_called_once_with() + self.assertFalse(self.mock_context.check_hostname) + self.assertEqual(self.mock_context.verify_mode, + salt.utils.vmware.ssl.CERT_NONE) + + def test_ssl_default_context_not_loaded(self): + type(salt.utils.vmware.sys).version_info = \ + PropertyMock(return_value=(2, 7, 8)) + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path') + self.assertEqual(self.mock_create_default_context.call_count, 0) + + def test_session_cookie_in_context(self): + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path') + self.assertEqual(self.context_dict['vcSessionCookie'], 'fake_cookie') + + def test_get_new_stub(self): + mock_get_new_stub = MagicMock() + with patch('salt.utils.vmware.SoapStubAdapter', mock_get_new_stub): + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path', 'fake_ns', 'fake_version') + mock_get_new_stub.assert_called_once_with( + host='fake_host', ns='fake_ns', path='fake_path', + version='fake_version', poolSize=0, sslContext=self.mock_context) + + def test_get_new_stub_2_7_8_python(self): + type(salt.utils.vmware.sys).version_info = \ + PropertyMock(return_value=(2, 7, 8)) + mock_get_new_stub = MagicMock() + with patch('salt.utils.vmware.SoapStubAdapter', mock_get_new_stub): + salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path', 'fake_ns', 'fake_version') + mock_get_new_stub.assert_called_once_with( + host='fake_host', ns='fake_ns', path='fake_path', + version='fake_version', poolSize=0, sslContext=None) + + def test_new_stub_returned(self): + ret = salt.utils.vmware.get_new_service_instance_stub( + self.mock_si, 'fake_path') + self.assertEqual(self.mock_new_stub.cookie, 'ignore"fake_cookie') + self.assertEqual(ret, self.mock_new_stub) + + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not HAS_PYVMOMI, 'The \'pyvmomi\' library is missing') class GetServiceInstanceFromManagedObjectTestCase(TestCase):