diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 833ec304..bcd6d0f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.7 For Nox + - name: Set up Python 3.10 For Nox uses: actions/setup-python@v4 with: python-version: "3.10" @@ -64,12 +64,9 @@ jobs: max-parallel: 4 matrix: python-version: - - 3.7 - - 3.8 - - 3.9 - "3.10" salt-version: - - 3006.4 + - 3006.9 steps: - uses: actions/checkout@v2 @@ -187,7 +184,7 @@ jobs: python-version: - "3.10" salt-version: - - 3006.4 + - 3006.9 steps: - uses: actions/checkout@v2 @@ -206,7 +203,6 @@ jobs: shell: bash env: SALT_REQUIREMENT: salt==${{ matrix.salt-version }} - EXTRA_REQUIREMENTS_INSTALL: Cython run: | export PATH="/C/Program Files (x86)/Windows Kits/10/bin/10.0.18362.0/x64;$PATH" nox --force-color -e tests-3 --install-only @@ -310,7 +306,7 @@ jobs: python-version: - "3.10" salt-version: - - 3006.4 + - 3006.9 steps: - uses: actions/checkout@v2 diff --git a/docs/conf.py b/docs/conf.py index 15d7cae7..29ea5cf8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -148,8 +148,17 @@ # <---- Autodoc Config ----------------------------------------------------------------------------------------------- linkcheck_timeout = 10 + +# Ignoring linkcheck for links migrated from vmware to broadcom +linkcheck_ignore = [ + r"https://developer\.vmware\.com/.*", + r"http://pubs\.vmware\.com/.*", + r"https://code\.vmware\.com/.*", +] if not os.environ.get("SKIP_LINKCHECK_IGNORE"): - linkcheck_ignore = ["https://docs.github.com/en/authentication/connecting-to-github-with-ssh"] + linkcheck_ignore.append( + "https://docs.github.com/en/authentication/connecting-to-github-with-ssh" + ) def setup(app): diff --git a/docs/ref/modules/all.rst b/docs/ref/modules/all.rst index 3134b990..1cfaeb8a 100644 --- a/docs/ref/modules/all.rst +++ b/docs/ref/modules/all.rst @@ -11,6 +11,8 @@ Execution Modules saltext.vmware.modules.cluster saltext.vmware.modules.cluster_drs saltext.vmware.modules.cluster_ha + saltext.vmware.modules.compliance_control + saltext.vmware.modules.controller_metadata saltext.vmware.modules.datacenter saltext.vmware.modules.datastore saltext.vmware.modules.dvportgroup diff --git a/docs/ref/modules/saltext.vmware.modules.compliance_control.rst b/docs/ref/modules/saltext.vmware.modules.compliance_control.rst new file mode 100644 index 00000000..7902d68d --- /dev/null +++ b/docs/ref/modules/saltext.vmware.modules.compliance_control.rst @@ -0,0 +1,6 @@ + +saltext.vmware.modules.compliance_control +========================================= + +.. automodule:: saltext.vmware.modules.compliance_control + :members: diff --git a/docs/ref/modules/saltext.vmware.modules.controller_metadata.rst b/docs/ref/modules/saltext.vmware.modules.controller_metadata.rst new file mode 100644 index 00000000..2c342880 --- /dev/null +++ b/docs/ref/modules/saltext.vmware.modules.controller_metadata.rst @@ -0,0 +1,6 @@ + +saltext.vmware.modules.controller_metadata +========================================== + +.. automodule:: saltext.vmware.modules.controller_metadata + :members: diff --git a/docs/ref/states/all.rst b/docs/ref/states/all.rst index c6ee2f5f..2eaed20b 100644 --- a/docs/ref/states/all.rst +++ b/docs/ref/states/all.rst @@ -8,6 +8,8 @@ State Modules .. autosummary:: :toctree: + saltext.vmware.states.compliance_control + saltext.vmware.states.controller_metadata saltext.vmware.states.datacenter saltext.vmware.states.datastore saltext.vmware.states.esxi diff --git a/docs/ref/states/saltext.vmware.states.compliance_control.rst b/docs/ref/states/saltext.vmware.states.compliance_control.rst new file mode 100644 index 00000000..19031842 --- /dev/null +++ b/docs/ref/states/saltext.vmware.states.compliance_control.rst @@ -0,0 +1,6 @@ + +saltext.vmware.states.compliance_control +======================================== + +.. automodule:: saltext.vmware.states.compliance_control + :members: diff --git a/docs/ref/states/saltext.vmware.states.controller_metadata.rst b/docs/ref/states/saltext.vmware.states.controller_metadata.rst new file mode 100644 index 00000000..f3bc1e8f --- /dev/null +++ b/docs/ref/states/saltext.vmware.states.controller_metadata.rst @@ -0,0 +1,6 @@ + +saltext.vmware.states.controller_metadata +========================================= + +.. automodule:: saltext.vmware.states.controller_metadata + :members: diff --git a/setup.cfg b/setup.cfg index 33cceb9c..78503088 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,12 +43,13 @@ install_requires = pyvmomi==7.0.3 importlib_metadata; python_version < "3.8" jinja2>=3.1.0 + config_modules_vmware [options.extras_require] tests = pytest>=6.1.0 pytest-cov - pytest-salt-factories>=1.0.0rc27 + pytest-salt-factories>=1.0.1 dev = nox towncrier==21.9.0rc1 diff --git a/src/saltext/vmware/modules/compliance_control.py b/src/saltext/vmware/modules/compliance_control.py new file mode 100644 index 00000000..a14afcdb --- /dev/null +++ b/src/saltext/vmware/modules/compliance_control.py @@ -0,0 +1,77 @@ +# SPDX-License: Apache-2.0 +import logging + +import salt.exceptions +import saltext.vmware.utils.compliance_control as compliance_control_util +from config_modules_vmware.interfaces.controller_interface import ControllerInterface + +log = logging.getLogger(__name__) + +__virtualname__ = "vmware_compliance_control" + + +def __virtual__(): + return __virtualname__ + + +def control_config_compliance_check(control_config, product, auth_context=None): + """ + Checks compliance of control config. Control config can be ntp, dns, syslog, etc. + Returns control compliance response object. + + control_config + control config dict object. + product + appliance name - vcenter, sddc-manager, etc. + auth_context + optional auth context to access product. + """ + + log.info("Checking compliance %s", control_config) + if not auth_context: + config = __opts__ + auth_context = compliance_control_util.create_auth_context(config=config, product=product) + + try: + controller_interface_obj = ControllerInterface(auth_context) + response_check_compliance = controller_interface_obj.check_compliance( + desired_state_spec=control_config + ) + log.debug("Response for compliance check %s", response_check_compliance) + return response_check_compliance + except Exception as exc: + log.error("Compliance check encountered an error: %s", str(exc)) + raise salt.exceptions.VMwareRuntimeError(str(exc)) + + +def control_config_remediate(control_config, product, auth_context=None): + """ + Remediate given compliance control config. Control config can be ntp, dns, syslog, etc. + Returns remediation response object. + + control_config + control config dict object. + product + appliance name. vcenter, sddc-manager, etc. + auth_context + Optional auth context to access product. + """ + + log.info("Remediation : %s", control_config) + + if not auth_context: + config = __opts__ + auth_context = compliance_control_util.create_auth_context(config=config, product=product) + + try: + controller_interface_obj = ControllerInterface(auth_context) + response_remediate = controller_interface_obj.remediate_with_desired_state( + desired_state_spec=control_config + ) + log.debug("Remediation response %s", response_remediate) + return response_remediate + + except Exception as exc: + # Handle exceptions by setting status as false and including exception details + log.error("Remediation encountered an error: %s", str(exc)) + raise salt.exceptions.VMwareRuntimeError(str(exc)) diff --git a/src/saltext/vmware/modules/controller_metadata.py b/src/saltext/vmware/modules/controller_metadata.py new file mode 100644 index 00000000..6c6a378a --- /dev/null +++ b/src/saltext/vmware/modules/controller_metadata.py @@ -0,0 +1,30 @@ +# SPDX-License: Apache-2.0 +import logging + +import salt.exceptions +from config_modules_vmware.interfaces.metadata_interface import ControllerMetadataInterface + +log = logging.getLogger(__name__) + +__virtualname__ = "vmware_controller_metadata" + + +def __virtual__(): + return __virtualname__ + + +def validate(controller_metadata): + """ + Validates that the controller custom metadata is valid - has correct product/controls, format, and types. + + controller_metadata + controller metadata dict to validate + """ + + log.info("Validating controller metadata: %s", controller_metadata) + + try: + ControllerMetadataInterface.validate_custom_metadata(controller_metadata) + except Exception as exc: + log.error("Error when validating controller metadata: %s", str(exc)) + raise salt.exceptions.VMwareRuntimeError(str(exc)) diff --git a/src/saltext/vmware/states/compliance_control.py b/src/saltext/vmware/states/compliance_control.py new file mode 100644 index 00000000..7e76ef14 --- /dev/null +++ b/src/saltext/vmware/states/compliance_control.py @@ -0,0 +1,175 @@ +# SPDX-License-Identifier: Apache-2.0 +import json +import logging + +import saltext.vmware.utils.compliance_control as compliance_control_util +from config_modules_vmware.framework.models.output_models.compliance_response import ( + ComplianceStatus, +) +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +log = logging.getLogger(__name__) + +__virtualname__ = "vmware_compliance_control" +__proxyenabled__ = ["vmware_compliance_control"] + + +def __virtual__(): + return __virtualname__ + + +def check_control(name, control_config, product, ids=None): + """ + Check and apply vcenter control configs. Control config can be ntp, dns, syslog, etc. + Return control compliance response if test=true. Otherwise, return remediate response. + + name + Config name + control_config + vc control config dict object. + product + appliance name. vcenter, nsx, etc. + ids + List of product ids within the parent product. + """ + + log.info("Starting compliance check for %s", name) + + config = __opts__ + auth_context = compliance_control_util.create_auth_context( + config=config, product=product, ids=ids + ) + control_config = json.loads(json.dumps(control_config)) + log.debug("Opts: %s", __opts__) + + try: + compliance_config = control_config.get("compliance_config") + if not isinstance(compliance_config, dict) or not compliance_config: + raise Exception(f"Desired spec is empty or not in correct format") + else: + product_control_config = control_config.get("compliance_config", {}).get(product) + if product_control_config: + control_config = {"compliance_config": {product: product_control_config}} + else: + err_msg = f"Desired spec is empty for {product}" + log.error(err_msg) + return { + "name": name, + "result": None, + "changes": {}, + "comment": err_msg, + } + + if __opts__["test"]: + # If in test mode, perform audit + log.info("Running in test mode. Performing check_control_compliance_response.") + check_control_compliance_response = __salt__[ + "vmware_compliance_control.control_config_compliance_check" + ]( + control_config=control_config, + product=product, + auth_context=auth_context, + ) + + if ( + check_control_compliance_response["status"] == ComplianceStatus.COMPLIANT + or check_control_compliance_response["status"] == ComplianceStatus.SKIPPED + ): + ret = { + "name": name, + "result": True, + "comment": check_control_compliance_response["status"], + "changes": check_control_compliance_response.get("changes", {}), + } + elif ( + check_control_compliance_response["status"] == ComplianceStatus.NON_COMPLIANT + or check_control_compliance_response["status"] == ComplianceStatus.FAILED + ): + ret = { + "name": name, + "result": None + if check_control_compliance_response["status"] == ComplianceStatus.NON_COMPLIANT + else False, + "comment": check_control_compliance_response["status"], + "changes": check_control_compliance_response.get("changes", {}), + } + else: + # Exception running compliance workflow + ret = { + "name": name, + "result": False, + "comment": "Failed to run compliance check. Please check changes for more details.", + "changes": { + "message": check_control_compliance_response.get( + "message", "Exception running compliance." + ) + }, + } + else: + # Not in test mode, proceed with pre-check and remediation + log.debug("Performing remediation.") + remediate_response = __salt__["vmware_compliance_control.control_config_remediate"]( + control_config=control_config, + product=product, + auth_context=auth_context, + ) + + if remediate_response["status"] == RemediateStatus.SUCCESS: + log.debug("Remediation completed with success status.") + # Update return data for successful remediation + ret = { + "name": name, + "result": True, + "changes": remediate_response.get("changes", {}), + "comment": "Remediation completed successfully.", + } + elif remediate_response["status"] == RemediateStatus.SKIPPED: + log.debug("Remediation completed with skipped status.") + ret = { + "name": name, + "result": True, + "changes": remediate_response.get("changes", {}), + "comment": "Nothing to remediate.", + } + elif remediate_response["status"] == RemediateStatus.FAILED: + log.debug("Remediation failed.") + # Update return data for failed remediation + ret = { + "name": name, + "result": False, + "comment": "Remediation failed.", + "changes": remediate_response.get("changes", {}), + } + elif remediate_response["status"] == RemediateStatus.PARTIAL: + log.debug("Remediation completed partially.") + ret = { + "name": name, + "result": False, + "comment": "Remediation completed with status partial.", + "changes": remediate_response.get("changes", {}), + } + else: + # Exception running remediation workflow + ret = { + "name": name, + "result": False, + "comment": remediate_response["status"], + "changes": { + "message": remediate_response.get( + "message", "Exception running remediation." + ), + }, + } + + except Exception as e: + # Exception occurred + log.error("An error occurred: %s", str(e)) + return { + "name": name, + "result": False, + "changes": {}, + "comment": f"An error occurred: {str(e)}", + } + + log.debug("Completed workflow for %s", name) + return ret diff --git a/src/saltext/vmware/states/controller_metadata.py b/src/saltext/vmware/states/controller_metadata.py new file mode 100644 index 00000000..78ffc293 --- /dev/null +++ b/src/saltext/vmware/states/controller_metadata.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: Apache-2.0 +import json +import logging + +log = logging.getLogger(__name__) + +__virtualname__ = "vmware_controller_metadata" +__proxyenabled__ = ["vmware_controller_metadata"] + + +def __virtual__(): + return __virtualname__ + + +def managed(name, controller_metadata, **kwargs): + """ + Validates that the controller custom metadata is valid - has correct product/controls, format, and types. + If valid, will then invoke the file.managed module to persist the controller custom metadata. + + name + The location of the file to manage, as an absolute path. + controller_metadata + controller metadata dict to validate and persist + kwargs + Keyword arguments to pass to 'file.managed' state module + """ + + log.info("Starting controller_metadata.managed for %s", name) + + __salt__["vmware_controller_metadata.validate"]( + controller_metadata=controller_metadata, + ) + + log.info("Controller metadata successfully validated") + + log.info("Calling file.managed for controller metadata") + return __states__["file.managed"](name, contents=json.dumps(controller_metadata), **kwargs) diff --git a/src/saltext/vmware/utils/compliance_control.py b/src/saltext/vmware/utils/compliance_control.py new file mode 100644 index 00000000..93d19fcd --- /dev/null +++ b/src/saltext/vmware/utils/compliance_control.py @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: Apache-2.0 +import logging + +import salt.exceptions + +# pylint: disable=no-name-in-module +try: + + from config_modules_vmware.interfaces.controller_interface import ControllerInterface + from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext + from config_modules_vmware.framework.auth.contexts.sddc_manager_context import ( + SDDCManagerContext, + ) + from config_modules_vmware.framework.auth.contexts.base_context import BaseContext + from config_modules_vmware.framework.auth.contexts.esxi_context import EsxiContext + from config_modules_vmware.framework.auth.contexts.vrslcm_context import VrslcmContext + + HAS_CONFIG_MODULE = True +except ImportError: + HAS_CONFIG_MODULE = False + +log = logging.getLogger(__name__) + + +def _create_vcenter_context(conf, fqdn): + return VcenterContext( + hostname=fqdn, + username=conf["user"], + password=conf["password"], + ssl_thumbprint=conf.get("ssl_thumbprint", None), + verify_ssl=conf.get("verify_ssl", True), + ) + + +def _create_sddc_manager_context(conf, fqdn): + return SDDCManagerContext( + hostname=fqdn, + username=conf["user"], + password=conf["password"], + ssl_thumbprint=conf.get("ssl_thumbprint", None), + verify_ssl=conf.get("verify_ssl", True), + ) + + +def _create_esxi_context(vcenter_conf, fqdn=None, ids=None): + return EsxiContext( + vc_hostname=fqdn, + vc_username=vcenter_conf["user"], + vc_password=vcenter_conf["password"], + vc_ssl_thumbprint=vcenter_conf.get("ssl_thumbprint", None), + vc_saml_token=vcenter_conf.get("saml_token", None), + esxi_host_names=ids, + verify_ssl=vcenter_conf.get("verify_ssl", True), + ) + + +def _get_conf(config, product): + if product == BaseContext.ProductEnum.ESXI.value: + # for esxi get parent product (vcenter) conf + product = BaseContext.ProductEnum.VCENTER.value + + return ( + config.get("saltext.vmware") + or config.get("grains", {}).get("saltext.vmware") + or config.get("pillar", {}).get("saltext.vmware") + or config.get(product) + or config.get("pillar", {}).get(product) + or config.get("grains", {}).get(product) + or {} + ) + + +def _create_product_context(config, product, ids=None): + conf = _get_conf(config, product) + # Fetch fqdn from grains if available + fqdn = config.get("grains", {}).get("fqdn") + if not fqdn: + fqdn = conf.get("host") + if product == BaseContext.ProductEnum.VCENTER.value: + return _create_vcenter_context(conf, fqdn) + elif product == BaseContext.ProductEnum.SDDC_MANAGER.value: + return _create_sddc_manager_context(conf, fqdn) + elif product == BaseContext.ProductEnum.NSXT_MANAGER.value: + return BaseContext(BaseContext.ProductEnum.NSXT_MANAGER) + elif product == BaseContext.ProductEnum.NSXT_EDGE.value: + return BaseContext(BaseContext.ProductEnum.NSXT_EDGE) + elif product == BaseContext.ProductEnum.ESXI.value: + return _create_esxi_context(vcenter_conf=conf, fqdn=fqdn, ids=ids) + elif product == BaseContext.ProductEnum.VRSLCM.value: + return VrslcmContext(fqdn) + else: + raise salt.exceptions.VMwareApiError({f"Unsupported product {product}"}) + + +def create_auth_context(config, product, ids=None): + return _create_product_context(config=config, product=product, ids=ids) diff --git a/src/saltext/vmware/utils/connect.py b/src/saltext/vmware/utils/connect.py index 44e28272..b4488c73 100644 --- a/src/saltext/vmware/utils/connect.py +++ b/src/saltext/vmware/utils/connect.py @@ -51,14 +51,16 @@ def get_config(config, profile=None, esxi_host=None): credentials = credentials.get("esxi_host", {}).get(esxi_host) password = credentials.get("password") user = credentials.get("user") + ssl_thumbprint = credentials.get("ssl_thumbprint") else: host = os.environ.get("SALTEXT_VMWARE_HOST") or credentials.get("host") password = os.environ.get("SALTEXT_VMWARE_PASSWORD") or credentials.get("password") user = os.environ.get("SALTEXT_VMWARE_USER") or credentials.get("user") + ssl_thumbprint = credentials.get("ssl_thumbprint") if host is None or password is None or user is None: raise ValueError("Cannot create service instance, VMware credentials incomplete.") - return {"host": host, "user": user, "password": password} + return {"host": host, "user": user, "password": password, "ssl_thumbprint": ssl_thumbprint} def get_service_instance(*, config, esxi_host=None, profile=None): diff --git a/src/saltext/vmware/version.py b/src/saltext/vmware/version.py index 471b8650..a199e20d 100644 --- a/src/saltext/vmware/version.py +++ b/src/saltext/vmware/version.py @@ -1 +1 @@ -__version__ = "23.6.29.0rc1" +__version__ = "23.6.29.0rc2" diff --git a/tests/unit/modules/test_compliance_control.py b/tests/unit/modules/test_compliance_control.py new file mode 100644 index 00000000..3f301477 --- /dev/null +++ b/tests/unit/modules/test_compliance_control.py @@ -0,0 +1,133 @@ +""" +Unit Tests for compliance control execution module. +""" +import logging +from unittest.mock import patch + +import pytest +import salt.exceptions +import saltext.vmware.modules.compliance_control as compliance_control + +log = logging.getLogger(__name__) + + +@pytest.fixture +def configure_loader_modules(): + return {compliance_control: {"__opts__": {}, "__pillar__": {}}} + + +@pytest.fixture(autouse=True) +def patch_salt_loaded_objects(): + with patch( + "saltext.vmware.modules.compliance_control.__opts__", + { + "saltext.vmware": {"host": "test.vcenter.local", "user": "test", "password": "test"}, + }, + create=True, + ), patch.object(compliance_control, "__pillar__", {}, create=True), patch.object( + compliance_control, "__salt__", {}, create=True + ): + yield + + +@pytest.mark.parametrize( + "exception", + [False, True], +) +def test_control_config_compliance_check(exception): + # mock auth + patch_auth_context = patch( + "saltext.vmware.utils.compliance_control.create_auth_context", + autospec=True, + return_value={}, + ) + # mock successful compliance check + mock_response = {"status": "COMPLIANT"} + patch_compliance_check = patch( + "config_modules_vmware.interfaces.controller_interface.ControllerInterface.check_compliance", + autospec=True, + return_value=mock_response, + ) + # mock exception in compliance check + patch_compliance_check_exception = patch( + "config_modules_vmware.interfaces.controller_interface.ControllerInterface.check_compliance", + autospec=True, + side_effect=Exception("Testing"), + ) + # mock input + mock_control_config = { + "compliance_config": { + "vcenter": { + "ntp": { + "value": {"mode": "NTP", "servers": ["ntp server"]}, + "metadata": {"configuration_id": "1246", "configuration_title": "time server"}, + } + } + } + } + if not exception: + with patch_auth_context and patch_compliance_check: + compliance_check_response = compliance_control.control_config_compliance_check( + mock_control_config, product="vcenter" + ) + assert compliance_check_response == mock_response + else: + with patch_auth_context and patch_compliance_check_exception: + with pytest.raises(salt.exceptions.VMwareRuntimeError): + compliance_control.control_config_compliance_check( + mock_control_config, product="vcenter" + ) + + +@pytest.mark.parametrize( + "exception", + [False, True], +) +def test_control_config_remediate(exception): + # mock auth + patch_auth_context = patch( + "saltext.vmware.utils.compliance_control.create_auth_context", + autospec=True, + return_value={}, + ) + # mock successful remediation + mock_response = {"status": "SUCCESS"} + patch_remediate = patch( + "config_modules_vmware.interfaces.controller_interface.ControllerInterface.remediate_with_desired_state", + autospec=True, + return_value=mock_response, + ) + # mock exception in remediation + patch_remediate_exception = patch( + "config_modules_vmware.interfaces.controller_interface.ControllerInterface.remediate_with_desired_state", + autospec=True, + side_effect=Exception("Testing"), + ) + # mock input + mock_control_config = { + "compliance_config": { + "vcenter": { + "ntp": { + "value": {"mode": "NTP", "servers": ["ntp server"]}, + "metadata": {"configuration_id": "1246", "configuration_title": "time server"}, + } + } + } + } + if not exception: + with patch_auth_context and patch_remediate: + remediate_response = compliance_control.control_config_remediate( + mock_control_config, product="vcenter" + ) + assert remediate_response == mock_response + + # Error in remediation test with invalid fields + mock_control_config.update({"invalid_field": "invalid"}) + error_response = compliance_control.control_config_remediate( + mock_control_config, product="vcenter" + ) + assert error_response["status"] == "ERROR" + else: + with patch_auth_context and patch_remediate_exception: + with pytest.raises(salt.exceptions.VMwareRuntimeError): + compliance_control.control_config_remediate(mock_control_config, product="vcenter") diff --git a/tests/unit/modules/test_controller_metadata.py b/tests/unit/modules/test_controller_metadata.py new file mode 100644 index 00000000..25d267eb --- /dev/null +++ b/tests/unit/modules/test_controller_metadata.py @@ -0,0 +1,69 @@ +""" + :codeauthor: VMware +""" +import logging +from unittest.mock import patch + +import pytest +import salt.exceptions +import saltext.vmware.modules.controller_metadata as controller_metadata + +log = logging.getLogger(__name__) + + +@pytest.fixture +def tgz_file(session_temp_dir): + tgz = session_temp_dir / "vmware.tgz" + tgz.write_bytes(b"1") + yield tgz + + +@pytest.fixture +def configure_loader_modules(): + return {controller_metadata: {"__opts__": {}, "__pillar__": {}}} + + +@pytest.fixture(autouse=True) +def patch_salt_loaded_objects(): + # This needs to be the same as the module we're importing + with patch( + "saltext.vmware.modules.controller_metadata.__opts__", + { + "cachedir": ".", + "saltext.vmware": {"host": "test.vcenter.local", "user": "test", "password": "test"}, + }, + create=True, + ), patch.object(controller_metadata, "__pillar__", {}, create=True), patch.object( + controller_metadata, "__salt__", {}, create=True + ): + yield + + +@pytest.mark.parametrize( + "exception", + [False, True], +) +def test_validate(exception): + patch_validate_exception = patch( + "config_modules_vmware.interfaces.metadata_interface.ControllerMetadataInterface.validate_custom_metadata", + autospec=True, + side_effect=Exception("Testing"), + ) + mock_controller_metadata = { + "vcenter": { + "backup_schedule_config": { + "metadata": { + "global_key": "global_value", + "configuration_id": "1112", + "instance_metadata_1": True, + "metadata_2": {"nested_key_1": "nested_key_1_value"}, + } + } + } + } + if exception: + with patch_validate_exception: + with pytest.raises(salt.exceptions.VMwareRuntimeError): + controller_metadata.validate(mock_controller_metadata) + else: + controller_metadata.validate(mock_controller_metadata) diff --git a/tests/unit/states/test_compliance_control.py b/tests/unit/states/test_compliance_control.py new file mode 100644 index 00000000..6a3d5bca --- /dev/null +++ b/tests/unit/states/test_compliance_control.py @@ -0,0 +1,125 @@ +""" +Unit Tests for compliance control state module. +""" +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from saltext.vmware.states import compliance_control + +NAME = "test" + + +@pytest.fixture +def configure_loader_modules(): + return {compliance_control: {"__opts__": {}, "__pillar__": {}}} + + +@pytest.fixture(autouse=True) +def patch_salt_loaded_objects(): + with patch( + "saltext.vmware.states.compliance_control.__opts__", + { + "saltext.vmware": {"host": "test.vcenter.local", "user": "test", "password": "test"}, + }, + create=True, + ), patch.object(compliance_control, "__pillar__", {}, create=True), patch.object( + compliance_control, "__salt__", {}, create=True + ): + yield + + +def mock_valid_control_config(): + return { + "compliance_config": { + "vcenter": { + "ntp": { + "value": {"mode": "NTP", "servers": ["ntp server"]}, + "metadata": {"configuration_id": "1246", "configuration_title": "time server"}, + } + } + } + } + + +def mock_missing_product_control_config(): + return { + "compliance_config": { + "invalid": { + "ntp": { + "value": {"mode": "NTP", "servers": ["ntp server"]}, + "metadata": {"configuration_id": "1246", "configuration_title": "time server"}, + } + } + } + } + + +@pytest.mark.parametrize( + "mock_input, expected_status, expected_comment, expected_result, test_mode", + [ + (mock_valid_control_config(), {"status": "COMPLIANT"}, "COMPLIANT", True, True), + (mock_valid_control_config(), {"status": "SKIPPED"}, "SKIPPED", True, True), + (mock_valid_control_config(), {"status": "NON_COMPLIANT"}, "NON_COMPLIANT", None, True), + (mock_valid_control_config(), {"status": "FAILED"}, "FAILED", False, True), + ( + mock_valid_control_config(), + {"status": "ERROR"}, + "Failed to run compliance check. Please check changes for more details.", + False, + True, + ), + ( + mock_missing_product_control_config(), + {"status": "ERROR"}, + "Desired spec is empty for vcenter", + None, + True, + ), + ( + mock_valid_control_config(), + {"status": "SUCCESS"}, + "Remediation completed successfully.", + True, + False, + ), + (mock_valid_control_config(), {"status": "SKIPPED"}, "Nothing to remediate.", True, False), + (mock_valid_control_config(), {"status": "FAILED"}, "Remediation failed.", False, False), + ( + mock_valid_control_config(), + {"status": "PARTIAL"}, + "Remediation completed with status partial.", + False, + False, + ), + (mock_valid_control_config(), {"status": "ERROR"}, "ERROR", False, False), + ( + {}, + None, + "An error occurred: Desired spec is empty or not in correct format", + False, + False, + ), + ], +) +def test_check_control_config( + mock_input, expected_status, expected_comment, expected_result, test_mode +): + mock_check_control_compliance_config = MagicMock(return_value=expected_status) + mock_check_control_remediate_config = MagicMock(return_value=expected_status) + + with patch.dict( + compliance_control.__salt__, + { + "vmware_compliance_control.control_config_compliance_check": mock_check_control_compliance_config, + "vmware_compliance_control.control_config_remediate": mock_check_control_remediate_config, + }, + ): + with patch.dict(compliance_control.__opts__, {"test": test_mode}): + result = compliance_control.check_control( + name=NAME, control_config=mock_input, product="vcenter" + ) + + assert result is not None + assert result["comment"] == expected_comment + assert result["result"] == expected_result diff --git a/tests/unit/states/test_controller_metadata.py b/tests/unit/states/test_controller_metadata.py new file mode 100644 index 00000000..559fdf12 --- /dev/null +++ b/tests/unit/states/test_controller_metadata.py @@ -0,0 +1,65 @@ +""" + Unit Tests for controller metadata +""" +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from saltext.vmware.states import controller_metadata + +NAME = "test" + + +@pytest.fixture +def configure_loader_modules(): + return {controller_metadata: {"__opts__": {}, "__pillar__": {}}} + + +@pytest.fixture(autouse=True) +def patch_salt_loaded_objects(): + # This needs to be the same as the module we're importing + with patch( + "saltext.vmware.states.controller_metadata.__opts__", + { + "cachedir": ".", + "saltext.vmware": {"host": "test.vcenter.local", "user": "test", "password": "test"}, + }, + create=True, + ), patch.object(controller_metadata, "__pillar__", {}, create=True), patch.object( + controller_metadata, "__salt__", {}, create=True + ): + yield + + +def test_managed(): + mock_validate = MagicMock(return_value=True) + mock_file_managed = MagicMock(return_value={"Result": True}) + mock_controller_metadata = { + "vcenter": { + "backup_schedule_config": { + "metadata": { + "global_key": "global_value", + "configuration_id": "1112", + "instance_metadata_1": True, + "metadata_2": {"nested_key_1": "nested_key_1_value"}, + } + } + } + } + + with patch.dict( + controller_metadata.__salt__, + { + "vmware_controller_metadata.validate": mock_validate, + }, + ): + with patch.dict( + controller_metadata.__states__, + {"file.managed": mock_file_managed}, + ): + result = controller_metadata.managed( + name=NAME, controller_metadata=mock_controller_metadata + ) + + assert result is not None + assert result["Result"] diff --git a/tests/unit/utils/test_compliance_control.py b/tests/unit/utils/test_compliance_control.py new file mode 100644 index 00000000..21f3e802 --- /dev/null +++ b/tests/unit/utils/test_compliance_control.py @@ -0,0 +1,78 @@ +""" +Unit Tests for compliance control utils. +""" +import pytest +import salt.exceptions +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from saltext.vmware.utils import compliance_control + + +def vcenter_pillar(): + return { + "vcenter": { + "host": "test.vcenter.local", + "user": "vcenter-user", + "password": "vcenter-password", + "ssl_thumbprint": "vcenter-thumbprint", + } + } + + +def sddc_manager_pillar(): + return { + "sddc_manager": { + "host": "test.sddc-mgr.local", + "user": "sddc-user", + "password": "sddc-password", + "verify_ssl": False, + } + } + + +@pytest.mark.parametrize( + "product_type, mock_product_pillar", + [ + ("vcenter", vcenter_pillar()), + ("sddc_manager", sddc_manager_pillar()), + ("esxi", vcenter_pillar()), + ], +) +def test_create_auth_context(product_type, mock_product_pillar): + result = compliance_control.create_auth_context( + config=mock_product_pillar, product=product_type, ids=None + ) + + assert result is not None + assert result._product_category == BaseContext.ProductEnum(product_type) + if product_type == "esxi": + product_type = "vcenter" + assert result._hostname == mock_product_pillar.get(product_type).get("host") + assert result._username == mock_product_pillar.get(product_type).get("user") + assert result._password == mock_product_pillar.get(product_type).get("password") + assert result._ssl_thumbprint == mock_product_pillar.get(product_type).get("ssl_thumbprint") + assert result._verify_ssl == mock_product_pillar.get(product_type).get("verify_ssl", True) + + +@pytest.mark.parametrize( + "product_type, mock_product_pillar", + [ + ("nsxt_manager", {"nsxt_manager": {}}), + ("nsxt_edge", {"nsxt_edge": {}}), + ("vrslcm", {"vrslcm": {"grains": {"fqdn": "test.vrslcm.local"}}}), + ], +) +def test_create_base_auth_context(product_type, mock_product_pillar): + result = compliance_control.create_auth_context( + config=mock_product_pillar, product=product_type, ids=None + ) + + assert result is not None + assert result._hostname == mock_product_pillar.get(product_type).get("host") + assert result._product_category == BaseContext.ProductEnum(product_type) + + +def test_unsupported_product_auth_context(): + with pytest.raises(salt.exceptions.VMwareApiError): + compliance_control.create_auth_context( + config={"saltext.vmware": {}}, product="unsupported_product", ids=None + )