diff --git a/CHANGELOG.md b/CHANGELOG.md index 6862f47..bf40bc0 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,55 @@ # CURRENTLY-IN-DEVELOPMENT +# `v0.15.0.0` +### New Controllers +- ESXi Controllers + - 1114 - snmp_config +- VCSA Controllers + - 1225 - ip_based_storage_port_group_config +# `v0.14.7.0` +##### Released by balavigneshr on Sep 18, 2024 @ 09:45 PM UTC +### New Controllers +- ESXi Controllers + - 4 - ssh_host_based_authentication + - 7 - ssh_permit_user_environment + - 16 - ssh_permit_tunnel + - 13 - ssh_gateway_ports + - 147 - ntp_config + - 22 - password_quality_config + - 12 - ssh_compression + - 6 - ssh_permit_empty_passwords + - 11 - ssh_strict_mode + - 14 - ssh_x11_forwarding +### Controller enhancements +- ESXi Controllers + - Fix for alarm_esx_remote_syslog_failure by adding check for expression attribute + - Add version check for rhttpproxy fips 140 esxi control +# Adding schema documentation # `v0.14.6.0` ##### Released by codydouglasBC on Sept 05, 2024 @ 11:07 PM UTC +# `v0.14.5.0` +##### Released by balavigneshr on Aug 30, 2024 @ 10:17 PM UTC +### Bug Fixes +- Delete vCenter REST session as part of the vc_context `__exit__()`. +# `v0.14.4.0` +##### Released by balavigneshr on Aug 28, 2024 @ 05:43 PM UTC +### Controllers +- VCSA Controllers + - VM migration fix for template vms +# `v0.14.3.0` +##### Released by hguangsong on Aug 26, 2024 @ 11:42 PM UTC ### Dependency Version Changes - lxml version requirement changed to "lxml>=4.9.1,<=5.2.2" +# `v0.14.2.0` +##### Released by hguangsong on Aug 23, 2024 @ 07:19 PM UTC +### Dependency Version Changes - requests version requirement changed to "requests>=2.31.0" - pyOpenSSL version requirement changed to "pyOpenSSL>=23.2.0,<=24.0.0" - urllib3 version requirement changed to "urllib3>=1.26.6,<2.0.0" +# `v0.14.1.0` +##### Released by balavigneshr on Aug 21, 2024 @ 08:24 PM UTC +### New Controllers +- ESXi Controllers + - 136 - log_location_config # `v0.14.0.0` ##### Released by ravi-pratap-s on Aug 09, 2024 @ 06:33 PM UTC ### New Controllers diff --git a/README.md b/README.md index 486dc8c..a2b5b28 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,12 @@ Config-modules is a library that can be utilized by services written in python t | 2. [Configuration](docs/configuration.md) | | 3. [Instructions to Create New Controllers](docs/instructions-to-create-new-controllers.md) | | 4. [Testing Controllers](docs/testing-controllers.md) | -| 5. [Controller Documentation](docs/controllers/markdown/index.md) | -| 6. [Metadata](docs/metadata.md) | -| 7. [Functional Test](functional_tests/README.md) | -| 8. [API Service Documentation](docs/api-service.md) | -| 9. [Building and Running in Docker](docs/docker-instructions.md) | +| 5. [Compliance Schema Documentation](docs/compliance-schema-documentation.md) | +| 6. [Controller Documentation](docs/controllers/markdown/index.md) | +| 7. [Metadata](docs/metadata.md) | +| 8. [Functional Test](functional_tests/README.md) | +| 9. [API Service Documentation](docs/api-service.md) | +| 10. [Building and Running in Docker](docs/docker-instructions.md) | ## Directory Structure diff --git a/config_modules_vmware/__init__.py b/config_modules_vmware/__init__.py index 4a76741..3b720b3 100644 --- a/config_modules_vmware/__init__.py +++ b/config_modules_vmware/__init__.py @@ -1,6 +1,6 @@ # Copyright 2024 Broadcom. All Rights Reserved. -version: str = "0.14.6.0" +version: str = "0.15.0.0" name: str = "config_modules_vmware" author: str = "Broadcom" description: str = "VMware Unified Config Modules" diff --git a/config_modules_vmware/controllers/base_controller.py b/config_modules_vmware/controllers/base_controller.py index f926f46..44606fd 100644 --- a/config_modules_vmware/controllers/base_controller.py +++ b/config_modules_vmware/controllers/base_controller.py @@ -3,6 +3,7 @@ from abc import ABC from abc import abstractmethod from typing import Any +from typing import ClassVar from typing import Dict from typing import List from typing import Tuple @@ -12,7 +13,11 @@ from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.get_schema_response import GetSchemaStatus from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.models.output_models.validate_configuration_response import ( + ValidateConfigurationStatus, +) from config_modules_vmware.framework.utils.comparator import Comparator from config_modules_vmware.framework.utils.comparator import ComparatorOptionForList @@ -20,6 +25,8 @@ class BaseController(ABC): + metadata: ClassVar[ControllerMetadata] + def __init__(self): self.comparator_option = ComparatorOptionForList.COMPARE_AFTER_SORT self.instance_key = "name" @@ -105,7 +112,7 @@ def remediate(self, context: BaseContext, desired_values: Any) -> Dict: elif compliance_response.get(consts.STATUS) == ComplianceStatus.COMPLIANT: # For compliant_status as "COMPLIANT", return remediation as skipped. - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} elif compliance_response.get(consts.STATUS) == ComplianceStatus.SKIPPED: # For compliance_status as "SKIPPED", return remediation as SKIPPED since no remediation was performed. @@ -138,3 +145,27 @@ def remediate(self, context: BaseContext, desired_values: Any) -> Dict: else: result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} return result + + def get_schema(self, context: BaseContext) -> Dict: # pylint: disable=W0613 + """Get configuration schema. + Note: This is not yet implemented for compliance. + + :param context: Product context instance. + :type context: BaseContext + :return: Dict of status and schema result. + :rtype: dict + """ + return {consts.RESULT: self.metadata.spec, consts.STATUS: GetSchemaStatus.SUCCESS} + + def validate(self, context: BaseContext, desired_values: Any) -> Dict: # pylint: disable=W0613 + """Validate configuration. + Note: This is not yet implemented for compliance. + + :param context: Product context instance + :type context: BaseContext + :param desired_values: Desired configuration values. + :type desired_values: Any + :return: Dict of status and schema result. + :rtype: dict + """ + return {consts.STATUS: ValidateConfigurationStatus.SKIPPED} diff --git a/config_modules_vmware/controllers/esxi/firewall_rulesets_config.py b/config_modules_vmware/controllers/esxi/firewall_rulesets_config.py index d66c4b7..743b146 100644 --- a/config_modules_vmware/controllers/esxi/firewall_rulesets_config.py +++ b/config_modules_vmware/controllers/esxi/firewall_rulesets_config.py @@ -30,6 +30,8 @@ ADDRESS_KEY = "address" NETWORK_KEY = "network" NETWORK_CIDR_FORMAT = "{}/{}" +USER_CONTROLLABLE = "userControllable" +IP_LIST_CONFIGURABLE = "ipListUserConfigurable" logger = LoggerAdapter(logging.getLogger(__name__)) @@ -136,7 +138,7 @@ def remediate(self, context: HostContext, desired_values: Any) -> Dict: elif compliance_response.get(consts.STATUS) == ComplianceStatus.COMPLIANT: # For compliant case, return SKIPPED. - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} else: # Check for non-compliant items and iterate through each of drifts and invoke remediation. # Scenario 1. Handle addition/removal of ruleset in desired config @@ -220,6 +222,14 @@ def _set_ruleset_config( old = {} firewall_config = context.host_ref.configManager.firewallSystem ruleset_name = non_compliant_desired_config.get(NAME_KEY) + rulesets_metadata = { + ruleset.key: { + USER_CONTROLLABLE: getattr(ruleset, USER_CONTROLLABLE, True), + IP_LIST_CONFIGURABLE: getattr(ruleset, IP_LIST_CONFIGURABLE, True), + } + for ruleset in firewall_config.firewallInfo.ruleset + } + allow_all_ip, allowed_ips = None, None # desired_config dict contains only non-compliant config. This operation requires other keys that are # compliant as well. So get full desired config from input desired_values. @@ -235,13 +245,15 @@ def _set_ruleset_config( for config_key in non_compliant_desired_config.keys(): if config_key == NAME_KEY: continue - if config_key == ENABLED_KEY: + if config_key == ENABLED_KEY and rulesets_metadata[ruleset_name].get(USER_CONTROLLABLE): # Handle Enable/Disable Ruleset configuration. config = non_compliant_desired_config.get(ENABLED_KEY) self._toggle_ruleset( firewall_config, ruleset_name, config, new, old, errors, non_compliant_current_config ) - elif config_key == ALLOW_ALL_IP_KEY or config_key == ALLOWED_IPS_KEY: + elif (config_key == ALLOW_ALL_IP_KEY or config_key == ALLOWED_IPS_KEY) and rulesets_metadata[ + ruleset_name + ].get(IP_LIST_CONFIGURABLE): # create tuples with drift and full desired configs. allow_all_ip = ( desired_config_full.get(ALLOW_ALL_IP_KEY), diff --git a/config_modules_vmware/controllers/esxi/log_location_config.py b/config_modules_vmware/controllers/esxi/log_location_config.py new file mode 100644 index 0000000..3e78734 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/log_location_config.py @@ -0,0 +1,176 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +import re +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +LOG_LOCATION = "log_location" +IS_PERSISTENT = "is_persistent" + + +class LogLocationConfig(BaseController): + """ESXi controller to get/set/check_compliance/remediate persistent log location config. + + | Config Id - 136 + | Config Title - Configure a persistent log location for all locally stored logs. + + """ + + metadata = ControllerMetadata( + name="log_location_config", # controller name + path_in_schema="compliance_config.esxi.log_location_config", + # path in the schema to this controller's definition. + configuration_id="136", # configuration id as defined in compliance kit. + title="Configure a persistent log location for all locally stored logs", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def _is_log_location_persistent(self, context: HostContext) -> bool: + """Check if the log location is persistent. + + :param context: ESXi context instance. + :type context: HostContext + :return: True if log location is persistent otherwise False. + :rtype: bool + :raises: Exception if fetching the config fails. + """ + persistent_log_config_get_command = "system syslog config get" + cli_output, _, _ = context.esx_cli_client().run_esx_cli_cmd(context.hostname, persistent_log_config_get_command) + logger.debug(f"{persistent_log_config_get_command} output is {cli_output}") + + # Fetch persistent flag. + match = re.search(r"Local Log Output Is Persistent:\s*(\w+)", cli_output) + if not match: + err_msg = f"Unable to fetch persistent flag using command esxcli {persistent_log_config_get_command}" + raise Exception(err_msg) + + return match.group(1).lower() == "true" + + def _get_log_location(self, context: HostContext) -> str: + """Get log location for the esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Log location. + :rtype: str + :raises: Exception if fetching the config fails. + """ + persistent_log_config_get_command = "system syslog config get" + out, err, ret_code = context.esx_cli_client().run_esx_cli_cmd( + hostname=context.hostname, command=persistent_log_config_get_command, raise_on_non_zero=False + ) + logger.debug(f"{persistent_log_config_get_command} output is {out}") + + if ret_code: + err_msg = f"Command esxcli {persistent_log_config_get_command} failed." + if out: + err_msg += f" {out}" + if err: + err_msg += f" {err}" + raise Exception(err_msg) + + # Fetch persistent flag. + match = re.search(r"Local Log Output:\s*(/.+)", out) + if not match: + err_msg = f"Unable to fetch log location using command esxcli {persistent_log_config_get_command}" + raise Exception(err_msg) + + return match.group(1) + + def _set_log_location(self, context: HostContext, log_location: str): + """Set log location for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :param log_location: Log location + :type log_location: str + :raises: Exception if there is any error during set operation + """ + persistent_log_config_set_command = f"system syslog config set --logdir={log_location}" + out, err, ret_code = context.esx_cli_client().run_esx_cli_cmd( + hostname=context.hostname, command=persistent_log_config_set_command, raise_on_non_zero=False + ) + if ret_code: + err_msg = f"Command esxcli {persistent_log_config_set_command} failed." + if out: + err_msg += f" {out}" + if err: + err_msg += f" {err}" + raise Exception(err_msg) + + def get(self, context: HostContext) -> Tuple[dict, List[str]]: + """Get persistent log location config for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of dictionary with keys "log_location" and "is_persistent" and a list of errors. + :rtype: Tuple + """ + logger.info("Getting persistent log location config for syslog for esxi.") + errors = [] + persistent_log_config = {} + try: + persistent_log_config = { + IS_PERSISTENT: self._is_log_location_persistent(context), + LOG_LOCATION: self._get_log_location(context), + } + + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return persistent_log_config, errors + + def set(self, context: HostContext, desired_values: dict) -> Tuple[RemediateStatus, List[str]]: + """Set persistent log location config for esxi host. + It sets the log location and verifies if the log location persistent criteria matches with desired or not. + If it does not, then reverts to the original log location and report error + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: dictionary with keys "log_location" and "is_persistent" + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting persistent log location config for syslog for esxi.") + errors = [] + status = RemediateStatus.SUCCESS + try: + current_log_location = self._get_log_location(context) + + is_persistent_required = desired_values.get(IS_PERSISTENT) + + desired_log_location = desired_values.get(LOG_LOCATION) + logger.debug(f"Set the desired logdir to {desired_log_location}") + self._set_log_location(context=context, log_location=desired_log_location) + if is_persistent_required != self._is_log_location_persistent(context): + # Revert the location path and report error message + self._set_log_location(context=context, log_location=current_log_location) + err_msg = ( + f"'log_location: {desired_log_location}' is not matching the " + f"desired criteria 'is_persistent: {is_persistent_required}'" + ) + raise Exception(err_msg) + + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/ntp_config.py b/config_modules_vmware/controllers/esxi/ntp_config.py new file mode 100644 index 0000000..872ed45 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ntp_config.py @@ -0,0 +1,97 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +PROTOCOL = "protocol" +NTP_PROTOCOL = "ntp" +NTP_SERVERS = "servers" + + +class NtpConfig(BaseController): + """ESXi controller to get/set ntp configurations for hosts. + + | Config Id - 147 + | Config Title - ESXi host must configure NTP time synchronization. + + """ + + metadata = ControllerMetadata( + name="ntp_config", # controller name + path_in_schema="compliance_config.esxi.ntp_config", + # path in the schema to this controller's definition. + configuration_id="147", # configuration id as defined in compliance kit. + title="ESXi host must configure NTP time synchronization.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[dict, List[str]]: + """Get ntp configuration for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of dict ({"protocol": "ntp", "server": ["10.0.0.250"]}) and a list of errors. + :rtype: Tuple + """ + logger.info("Getting ntp configurations for esxi.") + time_configs = {} + errors = [] + try: + datetime_info = context.host_ref.config.dateTimeInfo + time_protocol = datetime_info.systemClockProtocol + if time_protocol == NTP_PROTOCOL: + ntp_config = datetime_info.ntpConfig + time_configs[NTP_SERVERS] = [ip for ip in ntp_config.server] + time_configs[PROTOCOL] = time_protocol + logger.debug(f"Datetime configurations for esxi: {time_configs}") + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return time_configs, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ntp configurations for esxi host based on desired values. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: dict of { "protocol": "ntp", "server": ["10.0.0.250"] }. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ntp configiurations for esxi") + errors = [] + status = RemediateStatus.SUCCESS + try: + protocol = desired_values.get(PROTOCOL) + servers = [ip for ip in desired_values.get(NTP_SERVERS)] + datetime_config = vim.host.DateTimeConfig() + datetime_config.protocol = protocol + datetime_config.ntpConfig = vim.host.NtpConfig() + datetime_config.ntpConfig.server = servers + logger.debug(f"Updating ntp configiurations for esxi with {desired_values}") + context.host_ref.configManager.dateTimeSystem.UpdateDateTimeConfig(config=datetime_config) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/password_quality_config.py b/config_modules_vmware/controllers/esxi/password_quality_config.py new file mode 100644 index 0000000..a3a07c6 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/password_quality_config.py @@ -0,0 +1,165 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils.comparator import Comparator + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "Security.PasswordQualityControl" +SYSTEM_DEFAULTS = {"retry": 3, "min": "disabled,disabled,disabled,7,7", "max": 40, "passphrase": 3, "similar": "deny"} + + +class PasswordQualityConfig(BaseController): + """ESXi password quality control configuration. + + | Config Id - 22 + | Config Title - The ESXi host must enforce password complexity. + """ + + metadata = ControllerMetadata( + name="password_quality_config", # controller name + path_in_schema="compliance_config.esxi.password_quality_config", + # path in the schema to this controller's definition. + configuration_id="22", # configuration id as defined in compliance kit. + title="The ESXi host must enforce password complexity.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def _parse_config_string(self, config_str) -> Dict: + """parse config string retrieved from advanced setting. + :param config_str: configuration string. + :type config_str: string + :return: dict of quality control configs + :rtype: dict + """ + password_quality_config = {} + for key_value in config_str.split(): + key, value = key_value.split("=") + password_quality_config[key] = int(value) if key not in ["min", "similar"] else value + return password_quality_config + + def _create_config_string(self, desired_values) -> str: + """create config string desired values. + :param desired_values: desired configurations. + :type desired_value: dict + :return: configs string + :rtype: string + """ + config_str = "" + for key, value in desired_values.items(): + config_str += f"{key}={value} " + return config_str.rstrip() + + def _pre_compare_process(self, configs, desired_values) -> Tuple[dict, dict]: + """Process config string and desired values before compliance check. + :param configs: configurations retrieved from host. + :type configs: dict + :param desired_values: desired configurations. + :type desired_value: dict + :return: tuple of processed configs and desired values + :rtype: tuple + """ + # add missing keys from one dict to another with default values + all_keys = set(configs.keys()).union(desired_values.keys()) + for key in all_keys: + if key not in desired_values: + desired_values[key] = SYSTEM_DEFAULTS[key] + if key not in configs: + configs[key] = SYSTEM_DEFAULTS[key] + return configs, desired_values + + def get(self, context: HostContext) -> Tuple[dict, List[str]]: + """Get password quality control configuration for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of dict for password quality control configs and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting password quality control configuration for esxi.") + errors = [] + password_quality_config = {} + try: + # Fetch configuration from advanced option setting. + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + password_quality_config = self._parse_config_string(result[0].value) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return password_quality_config, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set password quality control configurations for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: dict of desired configs to update ESXi password quality control configurations. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting password quality control configs in advanced option for esxi.") + password_quality_config = self._create_config_string(desired_values) + logger.debug(f"Password quality control configs: {password_quality_config}") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=password_quality_config) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context: HostContext, desired_values: Any) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: ESX context instance. + :type context: HostContext + :param desired_values: Desired values for rulesets. + :type desired_values: Any + :return: Dict of status and list of current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.info("Checking compliance for ESXi password quality control configurations") + password_quality_config, errors = self.get(context=context) + + # If errors are seen during get, return "FAILED" status with errors. + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + password_quality_config, desired_values = self._pre_compare_process(password_quality_config, desired_values) + current, desired = Comparator.get_non_compliant_configs(password_quality_config, desired_values) + if current or desired: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current, + consts.DESIRED: desired, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/esxi/rhttpproxy_fips_140_2_crypt_config.py b/config_modules_vmware/controllers/esxi/rhttpproxy_fips_140_2_crypt_config.py index 56064bf..aa6c561 100644 --- a/config_modules_vmware/controllers/esxi/rhttpproxy_fips_140_2_crypt_config.py +++ b/config_modules_vmware/controllers/esxi/rhttpproxy_fips_140_2_crypt_config.py @@ -1,21 +1,27 @@ # Copyright 2024 Broadcom. All Rights Reserved. import logging import re +from typing import Dict from typing import List from typing import Tuple from config_modules_vmware.controllers.base_controller import BaseController from config_modules_vmware.framework.auth.contexts.base_context import BaseContext from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils +from config_modules_vmware.framework.utils.comparator import Comparator logger = LoggerAdapter(logging.getLogger(__name__)) class RHttpProxyFips140_2CryptConfig(BaseController): - """ESXi controller to get/config ssh fips 140-2 validated cryptographic modules. + """ESXi controller to get/config ssh fips 140-2 validated cryptographic modules. This control is applicable only + below vsphere 8.x version. | Config Id - 1117 | Config Title - The ESXi host rhttpproxy daemon must use FIPS 140-2 validated cryptographic modules to protect the confidentiality of remote access sessions @@ -50,44 +56,88 @@ def get(self, context: HostContext) -> Tuple[bool, List[str]]: logger.info("Getting rhttpproxy daemon FIPS 140-2 validated cryptographic modules config for esxi.") errors = [] enabled = None - try: - rtthpproxy_fips_140_2_get_command = "system security fips140 rhttpproxy get" - cli_output, _, _ = context.esx_cli_client().run_esx_cli_cmd( - context.hostname, rtthpproxy_fips_140_2_get_command - ) - logger.debug(f"cli_output is {cli_output}") - match = re.search(r"Enabled:\s*(\w+)", cli_output) - if not match: - err_msg = ( - f"Unable to fetch rhttpproxy fips config using command esxcli {rtthpproxy_fips_140_2_get_command}" + + # Check product version, if product version is >= 8.0.0, this control is not applicable. + if utils.is_newer_or_same_version(context.product_version, "8.0.0"): + errors.append(consts.SKIPPED) + else: + try: + rtthpproxy_fips_140_2_get_command = "system security fips140 rhttpproxy get" + cli_output, _, _ = context.esx_cli_client().run_esx_cli_cmd( + context.hostname, rtthpproxy_fips_140_2_get_command ) - raise Exception(err_msg) - else: - enabled = match.group(1).lower() == "true" - except Exception as e: - logger.exception(f"An error occurred: {e}") - errors.append(str(e)) + logger.debug(f"cli_output is {cli_output}") + match = re.search(r"Enabled:\s*(\w+)", cli_output) + if not match: + err_msg = f"Unable to fetch rhttpproxy fips config using command esxcli {rtthpproxy_fips_140_2_get_command}" + raise Exception(err_msg) + else: + enabled = match.group(1).lower() == "true" + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) return enabled, errors - def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + def set(self, context: HostContext, desired_values: bool) -> Tuple[RemediateStatus, List[str]]: """Set rhttpproxy daemon FIPS 140-2 validated cryptographic modules config for esxi host based on desired value. :param context: Esxi context instance. :type context: HostContext :param desired_values: boolean value True/False to update config. - :type desired_values: dict + :type desired_values: bool :return: Tuple of "status" and list of error messages. :rtype: Tuple """ logger.info("Setting rhttpproxy daemon FIPS 140-2 validated cryptographic modules config for esxi") errors = [] status = RemediateStatus.SUCCESS - try: - enabled_str = "true" if desired_values is True else "false" - rtthpproxy_fips_140_2_set_command = f"system security fips140 rhttpproxy set --enable={enabled_str}" - context.esx_cli_client().run_esx_cli_cmd(context.hostname, rtthpproxy_fips_140_2_set_command) - except Exception as e: - logger.exception(f"An error occurred: {e}") - errors.append(str(e)) - status = RemediateStatus.FAILED + # Check product version, if product version is >= 8.0.0, this control is not applicable. + if utils.is_newer_or_same_version(context.product_version, "8.0.0"): + errors.append(consts.CONTROL_NOT_APPLICABLE) + status = RemediateStatus.SKIPPED + else: + try: + enabled_str = "true" if desired_values else "false" + rtthpproxy_fips_140_2_set_command = f"system security fips140 rhttpproxy set --enable={enabled_str}" + context.esx_cli_client().run_esx_cli_cmd(context.hostname, rtthpproxy_fips_140_2_set_command) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED return status, errors + + def check_compliance(self, context: HostContext, desired_values: bool) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: boolean value True/False to update config. + :type desired_values: bool + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + current_value, errors = self.get(context=context) + if errors: + if len(errors) == 1 and errors[0] == consts.SKIPPED: + return { + consts.STATUS: ComplianceStatus.SKIPPED, + consts.ERRORS: [consts.CONTROL_NOT_APPLICABLE], + } + # If errors are seen during get, return "FAILED" status with errors. + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + # If no errors seen, compare the current and desired value. If not same, return "NON_COMPLIANT" with values. + # Otherwise, return "COMPLIANT". + current_config, desired_config = Comparator.get_non_compliant_configs( + current_value, desired_values, comparator_option=self.comparator_option, instance_key=self.instance_key + ) + if current_config or desired_config: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_config, + consts.DESIRED: desired_config, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/esxi/snmp_config.py b/config_modules_vmware/controllers/esxi/snmp_config.py new file mode 100644 index 0000000..1769e17 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/snmp_config.py @@ -0,0 +1,187 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SNMP_CONFIG_GET = "system snmp get" +SNMP_CONFIG_SET_V3TARGETS = "system snmp set --v3targets {v3_targets}" +SNMP_CONFIG_SET_ENABLE = "system snmp set --enable {enable}" +SNMP_CONFIG_SET_AUTH = "system snmp set --authentication {auth}" +SNMP_CONFIG_SET_PRIVACY = "system snmp set --privacy {privacy}" +SNMP_CONFIG_SET_COMMUNITIES = "system snmp set --communities {communities}" + +CLI_AUTHENTICATION = "Authentication" +AUTHENTICATION = "authentication" +CLI_PRIVACY = "Privacy" +PRIVACY = "privacy" +CLI_COMMUNITIES = "Communities" +COMMUNITIES = "communities" +CLI_ENABLE = "Enable" +ENABLE = "enable" +CLI_V3TARGETS = "V3targets" +V3TARGETS = "v3_targets" +HOSTNAME = "hostname" +PORT = "port" +USERID = "userid" +SECURITY_LEVEL = "security_level" +MESSAGE_TYPE = "message_type" + + +class SnmpConfig(BaseController): + """ESXi controller to get/set snmp configurations for ESXi host. + + | Config Id - 1114 + | Config Title - SNMP must be configured properly on the ESXi host. + + """ + + metadata = ControllerMetadata( + name="snmp_config", # controller name + path_in_schema="compliance_config.esxi.snmp_config", + # path in the schema to this controller's definition. + configuration_id="1114", # configuration id as defined in compliance kit. + title="SNMP must be configured properly on the ESXi host.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def _parse_snmp_configs(self, cli_output) -> Dict: + """Parse snmp configs retrieved from esxi host. + + :param cli_output: snmp configs received from esxcli output. + :type cli_output: string + :return: Dict of parsed snmp configs. + :rtype: Dict + """ + + snmp_configs = {} + # parse esxcli output line by line + for line in cli_output.strip().split("\n"): + key, value = map(str.strip, line.split(":", 1)) + # if it is authentication + if key == CLI_AUTHENTICATION: + snmp_configs[AUTHENTICATION] = value.strip() or "none" + # if it is privacy + elif key == CLI_PRIVACY: + snmp_configs[PRIVACY] = value.strip() or "none" + # if it is community list + elif key == CLI_COMMUNITIES: + snmp_configs[COMMUNITIES] = [comm.strip() for comm in value.split(",")] + # if it is Enabled + elif key == CLI_ENABLE: + snmp_configs[ENABLE] = value.strip().lower() == "true" + # if it is V3targets, if it is, extract V3targets values + # v3targets contain hostname/IP, port, userid, security level and message type + elif key == CLI_V3TARGETS: + logger.debug(f"V3targets: {value}") + if value: + hostname, rest_v3targets = value.split("@", 1) + port, userid, security_level, message_type = rest_v3targets.strip().split(maxsplit=3) + snmp_configs[V3TARGETS] = { + HOSTNAME: hostname.strip(), + PORT: int(port), + USERID: userid.strip(), + SECURITY_LEVEL: security_level.strip(), + MESSAGE_TYPE: message_type.strip(), + } + + logger.debug(f"Parsed snmp configs: {snmp_configs}") + return snmp_configs + + def _gen_v3targets_str(self, v3_targets) -> str: + """Generate V3targets string for esxcli snmp configs. + + :param v3_targets: V3targets configs. + :type v3_targets: dict + :return: V3targets string for esxcli set. + :rtype: string + """ + return ( + f"{v3_targets[HOSTNAME]}@{v3_targets[PORT]}" + f"/{v3_targets[USERID]}" + f"/{v3_targets[SECURITY_LEVEL]}" + f"/{v3_targets[MESSAGE_TYPE]}" + ) + + def get(self, context: HostContext) -> Tuple[bool, List[str]]: + """Get snmp configs for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of boolean value True/False and a list of errors. + :rtype: Tuple + """ + logger.info("Getting snmp config for esxi.") + errors = [] + snmp_configs = {} + try: + cli_output, _, _ = context.esx_cli_client().run_esx_cli_cmd(context.hostname, SNMP_CONFIG_GET) + logger.debug(f"Snmp configs output for esxi: {cli_output}") + snmp_configs = self._parse_snmp_configs(cli_output) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return snmp_configs, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set snmp config for esxi host based on desired value. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: boolean value True/False to update config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting snmp configs for esxi") + errors = [] + status = RemediateStatus.SUCCESS + try: + esxcli = context.esx_cli_client() + # set snmp authentication + auth = desired_values.get(AUTHENTICATION) + if auth: + esxcli.run_esx_cli_cmd(context.hostname, SNMP_CONFIG_SET_AUTH.format(auth=auth)) + # set snmp privacy + privacy = desired_values.get(PRIVACY) + if privacy: + esxcli.run_esx_cli_cmd(context.hostname, SNMP_CONFIG_SET_PRIVACY.format(privacy=privacy)) + # set snmp v3targets + v3_targets = desired_values.get(V3TARGETS) + if v3_targets: + v3_targets_str = self._gen_v3targets_str(v3_targets) + esxcli.run_esx_cli_cmd(context.hostname, SNMP_CONFIG_SET_V3TARGETS.format(v3_targets=v3_targets_str)) + # set snmp communities + communities = desired_values.get(COMMUNITIES) + if communities: + communities_str = ",".join(map(str, communities)) + esxcli.run_esx_cli_cmd( + context.hostname, SNMP_CONFIG_SET_COMMUNITIES.format(communities=communities_str) + ) + # set snmp enable + enable = desired_values.get(ENABLE) + if enable is not None: + enable_str = "true" if enable is True else "false" + esxcli.run_esx_cli_cmd(context.hostname, SNMP_CONFIG_SET_ENABLE.format(enable=enable_str)) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/ssh_compression_policy.py b/config_modules_vmware/controllers/esxi/ssh_compression_policy.py new file mode 100644 index 0000000..c89e599 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_compression_policy.py @@ -0,0 +1,100 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "compression" + + +class SshCompressionPolicy(BaseController): + """ESXi ssh host compression settings. + The control is automated only for vsphere 8.x and above. No remediation support as the property is no configurable. + + | Config Id - 12 + | Config Title - Disallow compression for the ESXi host SSH daemon. + """ + + metadata = ControllerMetadata( + name="ssh_compression", # controller name + path_in_schema="compliance_config.esxi.ssh_compression", + # path in the schema to this controller's definition. + configuration_id="12", # configuration id as defined in compliance kit. + title="Disallow compression for the ESXi host SSH daemon", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh host compression settings for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'compression' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh host compression policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return "", [consts.SKIPPED] + else: + errors = [] + compression_settings = "" + try: + compression_settings = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return compression_settings, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh host compression settings for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'compression' config. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh host compression policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + errors = [consts.CONTROL_NOT_AUTOMATED] + else: + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors + + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the ssh host compression settings. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + compression_settings, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=compression_settings, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/ssh_gateway_ports_policy.py b/config_modules_vmware/controllers/esxi/ssh_gateway_ports_policy.py new file mode 100644 index 0000000..7da9fa4 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_gateway_ports_policy.py @@ -0,0 +1,111 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "gatewayports" + + +class SshGatewayPortsPolicy(BaseController): + """ESXi ssh gateway ports configuration. The control is automated only for vsphere 8.x and above. + + | Config Id - 13 + | Config Title - ESXi host SSH daemon does not contain gateway ports. + """ + + metadata = ControllerMetadata( + name="ssh_gateway_ports", # controller name + path_in_schema="compliance_config.esxi.ssh_gateway_ports", + # path in the schema to this controller's definition. + configuration_id="13", # configuration id as defined in compliance kit. + title="ESXi host SSH daemon does not contain gateway ports.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh host gateway ports policy for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'gatewayports' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh host gateway ports policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return self._get_skipped() + else: + errors = [] + allow_ssh_gateway_ports = "" + try: + allow_ssh_gateway_ports = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return allow_ssh_gateway_ports, errors + + def _get_skipped(self) -> Tuple[str, List[str]]: + return "", [consts.SKIPPED] + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh host gateway ports policy for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'gatewayports' config. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh host gateway ports policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return self._set_skipped() + else: + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def _set_skipped(self) -> Tuple[RemediateStatus, List[str]]: + return RemediateStatus.SKIPPED, [] + + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the host gateway ports config. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + allow_ssh_gateway_ports, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=allow_ssh_gateway_ports, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/ssh_host_based_auth_policy.py b/config_modules_vmware/controllers/esxi/ssh_host_based_auth_policy.py new file mode 100644 index 0000000..58873b4 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_host_based_auth_policy.py @@ -0,0 +1,111 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "hostbasedauthentication" + + +class SshHostBasedAuthPolicy(BaseController): + """ESXi ssh host based authentication configuration. The control is automated only for vsphere 8.x and above. + + | Config Id - 4 + | Config Title - ESXi host SSH daemon does not allow host-based authentication. + """ + + metadata = ControllerMetadata( + name="ssh_host_based_authentication", # controller name + path_in_schema="compliance_config.esxi.ssh_host_based_authentication", + # path in the schema to this controller's definition. + configuration_id="4", # configuration id as defined in compliance kit. + title="ESXi host SSH daemon does not allow host-based authentication.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh host based auth policy for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'hostbasedauthentication' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh host based authentication policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return self._get_skipped() + else: + errors = [] + allow_host_based_authentication = "" + try: + allow_host_based_authentication = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return allow_host_based_authentication, errors + + def _get_skipped(self) -> Tuple[str, List[str]]: + return "", [consts.SKIPPED] + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh host based auth policy for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'hostbasedauthentication' config. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh host based authentication policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return self._set_skipped() + else: + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def _set_skipped(self) -> Tuple[RemediateStatus, List[str]]: + return RemediateStatus.SKIPPED, [] + + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the ssh host based authentication config. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + allow_host_based_authentication, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=allow_host_based_authentication, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/ssh_ignore_rhosts_policy.py b/config_modules_vmware/controllers/esxi/ssh_ignore_rhosts_policy.py index c9e3041..21cbd21 100644 --- a/config_modules_vmware/controllers/esxi/ssh_ignore_rhosts_policy.py +++ b/config_modules_vmware/controllers/esxi/ssh_ignore_rhosts_policy.py @@ -1,5 +1,6 @@ # Copyright 2024 Broadcom. All Rights Reserved. import logging +from typing import Dict from typing import List from typing import Tuple @@ -19,7 +20,7 @@ class SshIgnoreRHostsPolicy(BaseController): - """ESXi ignore ssh rhosts configuration. + """ESXi ignore ssh rhosts configuration. The control is automated only for vsphere 8.x and above. | Config Id - 3 | Config Title - The ESXi host Secure Shell (SSH) daemon must ignore .rhosts files. @@ -51,25 +52,21 @@ def get(self, context: HostContext) -> Tuple[str, List[str]]: :rtype: Tuple """ logger.info("Getting ssh ignore rhosts policy for esxi.") - major_version = utils.get_product_major_version(context.product_version) - if major_version and major_version >= 8: - return self._get_v8(context) - else: + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): return self._get_skipped() + else: + errors = [] + ssh_ignore_rhosts = "" + try: + ssh_ignore_rhosts = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return ssh_ignore_rhosts, errors def _get_skipped(self) -> Tuple[str, List[str]]: return "", [consts.SKIPPED] - def _get_v8(self, context: HostContext) -> Tuple[str, List[str]]: - errors = [] - ssh_ignore_rhosts = "" - try: - ssh_ignore_rhosts = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) - except Exception as e: - logger.exception(f"An error occurred: {e}") - errors.append(str(e)) - return ssh_ignore_rhosts, errors - def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: """Set ssh ignore rhosts policy for esxi host. @@ -81,22 +78,34 @@ def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, Li :rtype: Tuple """ logger.info("Setting ssh ignore rhosts policy for esxi.") - major_version = utils.get_product_major_version(context.product_version) - if major_version and major_version >= 8: - return self._set_v8(context, desired_values) - else: + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): return self._set_skipped() + else: + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors def _set_skipped(self) -> Tuple[RemediateStatus, List[str]]: return RemediateStatus.SKIPPED, [] - def _set_v8(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: - errors = [] - status = RemediateStatus.SUCCESS - try: - esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) - except Exception as e: - logger.exception(f"An error occurred: {e}") - errors.append(str(e)) - status = RemediateStatus.FAILED - return status, errors + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the host ignore rhosts config. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + ssh_ignore_rhosts, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=ssh_ignore_rhosts, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/ssh_permit_empty_passwords_policy.py b/config_modules_vmware/controllers/esxi/ssh_permit_empty_passwords_policy.py new file mode 100644 index 0000000..b5082a6 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_permit_empty_passwords_policy.py @@ -0,0 +1,100 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "permitemptypasswords" + + +class SshPermitEmptyPasswordsPolicy(BaseController): + """ESXi ssh host permit empty passwords settings. + The control is automated only for vsphere 8.x and above. No remediation support as the property is no configurable. + + | Config Id - 6 + | Config Title - ESXi host SSH daemon rejects authentication using an empty password. + """ + + metadata = ControllerMetadata( + name="ssh_permit_empty_passwords", # controller name + path_in_schema="compliance_config.esxi.ssh_permit_empty_passwords", + # path in the schema to this controller's definition. + configuration_id="6", # configuration id as defined in compliance kit. + title="ESXi host SSH daemon rejects authentication using an empty password", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh host permit empty passwords settings for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'permitemptypasswords' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh host permit empty passwords policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return "", [consts.SKIPPED] + else: + errors = [] + permit_empty_passwords_settings = "" + try: + permit_empty_passwords_settings = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return permit_empty_passwords_settings, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh host permit empty passwords settings for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'permitemptypasswords' config. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh host permit empty passwords policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + errors = [consts.CONTROL_NOT_AUTOMATED] + else: + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors + + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the ssh host permit empty passwords settings. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + permit_empty_passwords_settings, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=permit_empty_passwords_settings, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/ssh_permit_tunnel_policy.py b/config_modules_vmware/controllers/esxi/ssh_permit_tunnel_policy.py new file mode 100644 index 0000000..e54448b --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_permit_tunnel_policy.py @@ -0,0 +1,111 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "permittunnel" + + +class SshPermitTunnelPolicy(BaseController): + """ESXi ssh permit tunnel configuration. The control is automated only for vsphere 8.x and above. + + | Config Id - 16 + | Config Title - ESXi host SSH daemon refuses tunnels. + """ + + metadata = ControllerMetadata( + name="ssh_permit_tunnel", # controller name + path_in_schema="compliance_config.esxi.ssh_permit_tunnel", + # path in the schema to this controller's definition. + configuration_id="16", # configuration id as defined in compliance kit. + title="ESXi host SSH daemon refuses tunnels.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh host permit tunnel policy for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'permittunnel' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh host permit tunnel policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return self._get_skipped() + else: + errors = [] + permit_tunnel_settings = "" + try: + permit_tunnel_settings = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return permit_tunnel_settings, errors + + def _get_skipped(self) -> Tuple[str, List[str]]: + return "", [consts.SKIPPED] + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh host permit tunnel policy for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'permittunnel' config. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh host permit tunnel policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return self._set_skipped() + else: + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def _set_skipped(self) -> Tuple[RemediateStatus, List[str]]: + return RemediateStatus.SKIPPED, [] + + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the host permit tunnel config. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + permit_tunnel_settings, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=permit_tunnel_settings, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/ssh_permit_user_environment_policy.py b/config_modules_vmware/controllers/esxi/ssh_permit_user_environment_policy.py new file mode 100644 index 0000000..a981f68 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_permit_user_environment_policy.py @@ -0,0 +1,111 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "permituserenvironment" + + +class SshPermitUserEnvironmentPolicy(BaseController): + """ESXi ssh host permit user environment configuration. The control is automated only for vsphere 8.x and above. + + | Config Id - 7 + | Config Title - ESXi host SSH daemon does not permit user environment settings. + """ + + metadata = ControllerMetadata( + name="ssh_permit_user_environment", # controller name + path_in_schema="compliance_config.esxi.ssh_permit_user_environment", + # path in the schema to this controller's definition. + configuration_id="7", # configuration id as defined in compliance kit. + title="ESXi host SSH daemon does not permit user environment settings.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh host permit user environment policy for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'permituserenvironment' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh host permit user environment policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return self._get_skipped() + else: + errors = [] + permit_user_environment_settings = "" + try: + permit_user_environment_settings = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return permit_user_environment_settings, errors + + def _get_skipped(self) -> Tuple[str, List[str]]: + return "", [consts.SKIPPED] + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh host permit user environment policy for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'permituserenvironment' config. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh host permit user environment policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return self._set_skipped() + else: + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def _set_skipped(self) -> Tuple[RemediateStatus, List[str]]: + return RemediateStatus.SKIPPED, [] + + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the ssh host permit user environment config. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + permit_user_environment_settings, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=permit_user_environment_settings, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/ssh_port_forwarding_policy.py b/config_modules_vmware/controllers/esxi/ssh_port_forwarding_policy.py index bf8c460..9dec537 100644 --- a/config_modules_vmware/controllers/esxi/ssh_port_forwarding_policy.py +++ b/config_modules_vmware/controllers/esxi/ssh_port_forwarding_policy.py @@ -1,5 +1,6 @@ # Copyright 2024 Broadcom. All Rights Reserved. import logging +from typing import Dict from typing import List from typing import Tuple @@ -19,7 +20,7 @@ class SshPortForwardingPolicy(BaseController): - """ESXi ssh port forwarding configuration. + """ESXi ssh port forwarding configuration. The control is automated only for vsphere 8.x and above. | Config Id - 1111 | Config Title - The ESXi host Secure Shell (SSH) daemon must disable port forwarding. @@ -51,25 +52,21 @@ def get(self, context: HostContext) -> Tuple[str, List[str]]: :rtype: Tuple """ logger.info("Getting ssh port forwarding policy for esxi.") - major_version = utils.get_product_major_version(context.product_version) - if major_version and major_version >= 8: - return self._get_v8(context) - else: + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): return self._get_skipped() + else: + errors = [] + ssh_port_forwarding_enabled = "" + try: + ssh_port_forwarding_enabled = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return ssh_port_forwarding_enabled, errors def _get_skipped(self) -> Tuple[str, List[str]]: return "", [consts.SKIPPED] - def _get_v8(self, context: HostContext) -> Tuple[str, List[str]]: - errors = [] - ssh_port_forwarding_enabled = "" - try: - ssh_port_forwarding_enabled = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) - except Exception as e: - logger.exception(f"An error occurred: {e}") - errors.append(str(e)) - return ssh_port_forwarding_enabled, errors - def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: """Set ssh port forwarding policy for esxi host. @@ -81,22 +78,34 @@ def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, Li :rtype: Tuple """ logger.info("Setting ssh port forwarding policy for esxi.") - major_version = utils.get_product_major_version(context.product_version) - if major_version and major_version >= 8: - return self._set_v8(context, desired_values) - else: + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): return self._set_skipped() + else: + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors def _set_skipped(self) -> Tuple[RemediateStatus, List[str]]: return RemediateStatus.SKIPPED, [] - def _set_v8(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: - errors = [] - status = RemediateStatus.SUCCESS - try: - esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) - except Exception as e: - logger.exception(f"An error occurred: {e}") - errors.append(str(e)) - status = RemediateStatus.FAILED - return status, errors + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the 'AllowTcpForwarding' config. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + ssh_port_forwarding_enabled, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=ssh_port_forwarding_enabled, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/ssh_strict_mode_policy.py b/config_modules_vmware/controllers/esxi/ssh_strict_mode_policy.py new file mode 100644 index 0000000..81ec736 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_strict_mode_policy.py @@ -0,0 +1,100 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "strictmodes" + + +class SshStrictModePolicy(BaseController): + """ESXi ssh host strict mode settings. + The control is automated only for vsphere 8.x and above. No remediation support as the property is no configurable. + + | Config Id - 11 + | Config Title - ESXi host SSH daemon performs strict mode checking of home directory configuration files. + """ + + metadata = ControllerMetadata( + name="ssh_strict_mode", # controller name + path_in_schema="compliance_config.esxi.ssh_strict_mode", + # path in the schema to this controller's definition. + configuration_id="11", # configuration id as defined in compliance kit. + title="ESXi host SSH daemon performs strict mode checking of home directory configuration files", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh host strict mode settings for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'strictmodes' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh host strict mode policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return "", [consts.SKIPPED] + else: + errors = [] + strict_mode_settings = "" + try: + strict_mode_settings = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return strict_mode_settings, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh host strict mode settings for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'strictmodes' config. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh host strict mode policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + errors = [consts.CONTROL_NOT_AUTOMATED] + else: + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors + + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the ssh host strict mode settings. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + strict_mode_settings, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=strict_mode_settings, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/ssh_x11_forwarding_policy.py b/config_modules_vmware/controllers/esxi/ssh_x11_forwarding_policy.py new file mode 100644 index 0000000..7819c01 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_x11_forwarding_policy.py @@ -0,0 +1,100 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "x11forwarding" + + +class SshX11ForwardingPolicy(BaseController): + """ESXi ssh host x11 forwarding settings. + The control is automated only for vsphere 8.x and above. No remediation support as the property is no configurable. + + | Config Id - 14 + | Config Title - ESXi host SSH daemon refuses X11 forwarding. + """ + + metadata = ControllerMetadata( + name="ssh_x11_forwarding", # controller name + path_in_schema="compliance_config.esxi.ssh_x11_forwarding", + # path in the schema to this controller's definition. + configuration_id="14", # configuration id as defined in compliance kit. + title="ESXi host SSH daemon refuses X11 forwarding", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh host x11 forwarding settings for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'x11forwarding' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh host x11 forwarding policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + return "", [consts.SKIPPED] + else: + errors = [] + x11_forwarding_settings = "" + try: + x11_forwarding_settings = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return x11_forwarding_settings, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh host x11 forwarding settings for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'x11forwarding' config. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh host x11 forwarding policy for esxi.") + if not utils.is_newer_or_same_version(context.product_version, "8.0.0"): + errors = [consts.CONTROL_NOT_AUTOMATED] + else: + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors + + def check_compliance(self, context: HostContext, desired_values: str) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: HostContext + :param desired_values: Desired value for the ssh host x11 forwarding settings. + :type desired_values: str + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + x11_forwarding_settings, errors = self.get(context=context) + return esxi_ssh_config_utils.check_compliance_for_ssh_config( + current_value=x11_forwarding_settings, desired_value=desired_values, errors=errors + ) diff --git a/config_modules_vmware/controllers/esxi/utils/esxi_ssh_config_utils.py b/config_modules_vmware/controllers/esxi/utils/esxi_ssh_config_utils.py index 69ae7e4..800912e 100644 --- a/config_modules_vmware/controllers/esxi/utils/esxi_ssh_config_utils.py +++ b/config_modules_vmware/controllers/esxi/utils/esxi_ssh_config_utils.py @@ -1,8 +1,13 @@ # Copyright 2024 Broadcom. All Rights Reserved. import logging +from typing import Dict +from typing import List from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.utils.comparator import Comparator logger = LoggerAdapter(logging.getLogger(__name__)) @@ -47,3 +52,38 @@ def set_ssh_config_value(context: HostContext, config_key: str, config_val: str) """ esx_cli_command = f"system ssh server config set -k {config_key} -v {config_val}" context.esx_cli_client().run_esx_cli_cmd(context.hostname, esx_cli_command) + + +def check_compliance_for_ssh_config(current_value: str, desired_value: str, errors: List) -> Dict: + """Helper method for checking compliance for ssh configurations. + + :param current_value: Current ssh config value. + :type current_value: str + :param desired_value: Desired ssh config value. + :type desired_value: str + :param errors: list + :type errors: errors found during get config. + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + if errors: + if len(errors) == 1 and errors[0] == consts.SKIPPED: + return { + consts.STATUS: ComplianceStatus.SKIPPED, + consts.ERRORS: [consts.CONTROL_NOT_AUTOMATED], + } + # If errors are seen during get, return "FAILED" status with errors. + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + # If no errors seen, compare the current and desired value. If not same, return "NON_COMPLIANT" with values. + # Otherwise, return "COMPLIANT". + current_config, desired_config = Comparator.get_non_compliant_configs(current_value, desired_value) + if current_config or desired_config: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_config, + consts.DESIRED: desired_config, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/sample/sample_controller.py b/config_modules_vmware/controllers/sample/sample_controller.py index f9b6af9..f95ac0d 100644 --- a/config_modules_vmware/controllers/sample/sample_controller.py +++ b/config_modules_vmware/controllers/sample/sample_controller.py @@ -191,7 +191,7 @@ def remediate(self, context, desired_values) -> Dict: elif compliance_response.get(consts.STATUS) == ComplianceStatus.COMPLIANT: # For compliant case, return SKIPPED. - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} elif compliance_response.get(consts.STATUS) != ComplianceStatus.NON_COMPLIANT: # Raise exception for unexpected compliance status (other than FAILED, COMPLIANT, NON_COMPLIANT). diff --git a/config_modules_vmware/controllers/sddc_manager/auto_rotate_schedule.py b/config_modules_vmware/controllers/sddc_manager/auto_rotate_schedule.py index e2eb760..a45c251 100644 --- a/config_modules_vmware/controllers/sddc_manager/auto_rotate_schedule.py +++ b/config_modules_vmware/controllers/sddc_manager/auto_rotate_schedule.py @@ -200,7 +200,7 @@ def remediate(self, context, desired_values: Dict) -> Dict: if non_compliant_credentials.get(AUTO_ROTATE_CREDENTIALS): status, errors = self.set(context=context, desired_values=desired_value) else: - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} if not errors: result = {consts.STATUS: status, consts.OLD: non_compliant_credentials, consts.NEW: desired_value} diff --git a/config_modules_vmware/controllers/vcenter/alarm_remote_syslog_failure_config.py b/config_modules_vmware/controllers/vcenter/alarm_remote_syslog_failure_config.py index 4eb2645..5b72910 100644 --- a/config_modules_vmware/controllers/vcenter/alarm_remote_syslog_failure_config.py +++ b/config_modules_vmware/controllers/vcenter/alarm_remote_syslog_failure_config.py @@ -77,11 +77,13 @@ def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: # Fetch details of all the alarms for which any expression within an alarm has eventId : # ESX_REMOTE_SYSLOG_FAILURE_EVENT for alarm_def in alarm_definitions: - for expression in alarm_def.info.expression.expression: - if isinstance(expression, vim.alarm.EventAlarmExpression): - if expression.eventTypeId == ESX_REMOTE_SYSLOG_FAILURE_EVENT: - target_type = vc_alarms_utils.get_target_type(expression.objectType) - alarms.append(vc_alarms_utils.get_alarm_details(alarm_def, target_type)) + # vim.alarm.EventAlarmExpression might not have 'expression' attribute if there is no expression + if hasattr(alarm_def.info.expression, "expression"): + for expression in alarm_def.info.expression.expression: + if isinstance(expression, vim.alarm.EventAlarmExpression): + if expression.eventTypeId == ESX_REMOTE_SYSLOG_FAILURE_EVENT: + target_type = vc_alarms_utils.get_target_type(expression.objectType) + alarms.append(vc_alarms_utils.get_alarm_details(alarm_def, target_type)) result = alarms except Exception as e: diff --git a/config_modules_vmware/controllers/vcenter/alarm_sso_config.py b/config_modules_vmware/controllers/vcenter/alarm_sso_config.py index 9ce7718..1d9860d 100644 --- a/config_modules_vmware/controllers/vcenter/alarm_sso_config.py +++ b/config_modules_vmware/controllers/vcenter/alarm_sso_config.py @@ -76,6 +76,7 @@ def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: # Fetch details of all the alarms for which any expression within an alarm has eventId : SSO_EVENT_ID for alarm_def in alarm_definitions: + # vim.alarm.EventAlarmExpression might not have 'expression' attribute if there is no expression if hasattr(alarm_def.info.expression, "expression"): for expression in alarm_def.info.expression.expression: if isinstance(expression, vim.alarm.EventAlarmExpression): diff --git a/config_modules_vmware/controllers/vcenter/cert_config.py b/config_modules_vmware/controllers/vcenter/cert_config.py index 0bb7ec0..0e4b505 100644 --- a/config_modules_vmware/controllers/vcenter/cert_config.py +++ b/config_modules_vmware/controllers/vcenter/cert_config.py @@ -89,7 +89,7 @@ def set(self, context: VcenterContext, desired_values) -> Tuple: :return: Tuple of status (RemediateStatus.SKIPPED) and errors if any :rtype: tuple """ - errors = ["Set is not implemented as this control requires manual intervention"] + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] status = RemediateStatus.SKIPPED return status, errors diff --git a/config_modules_vmware/controllers/vcenter/dv_pg_forged_transmits_policy.py b/config_modules_vmware/controllers/vcenter/dv_pg_forged_transmits_policy.py index f3542bc..27de3a7 100644 --- a/config_modules_vmware/controllers/vcenter/dv_pg_forged_transmits_policy.py +++ b/config_modules_vmware/controllers/vcenter/dv_pg_forged_transmits_policy.py @@ -277,7 +277,7 @@ def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: result = self.check_compliance(context, desired_values) if result[consts.STATUS] == ComplianceStatus.COMPLIANT: - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: non_compliant_configs = result[consts.CURRENT] desired_configs = result[consts.DESIRED] diff --git a/config_modules_vmware/controllers/vcenter/dv_pg_mac_address_change_policy.py b/config_modules_vmware/controllers/vcenter/dv_pg_mac_address_change_policy.py index 1c061cd..ab34388 100644 --- a/config_modules_vmware/controllers/vcenter/dv_pg_mac_address_change_policy.py +++ b/config_modules_vmware/controllers/vcenter/dv_pg_mac_address_change_policy.py @@ -284,7 +284,7 @@ def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: result = self.check_compliance(context, desired_values) if result[consts.STATUS] == ComplianceStatus.COMPLIANT: - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: non_compliant_configs = result[consts.CURRENT] desired_configs = result[consts.DESIRED] diff --git a/config_modules_vmware/controllers/vcenter/dv_pg_promiscuous_mode_policy.py b/config_modules_vmware/controllers/vcenter/dv_pg_promiscuous_mode_policy.py index b5e7645..4654b44 100644 --- a/config_modules_vmware/controllers/vcenter/dv_pg_promiscuous_mode_policy.py +++ b/config_modules_vmware/controllers/vcenter/dv_pg_promiscuous_mode_policy.py @@ -283,7 +283,7 @@ def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: result = self.check_compliance(context, desired_values) if result[consts.STATUS] == ComplianceStatus.COMPLIANT: - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: non_compliant_configs = result[consts.CURRENT] desired_configs = result[consts.DESIRED] diff --git a/config_modules_vmware/controllers/vcenter/dvs_health_check_config.py b/config_modules_vmware/controllers/vcenter/dvs_health_check_config.py index 8cf9679..3137f07 100644 --- a/config_modules_vmware/controllers/vcenter/dvs_health_check_config.py +++ b/config_modules_vmware/controllers/vcenter/dvs_health_check_config.py @@ -288,7 +288,7 @@ def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: result = self.check_compliance(context, desired_values) if result[consts.STATUS] == ComplianceStatus.COMPLIANT: - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: non_compliant_items = result[consts.CURRENT] else: diff --git a/config_modules_vmware/controllers/vcenter/dvs_network_io_control_policy.py b/config_modules_vmware/controllers/vcenter/dvs_network_io_control_policy.py index 98e553b..0d29a43 100644 --- a/config_modules_vmware/controllers/vcenter/dvs_network_io_control_policy.py +++ b/config_modules_vmware/controllers/vcenter/dvs_network_io_control_policy.py @@ -289,7 +289,7 @@ def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: result = self.check_compliance(context, desired_values) if result[consts.STATUS] == ComplianceStatus.COMPLIANT: - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: non_compliant_configs = result[consts.CURRENT] else: diff --git a/config_modules_vmware/controllers/vcenter/ip_based_storage_port_group_config.py b/config_modules_vmware/controllers/vcenter/ip_based_storage_port_group_config.py new file mode 100644 index 0000000..06fd4c6 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/ip_based_storage_port_group_config.py @@ -0,0 +1,541 @@ +# Copyright 2024 VMware, Inc. All rights reserved. -- VMware Confidential +import logging +import re +from typing import Any +from typing import Dict +from typing import List +from typing import Set +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SWITCH_NAME = "switch_name" +PORT_GROUP_NAME = "port_group_name" +GLOBAL = "__GLOBAL__" +OVERRIDES = "__OVERRIDES__" +IS_DEDICATED_VLAN = "is_dedicated_vlan" +ALLOW_MIX_TRAFFIC_TYPE = "allow_mix_traffic_type" +VLAN_INFO = "vlan_info" +PORTS = "ports" +NSX_BACKING_TYPE = "nsx" +VSAN_SERVICE = "vsan" +VSAN_WITNESS_SERVICE = "vsanWitness" + + +class IPBasedStoragePortGroupConfig(BaseController): + """Class for ip based storage port groups vlan isolation config with get and set methods. + + Remediation is not supported as it involves different configurations on vsan, iscsi and NFS. Any drifts should + be analyzed based on compliance report and manually remediated. + + | Config Id - 1225 + | Config Title - Isolate all IP-based storage traffic on distributed switches from other traffic types. + + """ + + metadata = ControllerMetadata( + name="ip_based_storage_port_group_config", # controller name + path_in_schema="compliance_config.vcenter.ip_based_storage_port_group_config", # path in the schema to this controller's definition. + configuration_id="1225", # configuration id as defined in compliance kit. + title="Isolate all IP-based storage traffic on distributed switches from other traffic types", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def __init__(self): + super().__init__() + self.all_port_groups_in_vcenter = set() + + @staticmethod + def _get_enabled_services(host: vim.HostSystem, device: str, hosts_nic_port_cache: dict) -> List: + """Helper method to get_enabled_services for the particular port on the host. + + :param host: Host object. + :type host: vim.HostSystem + :param device: VM kernel port adapter device name, + :type device: str + :param hosts_nic_port_cache: Host_nics to port mapping cache. + :type hosts_nic_port_cache: dict + :return: List of enabled services on the VM Kernel. + :rtype: list + """ + services = list() + for vnic_mgr_config in host.config.virtualNicManagerInfo.netConfig: + selectedVnics = vnic_mgr_config.selectedVnic + if selectedVnics: + for vnic_name in selectedVnics: + pattern = r"VirtualNic-(vmk\d+)" + match = re.search(pattern, vnic_name) + if match: + vnic_device = match.group(1) + else: + raise Exception("Incorrect selected vnic!!") + if vnic_device == device: + services.append(vnic_mgr_config.nicType) + host_nic_key = (host.name, vnic_device) + if host_nic_key not in hosts_nic_port_cache: + hosts_nic_port_cache[host_nic_key] = [vnic_mgr_config.nicType] + elif vnic_mgr_config.nicType not in hosts_nic_port_cache[host_nic_key]: + hosts_nic_port_cache[host_nic_key].append(vnic_mgr_config.nicType) + return services + + @staticmethod + def _get_dv_ports(dvs, port_keys) -> List: + """Get dvs ports data. + + :param dvs: distributed virtual switch. + :type dvs: vim.DistributedVirtualSwitch + :param port_keys: port keys. + :type port_keys: string + :return: a list of ports. + :rtype: List + """ + criteria = vim.dvs.PortCriteria() + criteria.portKey = port_keys + dv_ports = dvs.FetchDVPorts(criteria) + return dv_ports + + def _get_iscsi_vmknics(self, context) -> Set: + """Get iscsi binded vmknics. + + :param context: Product context instance. + :type context: VcenterContext + :return: a set of iscsi binded vmknics. + :rtype: Set + """ + vmknics = set() + vc_vsan_vmomi_client = context.vc_vsan_vmomi_client() + # get all vsan enabled clusters + all_vsan_enabled_clusters = vc_vsan_vmomi_client.get_all_vsan_enabled_clusters() + for cluster_ref in all_vsan_enabled_clusters: + # check vsan iscsi target service config (cluster level config) + iscsi_config = vc_vsan_vmomi_client.get_vsan_iscsi_targets_config_for_cluster(cluster_ref) + logger.debug(f"iscsi service config: {iscsi_config}") + if iscsi_config and iscsi_config.enabled: + vmknics.add(iscsi_config.defaultConfig.networkInterface) + # check each individual targets + cluster_iscsi_targets = vc_vsan_vmomi_client.get_vsan_iscsi_targets_for_cluster() + iscsi_targets = cluster_iscsi_targets.GetIscsiTargets(cluster_ref) + for iscsi_target in iscsi_targets: + vmknics.add(iscsi_target.networkInterface) + logger.debug(f"iscsi target: {iscsi_target}") + + return vmknics + + def _get_nfs_networks(self, context) -> Set: + """Get NFS traffic portgroups. + + :param context: Product context instance. + :type context: VcenterContext + :return: a set of NFS portgroups. + :rtype: Set + """ + vc_vsan_vmomi_client = context.vc_vsan_vmomi_client() + vsan_config_system = vc_vsan_vmomi_client.get_vsan_cluster_config_system() + logger.debug(f"vsan config system: {vsan_config_system}") + portgroups = set() + # get all vsan enabled clusters + all_vsan_enabled_clusters = vc_vsan_vmomi_client.get_all_vsan_enabled_clusters() + for cluster_ref in all_vsan_enabled_clusters: + vsan_configs = vsan_config_system.VsanClusterGetConfig(cluster_ref) + if vsan_configs and vsan_configs.fileServiceConfig: + portgroup = vsan_configs.fileServiceConfig.network + logger.debug(f"Portgroup used in NFS file service: {portgroup}") + portgroups.add(portgroup) + + return portgroups + + def _get_portgroup_vlan_info(self, port_group_obj, vlans_counts) -> Dict: + """Get portgroup vlan info. + + :param port_group_obj: portgroup object. + :type port_group_obj: vim.dvs.DistributedVirtualPortgroup + :return: vlan_info. + :rtype: Dict + """ + vlan_spec = port_group_obj.config.defaultPortConfig.vlan + # Check vlan configuration for the port and also build a map of vlans_counts. + # This would be used for checking overlapping of the port group vlan with others. + if isinstance(vlan_spec, vim.dvs.VmwareDistributedVirtualSwitch.VlanIdSpec): + vlan_id = vlan_spec.vlanId + vlan_info = {"vlan_type": "VLAN", "vlan_id": vlan_id} + vlans_counts[vlan_id] = vlans_counts.get(vlan_id, 0) + 1 + elif isinstance(vlan_spec, vim.dvs.VmwareDistributedVirtualSwitch.PvlanSpec): + vlan_id = vlan_spec.pvlanId + vlan_info = {"vlan_type": "Private VLAN", "vlan_id": vlan_id} + vlans_counts[vlan_id] = vlans_counts.get(vlan_id, 0) + 1 + else: + vlan_info = None + return vlan_info + + def _check_ip_based_storage_traffic( + self, dvs, port_group_obj, nfs_portgroups, iscsi_vmknics, hosts_nic_port_cache + ) -> Tuple: + """Check if portgroup is ip storage traffic and get ports data. + + :param dvs: distributed virtual switch. + :type dvs: vim.DistributedVirtualSwitch + :param port_group_obj: portgroup object. + :type port_group_obj: vim.dvs.DistributedVirtualPortgroup + :param nfs_portgroups: a set of portgroups used in NFS traffic. + :type nfs_portgroups: set + :param iscsi_vmknics: a set of vmknics used in iscsi traffic. + :type iscsi_vmknics: set + :param hosts_nic_port_cache: host nic port cache dictionary. + :type hosts_nic_port_cache: dict + :return: A tuple of ip based storage flag and ports data. + :rtype: Tuple + """ + logger.debug(f"Checking portgroup: {port_group_obj.name} for ip based storage traffic") + is_pg_ip_based_storage = False + ports_data = [] + port_keys = port_group_obj.portKeys + dv_ports = self._get_dv_ports(dvs, port_keys) + for dv_port in dv_ports: + # Only check for VM Kernel ports + if not dv_port.connectee or not isinstance(dv_port.connectee.connectedEntity, vim.HostSystem): + continue + host = dv_port.connectee.connectedEntity + device = dv_port.connectee.nicKey + host_nic_key = (host.name, device) + services = hosts_nic_port_cache.get(host_nic_key) + if services is None: + services = self._get_enabled_services(host, device, hosts_nic_port_cache) + logger.debug(f"vmk: {device} services: {services}") + hosts_nic_port_cache[host_nic_key] = services + ports_data.append({"host_name": host.name, "device": device, "services": services}) + if VSAN_SERVICE in services or VSAN_WITNESS_SERVICE in services: + is_pg_ip_based_storage = True + logger.debug(f"vsan service : {services} in portgroup: {port_group_obj.name}") + # check if this vmknic used in iscsi even if service "vsan" not enabled + elif device in iscsi_vmknics: + is_pg_ip_based_storage = True + logger.debug(f"iSCSI vmknics: {device} used in portgroup: {port_group_obj.name}") + if not is_pg_ip_based_storage and port_group_obj in nfs_portgroups: + # if portgroup used in NFS, no matter if VSAN service is enabled + # on its member vmknics, it is considered ip based storage pg + is_pg_ip_based_storage = True + logger.debug(f"NFS portgroup: {port_group_obj.name}") + return is_pg_ip_based_storage, ports_data + + def get(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """Get ip based storage distributed port groups vlan configurations for the vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :return: A tuple containing a dictionary to store ip based storage port groups data and a list of error messages if any. + :rtype: Tuple + """ + + logger.info("Getting all ip based storage port groups info.") + errors = [] + result = [] + try: + # retrieve all distributed switches + all_dv_switches = context.vc_vmomi_client().get_objects_by_vimtype(vim.DistributedVirtualSwitch) + # retrieve all vmknics used in iscsi configurations + iscsi_vmknics = self._get_iscsi_vmknics(context) + logger.debug(f"Vmknics for iscsi: {iscsi_vmknics}") + nfs_portgroups = self._get_nfs_networks(context) + logger.debug(f"Portgroups used in NFS file service: {nfs_portgroups}") + + ip_based_storage_port_groups = dict() + hosts_nic_port_cache = dict() + vlans_counts = dict() + for dvs in all_dv_switches: + for port_group_obj in dvs.portgroup: + # skip nsx backed port and uplink port + is_nsx_backed = getattr(port_group_obj.config, "backingType", "") == NSX_BACKING_TYPE + is_uplink_port_group = getattr(port_group_obj.config, "uplink", False) + if is_nsx_backed or is_uplink_port_group: + continue + + pg_name = port_group_obj.name + self.all_port_groups_in_vcenter.add((dvs.name, pg_name)) + + # get vlan info for this portgroup + vlan_info = self._get_portgroup_vlan_info(port_group_obj, vlans_counts) + + # check if this portgroup is ip storage traffic based portgroup + # criterias to qualify for ip based storage traffic portgroup: + # 1). if the "service" of any vmknics in portgroup marked as "vsan" or "vsanWitness", + # 2). if any vmknics in the portgroup used by iscsi, + # 3). if the portgroup is used as "network" by NFS + is_pg_ip_based_storage, ports_data = self._check_ip_based_storage_traffic( + dvs, port_group_obj, nfs_portgroups, iscsi_vmknics, hosts_nic_port_cache + ) + if is_pg_ip_based_storage: + portgroup_data = {VLAN_INFO: vlan_info, PORTS: ports_data} + ip_based_storage_port_groups.setdefault(dvs.name, {})[pg_name] = portgroup_data + + # Iterate over all ip based storage port groups candidates and create entries in result with + # all required keys including IS_DEDICATED_VLAN. + for switch_name, portgroups in ip_based_storage_port_groups.items(): + for portgroup_name, details in portgroups.items(): + vlan_info = details[VLAN_INFO] + # IF vlan_type is not VLAN or is overlapping with any other port group's VLAN, + # set is_dedicated as False, otherwise True + if vlan_info is None or vlan_info.get("vlan_type") != "VLAN": + is_dedicated_vlan = False + elif "vlan_id" not in vlan_info or vlans_counts.get(vlan_info["vlan_id"], 0) != 1: + is_dedicated_vlan = False + else: + is_dedicated_vlan = True + logger.debug( + f"Adding {portgroup_name} in switch {switch_name} as candidate ip based storage port group " + f"to run check compliance on." + ) + result.append( + { + SWITCH_NAME: switch_name, + PORT_GROUP_NAME: portgroup_name, + IS_DEDICATED_VLAN: is_dedicated_vlan, + PORTS: details[PORTS], + } + ) + + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """Set method is not implemented as this control requires user intervention to remediate. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired value for ip based storage port groups. + :type desired_values: dict + :return: Dict of status (RemediateStatus.SKIPPED) and errors if any + :rtype: Tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors + + def _process_desired_values(self, desired_values: Dict) -> Tuple[dict, dict]: + """Helper method to put desired values in map format. + + :param desired_values: Desired value for ip based storage port groups. + :type desired_values: dict + :return: Return tuple of global desired values and a override map + :rtype: Tuple[dict, dict] + """ + overrides = desired_values.get(OVERRIDES, []) + global_desired_value = desired_values.get(GLOBAL, {}) + + overrides_map = {} + for override in overrides: + switch_name = override.get(SWITCH_NAME) + port_group_name = override.get(PORT_GROUP_NAME) + key = (switch_name, port_group_name) + # Check if port group exists else raise exception to report non-existence of this port group. + if key not in self.all_port_groups_in_vcenter: + raise Exception( + f"Port group provided in desired config overrides does not " + f"exist {port_group_name} in switch {switch_name}" + ) + overrides_map[key] = { + IS_DEDICATED_VLAN: override.get(IS_DEDICATED_VLAN, global_desired_value.get(IS_DEDICATED_VLAN)), + ALLOW_MIX_TRAFFIC_TYPE: override.get( + ALLOW_MIX_TRAFFIC_TYPE, global_desired_value.get(ALLOW_MIX_TRAFFIC_TYPE) + ), + VLAN_INFO: override.get(VLAN_INFO), + } + return global_desired_value, overrides_map + + def _get_non_compliant_configs(self, ip_based_storage_port_groups: List, desired_values: Dict) -> Tuple[List, List]: + """Helper method to get non_compliant configs in the current and report current configs and desired configs. + + :param ip_based_storage_port_groups: Candidate portgroups which have atleast one ip based storage port. + :type ip_based_storage_port_groups: List + :param desired_values: Desired value for ip based storage port groups. + :type desired_values: dict + :return: Return tuple of non_compliance ip based storage port groups as current_configs, desired_configs + :rtype: Tuple[list, list] + """ + current_port_groups_configs = [] + desired_port_groups_configs = [] + errors = [] + try: + global_desired_value, overrides_map = self._process_desired_values(desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return current_port_groups_configs, desired_port_groups_configs, errors + + for pg in ip_based_storage_port_groups: + switch_name = pg.get(SWITCH_NAME) + port_group_name = pg.get(PORT_GROUP_NAME) + key = (switch_name, port_group_name) + pg_current = {} + pg_desired = {} + + # check dedicated vlan for ip based storage traffic + is_current_dedicated_vlan = pg.get(IS_DEDICATED_VLAN) + if key in overrides_map: + is_desired_dedicated_vlan = overrides_map[key].get(IS_DEDICATED_VLAN) + else: + is_desired_dedicated_vlan = desired_values.get(GLOBAL, {}).get(IS_DEDICATED_VLAN) + if is_current_dedicated_vlan != is_desired_dedicated_vlan: + pg_current = {IS_DEDICATED_VLAN: is_current_dedicated_vlan} + pg_desired = {IS_DEDICATED_VLAN: is_desired_dedicated_vlan} + + # check all ports are with ip based storage traffic based on desired spec + allow_mix_traffic_type = ( + overrides_map[key].get(ALLOW_MIX_TRAFFIC_TYPE) + if key in overrides_map + else global_desired_value.get(ALLOW_MIX_TRAFFIC_TYPE) + ) + if allow_mix_traffic_type is not None and not allow_mix_traffic_type: + current_ports = [] + for port in pg.get(PORTS): + # If port is not ip based storage type, append to current, desired to report in check compliance. + services = list(port.get("services")) + if services and not all( + service == VSAN_SERVICE or service == VSAN_WITNESS_SERVICE for service in services + ): + current_ports.append(port) + + if current_ports: + pg_current[PORTS] = current_ports + + # If there is any drift in the port group add switch_name and portgroup_name to the portgroup + # and append to current, desired port groups configs to be reported in check compliance. + if pg_current: + pg_current[SWITCH_NAME] = switch_name + pg_current[PORT_GROUP_NAME] = port_group_name + if allow_mix_traffic_type is not None: + pg_current[ALLOW_MIX_TRAFFIC_TYPE] = allow_mix_traffic_type + current_port_groups_configs.append(pg_current) + if key in overrides_map: + pg_desired[SWITCH_NAME] = switch_name + pg_desired[PORT_GROUP_NAME] = port_group_name + if allow_mix_traffic_type is not None: + pg_desired[ALLOW_MIX_TRAFFIC_TYPE] = allow_mix_traffic_type + desired_port_groups_configs.append(pg_desired) + + # add "__GLOBAL__" portion of desired spec for non-compliant display if any drift found. + if (current_port_groups_configs or desired_port_groups_configs) and global_desired_value: + desired_port_groups_configs.insert(0, {"__GLOBAL__": global_desired_value}) + + return current_port_groups_configs, desired_port_groups_configs, errors + + def check_compliance(self, context: VcenterContext, desired_values: Dict) -> Dict: + """Check compliance of all ip based storage distributed port groups vlan isolation configuration. + + | Sample desired values + + .. code-block:: json + + { + "__GLOBAL__": { + "is_dedicated_vlan": true + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch1", + "port_group_name": "PG2", + "is_dedicated_vlan": true, + } + ] + } + + | Sample check compliance response + + .. code-block:: json + + { + "status": "NON_COMPLIANT", + "current": [ + { + "is_dedicated_vlan": false, + "switch_name": "Switch1", + "port_group_name": "PG2" + }, + { + "is_dedicated_vlan": false, + "switch_name": "Switch1", + "port_group_name": "PG1" + } + ], + "desired": [ + { + "is_dedicated_vlan": true, + "switch_name": "Switch1", + "port_group_name": "PG2" + }, + { + "is_dedicated_vlan": true, + "switch_name": "Switch1", + "port_group_name": "PG1" + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for ip based stotage port groups. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance for ip based storage traffic isolation") + ip_based_storage_port_groups, errors = self.get(context=context) + + # If errors are seen during get, return "FAILED" status with errors. + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + # Iterate over desired_values and compare with current + # If overrides are present in desired values, then use that data + # Form switch_name[pg_name] desired values and one global desired_values + # If switch_name[pg_name] present in current, then compare that else compare with global desired_values + + current_configs, desired_configs, errors = self._get_non_compliant_configs( + ip_based_storage_port_groups=ip_based_storage_port_groups, desired_values=desired_values + ) + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + if current_configs or desired_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_configs, + consts.DESIRED: desired_configs, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + def remediate(self, context, desired_values) -> Dict: + """Remediate is not implemented as this control requires manual intervention. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired value for the ip based storage port groups. + :type desired_values: Dict + :return: Dict of status (RemediateStatus.SKIPPED) and errors if any + """ + logger.info("Running remediation for ip based storage traffic isolation") + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + logger.info(f"{consts.REMEDIATION_SKIPPED_MESSAGE}") + result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: errors} + return result diff --git a/config_modules_vmware/controllers/vcenter/ldap_identity_source_config.py b/config_modules_vmware/controllers/vcenter/ldap_identity_source_config.py index 82e2797..007716a 100644 --- a/config_modules_vmware/controllers/vcenter/ldap_identity_source_config.py +++ b/config_modules_vmware/controllers/vcenter/ldap_identity_source_config.py @@ -101,7 +101,7 @@ def set(self, context: VcenterContext, desired_values) -> Dict: :param context: Product context instance. :type context: VcenterContext - :param desired_values: Desired value for the certificate authority + :param desired_values: Desired value for ldap accounts :type desired_values: String or list of strings :return: Dict of status (RemediateStatus.SKIPPED) and errors if any :rtype: tuple diff --git a/config_modules_vmware/controllers/vcenter/tls_version_config.py b/config_modules_vmware/controllers/vcenter/tls_version_config.py index 96e5b85..c452841 100644 --- a/config_modules_vmware/controllers/vcenter/tls_version_config.py +++ b/config_modules_vmware/controllers/vcenter/tls_version_config.py @@ -171,7 +171,7 @@ def check_compliance(self, context: VcenterContext, desired_values: Dict) -> Dic if len(errors) == 1 and errors[0] == consts.SKIPPED: return { consts.STATUS: ComplianceStatus.SKIPPED, - consts.MESSAGE: "Control is not applicable on this product version", + consts.ERRORS: [consts.CONTROL_NOT_APPLICABLE], } # If errors are seen during get, return "FAILED" status with errors. return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} diff --git a/config_modules_vmware/controllers/vcenter/vm_migrate_encryption_policy.py b/config_modules_vmware/controllers/vcenter/vm_migrate_encryption_policy.py index 6661ebf..0d4a98a 100644 --- a/config_modules_vmware/controllers/vcenter/vm_migrate_encryption_policy.py +++ b/config_modules_vmware/controllers/vcenter/vm_migrate_encryption_policy.py @@ -109,6 +109,8 @@ def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: """ Set VM migrate Encryption policies for all Virtual machines. + If a VM is "template", mark it as "VM" before remediation, and mark it + back to "template" after remediation. | Recommended value for migrate encryption: "opportunistic" | "required" | Supported values: ["disabled", "opportunistic", "required"]. @@ -185,6 +187,55 @@ def __get_all_vm_migrate_encryption_policy(self, vc_vmomi_client: VcVmomiClient) all_vm_migrate_encryption_configs.append(vm_migrate_encryption_config) return all_vm_migrate_encryption_configs + def _get_data_center(self, vm_ref): + """ + Get datacenter where this VM/VM template located. + + :param vm_ref: vm reference object. + :type vm_ref: vim.VirtualMachine + :return: datacenter and a list of errors if any + :rtype: Tuple + """ + errors = [] + parent = vm_ref.parent + while parent: + if isinstance(parent, vim.Datacenter): + return parent, errors + parent = parent.parent + errors.append(f"Datacenter not found for the VM: {vm_ref.name}") + return parent, errors + + def _get_resource_pool(self, vm_ref): + """ + Get resource pool for converting a VM template to VM for remediation. + + :param vm_ref: vm reference object. + :type vm_ref: vim.VirtualMachine + :return: resource pool and a list of errors if any + :rtype: Tuple + """ + resource_pool = None + datacenter, errors = self._get_data_center(vm_ref) + if errors: + return resource_pool, errors + logger.debug(f"Datacenter : {datacenter.name} found for VM: {vm_ref.name}") + cluster_resource_pool = None + host_resource_pool = None + childs = datacenter.hostFolder.childEntity + for child in childs: + if isinstance(child, vim.ClusterComputeResource): + cluster_resource_pool = child.resourcePool + break + elif isinstance(child, vim.ComputerResource): + host_resource_pool = child.resourcePool + if cluster_resource_pool: + resource_pool = cluster_resource_pool + elif host_resource_pool: + resource_pool = host_resource_pool + else: + errors.append(f"Resource pool for VM: {vm_ref.name} not found") + return resource_pool, errors + def __set_vm_migrate_encryption_policy_for_all_non_compliant_vms( self, vc_vmomi_client: VcVmomiClient, desired_values: Dict ) -> List: @@ -220,13 +271,29 @@ def __set_vm_migrate_encryption_policy_for_all_non_compliant_vms( else desired_global_vm_migrate_encryption_policy ) if current_vm_migrate_encryption_policy != desired_vm_migrate_policy: - config_spec = vim.vm.ConfigSpec() - config_spec.migrateEncryption = desired_vm_migrate_policy logger.info(f"Setting VM migrate policy {desired_vm_migrate_policy} on VM {vm_ref.name}") # continue to remediate next vm if hitting any errors try: + template = vm_ref.config.template + if template: + # for VM template, convert to VM during remediation + resource_pool, errs = self._get_resource_pool(vm_ref) + if errs: + errors.append(errs) + continue + logger.debug(f"Resource pool for convert template to VM: {resource_pool}") + vm_ref.MarkAsVirtualMachine(pool=resource_pool) + logger.debug(f"Converted VM template to VM, template flag: {vm_ref.config.template}") + + config_spec = vim.vm.ConfigSpec() + config_spec.migrateEncryption = desired_vm_migrate_policy task = vm_ref.ReconfigVM_Task(config_spec) vc_vmomi_client.wait_for_task(task=task) + + if template: + # for VM template, convert it back to template after remediation + vm_ref.MarkAsTemplate() + logger.debug(f"Converted VM back to VM template, template flag: {vm_ref.config.template}") except Exception as e: logger.exception(f"An error occurred: {e}") errors.append(f"Failed to remediate VM: {vm_ref.name} - {str(e)}") @@ -347,7 +414,7 @@ def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: result = self.check_compliance(context, desired_values) if result[consts.STATUS] == ComplianceStatus.COMPLIANT: - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: non_compliant_configs = result[consts.CURRENT] else: diff --git a/config_modules_vmware/controllers/vcenter/vsan_hcl_proxy_config.py b/config_modules_vmware/controllers/vcenter/vsan_hcl_proxy_config.py index 93ca661..2d34ac7 100644 --- a/config_modules_vmware/controllers/vcenter/vsan_hcl_proxy_config.py +++ b/config_modules_vmware/controllers/vcenter/vsan_hcl_proxy_config.py @@ -225,7 +225,7 @@ def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: desired_keys=HCL_PROXY_DESIRED_KEYS_FOR_AUDIT, ) if not non_compliant_configs: - return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Control already compliant"]} + return {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} status, errors = self.set(context=context, desired_values=desired_values) diff --git a/config_modules_vmware/controllers/vcenter/vsan_iscsi_targets_mchap_config.py b/config_modules_vmware/controllers/vcenter/vsan_iscsi_targets_mchap_config.py index 5ff3bd0..0aa58a9 100644 --- a/config_modules_vmware/controllers/vcenter/vsan_iscsi_targets_mchap_config.py +++ b/config_modules_vmware/controllers/vcenter/vsan_iscsi_targets_mchap_config.py @@ -89,7 +89,7 @@ def set(self, context: VcenterContext, desired_values: Dict) -> Tuple: :param context: Product context instance. :type context: VcenterContext - :param desired_values: Desired value for the certificate authority + :param desired_values: Desired value for iscsi auth configuration :type desired_values: String or list of strings :return: Dict of status (RemediateStatus.SKIPPED) and errors if any :rtype: Tuple @@ -151,7 +151,7 @@ def check_compliance(self, context, desired_values) -> Dict: } :param context: Product context instance. - :param desired_values: Desired value for the certificate authority. + :param desired_values: Desired value for iscsi auth configuration. :return: Dict of status and current/desired value or errors (for failure). :rtype: dict """ diff --git a/config_modules_vmware/framework/auth/contexts/vc_context.py b/config_modules_vmware/framework/auth/contexts/vc_context.py index dd5406f..e08259a 100644 --- a/config_modules_vmware/framework/auth/contexts/vc_context.py +++ b/config_modules_vmware/framework/auth/contexts/vc_context.py @@ -62,6 +62,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): if self._vc_vmomi_client: self._vc_vmomi_client.disconnect() if self._vc_rest_client: + self._vc_rest_client.delete_vmware_api_session_id() del self._vc_rest_client self._vc_rest_client = None if self._vc_vmomi_sso_client: diff --git a/config_modules_vmware/framework/clients/common/consts.py b/config_modules_vmware/framework/clients/common/consts.py index 37d81c5..3501708 100644 --- a/config_modules_vmware/framework/clients/common/consts.py +++ b/config_modules_vmware/framework/clients/common/consts.py @@ -48,8 +48,14 @@ ERRORS = "errors" GLOBAL = "global" SKIPPED = "SKIPPED" -REMEDIATION_SKIPPED_MESSAGE = "Remediation is not implemented as this control requires manual intervention." COMPLIANCE_CONFIG = "compliance_config" METADATA = "metadata" +UNSUPPORTED_VERSION_MESSAGE_FORMAT = "Version [{}] is not supported for product [{}]" # Timestamp format DEFAULT_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" + +# Message consts for skipped workflows +REMEDIATION_SKIPPED_MESSAGE = "Remediation is not implemented as this control requires manual intervention" +CONTROL_ALREADY_COMPLIANT = "Control already compliant" +CONTROL_NOT_APPLICABLE = "Control is not applicable on this product version" +CONTROL_NOT_AUTOMATED = "Control is not automated for this product version" diff --git a/config_modules_vmware/framework/clients/esxi/esx_cli_client.py b/config_modules_vmware/framework/clients/esxi/esx_cli_client.py index 0257fe8..936116b 100644 --- a/config_modules_vmware/framework/clients/esxi/esx_cli_client.py +++ b/config_modules_vmware/framework/clients/esxi/esx_cli_client.py @@ -35,13 +35,15 @@ def __init__(self, vc_hostname: str, vc_username: str, vc_password: str, vc_ssl_ self._vc_ssl_thumbprint = vc_ssl_thumbprint self._path = shutil.which("esxcli") - def run_esx_cli_cmd(self, hostname: str, command: str) -> Tuple[str, str, int]: + def run_esx_cli_cmd(self, hostname: str, command: str, raise_on_non_zero: bool = True) -> Tuple[str, str, int]: """ Run the esxcli command against the given host. :param hostname: ESXi hostname :type hostname: str :param command: The esx cli command to run :type command: str + :param raise_on_non_zero: When set to true, it raises called processor error for all non-zero exit codes. + :type raise_on_non_zero: bool :return: The output from stdout, stderr and return code :rtype: Tuple[str, str, int] :raise: ValueError if input command is empty or any exception raised by the subprocess module. @@ -64,4 +66,4 @@ def run_esx_cli_cmd(self, hostname: str, command: str) -> Tuple[str, str, int]: # Workaround for esxcli dependent on "HOME" environment variable if not env.get("HOME"): env["HOME"] = "/tmp" # nosec - return utils.run_shell_cmd(command=esx_cli_cmd, env=env) + return utils.run_shell_cmd(command=esx_cli_cmd, env=env, raise_on_non_zero=raise_on_non_zero) diff --git a/config_modules_vmware/framework/clients/vcenter/vc_rest_client.py b/config_modules_vmware/framework/clients/vcenter/vc_rest_client.py index d389f3d..99060fd 100644 --- a/config_modules_vmware/framework/clients/vcenter/vc_rest_client.py +++ b/config_modules_vmware/framework/clients/vcenter/vc_rest_client.py @@ -68,8 +68,6 @@ def __init__(self, hostname, username, password, ssl_thumbprint=None, verify_ssl delete_session_func=delete_session, ) - self.set_vcsa_version() - @staticmethod def _create_vmware_api_session_id(client, hostname, username, password): """ @@ -138,6 +136,8 @@ def delete_vmware_api_session_id(self): :raise :class:`urllib3.exceptions.RequestException` If REST call response reports failed. """ + if not self._rest_client_session: + return self._rest_client_session.delete_session() def vcsa_request(self, url, method, **kwargs): @@ -264,7 +264,7 @@ def validate_cis_task_response(task_id: str, task_response: dict) -> dict: f"Key {vc_consts.CIS_TASK_KEY_VALUE} not found in CIS task response," f"CIS task: {task_id} returned unexpected response" ) - raise err_msg + raise Exception(err_msg) value = task_response["value"] # Missing "status" @@ -273,7 +273,7 @@ def validate_cis_task_response(task_id: str, task_response: dict) -> dict: f"Key {vc_consts.CIS_TASK_KEY_STATUS} not found in CIS task response," f"CIS task: {task_id} returned unexpected response" ) - raise err_msg + raise Exception(err_msg) return value @@ -320,15 +320,6 @@ def wait_for_cis_task_completion(self, task_id, timeout=None, retry_wait_time=No else: raise Exception(f"Task[{task_id}] returned an invalid status {status}") - def set_vcsa_version(self): - """ - Set VCSA version. - - return: None - """ - self._vcsa_version = self.get_vcsa_version() - logger.info(f"Setting the VCSA version to {self._vcsa_version} in the vc rest client") - def get_vcsa_version(self): """ Make REST call to get the version of vCenter, @@ -346,7 +337,8 @@ def get_vcsa_version(self): # Make REST request response = self.get_helper(url) - return response["version"] + self._vcsa_version = response.get("version") + return self._vcsa_version def get_base_url(self): return self._base_url diff --git a/config_modules_vmware/framework/models/output_models/configuration_drift_response.py b/config_modules_vmware/framework/models/output_models/configuration_drift_response.py index 9490eca..21b7ebf 100644 --- a/config_modules_vmware/framework/models/output_models/configuration_drift_response.py +++ b/config_modules_vmware/framework/models/output_models/configuration_drift_response.py @@ -384,7 +384,7 @@ def __init__( Initialize a new ConfigurationDriftResponse instance. """ super().__init__() - self._schema_version = "1.0-DRAFT" + self._schema_version = "1.0" self._id = uuid.uuid4() self._name = name self._timestamp = timestamp diff --git a/config_modules_vmware/framework/models/output_models/get_current_response.py b/config_modules_vmware/framework/models/output_models/get_current_response.py index 21e8783..d731e7c 100644 --- a/config_modules_vmware/framework/models/output_models/get_current_response.py +++ b/config_modules_vmware/framework/models/output_models/get_current_response.py @@ -17,7 +17,6 @@ class GetCurrentConfigurationStatus(str, Enum): SUCCESS = "SUCCESS" FAILED = "FAILED" SKIPPED = "SKIPPED" - ERROR = "ERROR" PARTIAL = "PARTIAL" diff --git a/config_modules_vmware/framework/models/output_models/get_schema_response.py b/config_modules_vmware/framework/models/output_models/get_schema_response.py new file mode 100644 index 0000000..287ed70 --- /dev/null +++ b/config_modules_vmware/framework/models/output_models/get_schema_response.py @@ -0,0 +1,86 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from enum import Enum + +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.output_models.output_response import OutputResponse + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class GetSchemaStatus(str, Enum): + """ + Enum Class to define status of retrieving the schema. + """ + + SUCCESS = "SUCCESS" + FAILED = "FAILED" + SKIPPED = "SKIPPED" + + +class GetSchemaResponse(OutputResponse): + """ + Class for handling get schema response and status. + """ + + def __init__(self): + """ + Initialize a new GetSchemaResponse instance. + """ + super().__init__() + self._status = GetSchemaStatus.SUCCESS + self._result = {} + + @property + def status(self) -> GetSchemaStatus: + """ + Return the get schema status. + + :return: Get schema status. + :rtype: GetSchemaStatus + """ + return self._status + + @property + def result(self): + """ + Return the output result. + + :return: Get schema result. + :rtype: dict + """ + return self._result + + @status.setter + def status(self, status: GetSchemaStatus): + """ + Update the status. + + :param status: Get schema status. + :type status: GetSchemaStatus + """ + self._status = status + + @result.setter + def result(self, result): + """ + Update the result. + + :param result: Get schema result. + :type result: dict + """ + self._result = result + + def to_dict(self): + """ + Get the reformatted get schema response as dict. + + :return: Reformatted get schema response. + :rtype: dict + """ + output_dict = super().to_dict() + output_dict[consts.STATUS] = self._status + if self._result: + output_dict[consts.RESULT] = self._result + return output_dict diff --git a/config_modules_vmware/framework/models/output_models/validate_configuration_response.py b/config_modules_vmware/framework/models/output_models/validate_configuration_response.py new file mode 100644 index 0000000..99e2ac6 --- /dev/null +++ b/config_modules_vmware/framework/models/output_models/validate_configuration_response.py @@ -0,0 +1,87 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from enum import Enum + +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.output_models.output_response import OutputResponse + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class ValidateConfigurationStatus(str, Enum): + """ + Enum Class to define status of validating the desired state. + """ + + VALID = "VALID" + INVALID = "INVALID" + FAILED = "FAILED" + SKIPPED = "SKIPPED" + + +class ValidateConfigurationResponse(OutputResponse): + """ + Class for handling validate response and status. + """ + + def __init__(self): + """ + Initialize a new ValidateConfigurationResponse instance. + """ + super().__init__() + self._status = ValidateConfigurationStatus.VALID + self._result = {} + + @property + def status(self) -> ValidateConfigurationStatus: + """ + Return the validate status. + + :return: Get Validate status. + :rtype: ValidateConfigurationStatus + """ + return self._status + + @property + def result(self): + """ + Return the output result. + + :return: Validate result. + :rtype: dict + """ + return self._result + + @status.setter + def status(self, status: ValidateConfigurationStatus): + """ + Update the status. + + :param status: Get Validate status. + :type status: ValidateConfigurationStatus + """ + self._status = status + + @result.setter + def result(self, result): + """ + Update the result. + + :param result: Validate result. + :type result: dict + """ + self._result = result + + def to_dict(self): + """ + Get the reformatted validate response as dict. + + :return: Reformatted validate response. + :rtype: dict + """ + output_dict = super().to_dict() + output_dict[consts.STATUS] = self._status + if self._result: + output_dict[consts.RESULT] = self._result + return output_dict diff --git a/config_modules_vmware/interfaces/controller_interface.py b/config_modules_vmware/interfaces/controller_interface.py index f182e3e..a10a5f3 100644 --- a/config_modules_vmware/interfaces/controller_interface.py +++ b/config_modules_vmware/interfaces/controller_interface.py @@ -12,9 +12,17 @@ from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus from config_modules_vmware.framework.models.output_models.get_current_response import GetCurrentConfigurationResponse from config_modules_vmware.framework.models.output_models.get_current_response import GetCurrentConfigurationStatus +from config_modules_vmware.framework.models.output_models.get_schema_response import GetSchemaResponse +from config_modules_vmware.framework.models.output_models.get_schema_response import GetSchemaStatus from config_modules_vmware.framework.models.output_models.output_response import OutputResponse from config_modules_vmware.framework.models.output_models.remediate_response import RemediateResponse from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.models.output_models.validate_configuration_response import ( + ValidateConfigurationResponse, +) +from config_modules_vmware.framework.models.output_models.validate_configuration_response import ( + ValidateConfigurationStatus, +) from config_modules_vmware.interfaces.metadata_interface import ControllerMetadataInterface from config_modules_vmware.services.workflows.compliance_operations import ComplianceOperations from config_modules_vmware.services.workflows.configuration_operations import ConfigurationOperations @@ -132,7 +140,7 @@ def get_current_configuration( ) except Exception as e: logging.error(f"Exception in get current configuration {e}") - get_current_output.status = GetCurrentConfigurationStatus.ERROR + get_current_output.status = GetCurrentConfigurationStatus.FAILED get_current_output.message = str(e) return get_current_output.to_dict() @@ -524,6 +532,85 @@ def remediate_with_desired_state( remediation_output.message = str(e) return remediation_output.to_dict() + def get_schema( + self, + controller_type: ControllerMetadata.ControllerType = ControllerMetadata.ControllerType.COMPLIANCE, + ) -> Dict: + """Get schema. + + Sample response for SUCCESS case: + + .. code-block:: json + + { + "result": { + ... + }, + "status": "SUCCESS" + } + + :param controller_type: Type of controller to invoke + :type controller_type: ControllerMetadata.ControllerType + :return: Get Schema output. + :rtype: dict + """ + with HostnameLoggingContext(self._context.hostname): + logger.info("Running get schema.") + get_schema_output = GetSchemaResponse() + try: + self._invoke_workflow( + {}, + get_schema_output, + Operations.GET_SCHEMA, + None, + controller_type, + ) + except Exception as e: + logging.error(f"Exception in get schema {str(e)}") + get_schema_output.status = GetSchemaStatus.FAILED + get_schema_output.message = str(e) + return get_schema_output.to_dict() + + def validate_configuration( + self, + desired_state_spec: Dict = None, + controller_type: ControllerMetadata.ControllerType = ControllerMetadata.ControllerType.COMPLIANCE, + ) -> Dict: + """Validate Configuration. + + Sample response for VALID case: + + .. code-block:: json + + { + "status": "VALID", + "result": {} + } + + :param desired_state_spec: The desired state spec + :type desired_state_spec: dict + :param controller_type: Type of controller to invoke + :type controller_type: ControllerMetadata.ControllerType + :return: Validate output. + :rtype: dict + """ + with HostnameLoggingContext(self._context.hostname): + logger.info("Running validate configuration.") + validate_output = ValidateConfigurationResponse() + try: + self._invoke_workflow( + desired_state_spec, + validate_output, + Operations.VALIDATE, + None, + controller_type, + ) + except Exception as e: + logging.error(f"Exception in validate {str(e)}") + validate_output.status = ValidateConfigurationStatus.FAILED + validate_output.message = str(e) + return validate_output.to_dict() + def _invoke_workflow( self, desired_state_spec: dict, @@ -555,7 +642,11 @@ def _invoke_workflow( metadata_filter=metadata_filter, ) output_response.status = workflow_response.get(consts.STATUS) - if operation == Operations.GET_CURRENT: + if ( + operation == Operations.GET_CURRENT + or operation == Operations.GET_SCHEMA + or operation == Operations.VALIDATE + ): output_response.result = workflow_response.get(consts.RESULT, {}) else: output_response.changes = workflow_response.get(consts.RESULT, {}) diff --git a/config_modules_vmware/schemas/compliance_reference_schema.json b/config_modules_vmware/schemas/compliance_reference_schema.json index 4822180..4020be2 100644 --- a/config_modules_vmware/schemas/compliance_reference_schema.json +++ b/config_modules_vmware/schemas/compliance_reference_schema.json @@ -686,17 +686,8 @@ } }, "minProperties": 1, - "anyOf": [ - { - "required": [ - "__GLOBAL__" - ] - }, - { - "required": [ - "__OVERRIDES__" - ] - } + "required": [ + "__GLOBAL__" ] } }, @@ -752,17 +743,8 @@ } }, "minProperties": 1, - "anyOf": [ - { - "required": [ - "__GLOBAL__" - ] - }, - { - "required": [ - "__OVERRIDES__" - ] - } + "required": [ + "__GLOBAL__" ] } }, @@ -848,35 +830,9 @@ } }, "minProperties": 1, - "anyOf": [ - { - "required": [ - "__GLOBAL__" - ] - }, - { - "required": [ - "__OVERRIDES__" - ] - } + "required": [ + "__GLOBAL__" ], - "if": { - "not": { - "required": [ - "__GLOBAL__" - ] - } - }, - "then": { - "properties": { - "__OVERRIDES__": { - "required": [ - "switch_override_config", - "portgroup_override_config" - ] - } - } - }, "additionalProperties": false } }, @@ -930,17 +886,8 @@ } }, "minProperties": 1, - "anyOf": [ - { - "required": [ - "__GLOBAL__" - ] - }, - { - "required": [ - "__OVERRIDES__" - ] - } + "required": [ + "__GLOBAL__" ] } }, @@ -997,17 +944,8 @@ } }, "minProperties": 1, - "anyOf": [ - { - "required": [ - "__GLOBAL__" - ] - }, - { - "required": [ - "__OVERRIDES__" - ] - } + "required": [ + "__GLOBAL__" ] } }, @@ -1064,17 +1002,8 @@ } }, "minProperties": 1, - "anyOf": [ - { - "required": [ - "__GLOBAL__" - ] - }, - { - "required": [ - "__OVERRIDES__" - ] - } + "required": [ + "__GLOBAL__" ] } }, @@ -1236,6 +1165,73 @@ ], "additionalProperties": false }, + "ip_based_storage_port_group_config": { + "type": "object", + "description": "ip based storage traffic vlan isolation configuration", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "properties": { + "__GLOBAL__": { + "type": "object", + "description": "Global configuration settings applicable for all ip storage dv port groups", + "properties": { + "is_dedicated_vlan": { + "type": "boolean", + "description": "Flag to check if vlan is isolated for ip based storage portgroups" + }, + "allow_mix_traffic_type": { + "type": "boolean", + "description": "Flag to check if allow ip based storage traffic such as iSCSI traffic to go through non-vsan portgroups such as management traffic portgroups" + } + }, + "additionalProperties": false, + "required": [ + "is_dedicated_vlan" + ] + }, + "__OVERRIDES__": { + "type": "array", + "description": "Object to hold overrides for the port groups", + "items": { + "type": "object", + "properties": { + "switch_name": { + "type": "string" + }, + "port_group_name": { + "type": "string" + }, + "is_dedicated_vlan": { + "type": "boolean" + }, + "allow_mix_traffic_type": { + "type": "boolean", + "description": "Flag to check if allow ip based storage traffic such as iSCSI traffic to go through non-vsan portgroups such as management traffic portgroups" + } + }, + "required": [ + "switch_name", + "port_group_name", + "is_dedicated_vlan" + ] + } + } + }, + "minProperties": 1, + "required": [ + "__GLOBAL__" + ], + "additionalProperties": false + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, "dvpg_vmotion_traffic_isolation": { "type": "object", "description": "vMotion port groups vlan isolation configuration", @@ -1298,36 +1294,9 @@ } }, "minProperties": 1, - "anyOf": [ - { - "required": [ - "__GLOBAL__" - ] - }, - { - "required": [ - "__OVERRIDES__" - ] - } + "required": [ + "__GLOBAL__" ], - "if": { - "not": { - "required": [ - "__GLOBAL__" - ] - } - }, - "then": { - "properties": { - "__OVERRIDES__": { - "items": { - "required": [ - "is_dedicated_vlan" - ] - } - } - } - }, "additionalProperties": false } }, @@ -1685,17 +1654,8 @@ } }, "minProperties": 1, - "anyOf": [ - { - "required": [ - "__GLOBAL__" - ] - }, - { - "required": [ - "__OVERRIDES__" - ] - } + "required": [ + "__GLOBAL__" ] } }, @@ -2408,6 +2368,100 @@ ], "additionalProperties": false }, + "snmp_config": { + "type": "object", + "title": "SNMP configuration for ESXi", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "object", + "properties": { + "enable": { + "type": "boolean", + "description": "SNMP enable flag" + }, + "communities": { + "type": "array", + "description": "SNMP communities values", + "items": { + "type": "string" + } + }, + "privacy": { + "type": "string", + "description": "Privacy algorithm to be used by SNMP", + "enum": [ + "none", + "AES128" + ] + }, + "authentication": { + "type": "string", + "description": "Authentication algorithm to be used by SNMP", + "enum": [ + "none", + "SHA1" + ] + }, + "v3_targets": { + "type": "object", + "description": "SNMP v3 targets", + "properties": { + "hostname": { + "type": "string", + "description": "host name or IP address" + }, + "port": { + "type": "number", + "description": "Port number on management system to receive traps or informs" + }, + "userid": { + "type": "string", + "description": "user name" + }, + "security_level": { + "type": "string", + "description": "the level of authentication and privacy", + "enum": [ + "auth", + "priv", + "none" + ] + }, + "message_type": { + "type": "string", + "description": "the type of the messages received by the management system. ", + "enum": [ + "trap", + "inform" + ] + } + }, + "required": [ + "hostname", + "port", + "userid", + "security_level", + "message_type" + ] + } + }, + "required": [ + "enable", + "communities", + "v3_targets" + ], + "additionalProperties": false + } + }, + "required": [ + "value" + ], + "minProperties": 1, + "additionalProperties": false + }, "ssh_fips_140_2_crypt_config": { "type": "object", "title": "SSH daemon FIPS 140-2 validated cryptographic modules configuration for ESXi", @@ -2614,6 +2668,46 @@ "minProperties": 1, "additionalProperties": false }, + "ntp_config": { + "type": "object", + "title": "ESXi host must configure NTP time synchronization.", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "object", + "properties": { + "protocol": { + "type": "string", + "enum": [ + "ntp" + ], + "description": "time synchronization protocol" + }, + "servers": { + "type": "array", + "description": "A valid list of reachable NTP servers", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "protocol", + "servers" + ], + "additionalProperties": false + } + }, + "required": [ + "value" + ], + "minProperties": 1, + "additionalProperties": false + }, "ntp_service_config": { "type": "object", "title": "NTP service running configuration for ESXi", @@ -3205,7 +3299,8 @@ }, "value": { "type": "string", - "description": "SSH 'AllowTcpForwarding' configuration value" + "description": "SSH 'AllowTcpForwarding' configuration value", + "enum": ["yes", "no"] } }, "required": [ @@ -3221,7 +3316,144 @@ }, "value": { "type": "string", - "description": "SSH 'IgnoreRhosts' configuration value" + "description": "SSH 'IgnoreRhosts' configuration value", + "enum": ["yes", "no"] + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "ssh_gateway_ports": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "string", + "description": "SSH 'GatewayPorts' configuration value", + "enum": ["yes", "no"] + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "ssh_host_based_authentication": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "string", + "description": "SSH 'HostBasedAuthentication' configuration value", + "enum": ["yes", "no"] + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "ssh_permit_tunnel": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "string", + "description": "SSH 'PermitTunnel' configuration value", + "enum": ["yes", "no"] + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "ssh_permit_user_environment": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "string", + "description": "SSH 'PermitUserEnvironment' configuration value", + "enum": ["yes", "no"] + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "ssh_permit_empty_passwords": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "string", + "description": "SSH 'PermitEmptyPasswords' configuration value", + "enum": ["yes", "no"] + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "ssh_compression": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "string", + "description": "SSH 'Compression' configuration value", + "enum": ["yes", "no"] + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "ssh_strict_mode": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "string", + "description": "SSH 'StrictModes' configuration value", + "enum": ["yes", "no"] + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "ssh_x11_forwarding": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "string", + "description": "SSH 'X11Forwarding' configuration value", + "enum": ["yes", "no"] } }, "required": [ @@ -3296,6 +3528,76 @@ ], "additionalProperties": false }, + "password_quality_config": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "object", + "description": "esxi password quality control parameters", + "properties": { + "retry": { + "type": "integer", + "minmum": 0 + }, + "min": { + "type": "string", + "pattern": "^(disabled|\\d+)(,(disabled|\\d+)){4}$", + "description": "min=N0,N1,N2,N3,N4, N0 is minimum length of passwords from a single character class, N1 is minimum length of passwords from two character classes, N2 is minimum length for a passphrase, N3 is minimum length for three character classes, N4 is minimum length for four character classes." + }, + "max": { + "type": "integer", + "minmum": 0 + }, + "passphrase": { + "type": "integer", + "minmum": 0 + }, + "similar": { + "type": "string", + "enum": ["permit", "deny"] + } + }, + "required": [] + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "log_location_config": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/definitions/metadata" + }, + "value": { + "type": "object", + "properties": { + "log_location": { + "type": "string", + "description": "Datastore path of the directory to output logs to. Example: [datastoreName]/logdir" + }, + "is_persistent": { + "type": "boolean", + "description": "True for persistent log location or False for non-persistent log location" + } + }, + "required": [ + "log_location", + "is_persistent" + ], + "additionalProperties": false + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, "vim_api_session_timeout": { "type": "object", "properties": { diff --git a/config_modules_vmware/services/apis/controllers/consts.py b/config_modules_vmware/services/apis/controllers/consts.py index b6162f4..2ae6e4a 100644 --- a/config_modules_vmware/services/apis/controllers/consts.py +++ b/config_modules_vmware/services/apis/controllers/consts.py @@ -6,6 +6,8 @@ # vcenter endpoints VC_GET_CONFIGURATION_V1 = CONFIG_MODULES_API + "/vcenter/configuration/v1/get" VC_SCAN_DRIFTS_V1 = CONFIG_MODULES_API + "/vcenter/configuration/v1/scan-drifts" +VC_VALIDATE_CONFIGURATION_V1 = CONFIG_MODULES_API + "/vcenter/configuration/v1/validate" +VC_GET_SCHEMA_V1 = CONFIG_MODULES_API + "/vcenter/schema/v1/get" # misc endpoints ABOUT_ENDPOINT = CONFIG_MODULES + "/about" diff --git a/config_modules_vmware/services/apis/controllers/vcenter.py b/config_modules_vmware/services/apis/controllers/vcenter.py index aeeb8b1..abfb260 100644 --- a/config_modules_vmware/services/apis/controllers/vcenter.py +++ b/config_modules_vmware/services/apis/controllers/vcenter.py @@ -3,7 +3,6 @@ from fastapi import APIRouter from fastapi import Body -from fastapi.encoders import jsonable_encoder from starlette import status from starlette.responses import JSONResponse from typing_extensions import Annotated @@ -12,98 +11,64 @@ from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext from config_modules_vmware.framework.clients.common import consts from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.logging.logging_context import HostnameLoggingContext from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus -from config_modules_vmware.framework.models.output_models.configuration_drift_response import Status from config_modules_vmware.framework.models.output_models.get_current_response import GetCurrentConfigurationStatus +from config_modules_vmware.framework.models.output_models.get_schema_response import GetSchemaStatus +from config_modules_vmware.framework.models.output_models.validate_configuration_response import ( + ValidateConfigurationStatus, +) from config_modules_vmware.framework.utils import utils from config_modules_vmware.interfaces.controller_interface import ControllerInterface from config_modules_vmware.services.apis.controllers.consts import VC_GET_CONFIGURATION_V1 +from config_modules_vmware.services.apis.controllers.consts import VC_GET_SCHEMA_V1 from config_modules_vmware.services.apis.controllers.consts import VC_SCAN_DRIFTS_V1 +from config_modules_vmware.services.apis.controllers.consts import VC_VALIDATE_CONFIGURATION_V1 +from config_modules_vmware.services.apis.models import schema_payload +from config_modules_vmware.services.apis.models import validate_payload from config_modules_vmware.services.apis.models.drift_payload import DriftResponsePayload from config_modules_vmware.services.apis.models.error_model import Error from config_modules_vmware.services.apis.models.error_model import ErrorSource from config_modules_vmware.services.apis.models.error_model import Message from config_modules_vmware.services.apis.models.get_config_payload import GetConfigResponsePayload from config_modules_vmware.services.apis.models.get_config_payload import GetConfigStatus -from config_modules_vmware.services.apis.models.openapi_examples import get_config_vc_request_payload_invalid_example from config_modules_vmware.services.apis.models.openapi_examples import ( - get_config_vc_request_payload_with_template_example, + get_config_vc_8_request_payload_with_template_example, +) +from config_modules_vmware.services.apis.models.openapi_examples import ( + get_config_vc_9_request_payload_with_template_example, ) from config_modules_vmware.services.apis.models.openapi_examples import ( get_config_vc_request_payload_without_template_example, ) +from config_modules_vmware.services.apis.models.openapi_examples import get_schema_vc_200_response_example +from config_modules_vmware.services.apis.models.openapi_examples import get_schema_vc_500_response_example +from config_modules_vmware.services.apis.models.openapi_examples import get_schema_vc_9_request_payload_example from config_modules_vmware.services.apis.models.openapi_examples import scan_drift_vc_200_response_example from config_modules_vmware.services.apis.models.openapi_examples import scan_drift_vc_500_response_example -from config_modules_vmware.services.apis.models.openapi_examples import scan_drift_vc_request_payload_example +from config_modules_vmware.services.apis.models.openapi_examples import scan_drift_vc_8_request_payload_example +from config_modules_vmware.services.apis.models.openapi_examples import scan_drift_vc_9_request_payload_example +from config_modules_vmware.services.apis.models.openapi_examples import vc_request_payload_missing_hostname_body from config_modules_vmware.services.apis.models.request_payload import GetConfigurationRequest +from config_modules_vmware.services.apis.models.request_payload import GetSchema from config_modules_vmware.services.apis.models.request_payload import ScanDriftsRequest +from config_modules_vmware.services.apis.models.request_payload import ValidateConfigurationRequest +from config_modules_vmware.services.apis.models.schema_payload import SchemaResponsePayload from config_modules_vmware.services.apis.models.target_model import AuthType from config_modules_vmware.services.apis.models.target_model import RequestTarget from config_modules_vmware.services.apis.models.target_model import Target +from config_modules_vmware.services.apis.models.validate_payload import ValidateResponsePayload +from config_modules_vmware.services.apis.models.validate_payload import ValidateResult +from config_modules_vmware.services.apis.utils.exception_handlers import create_vcenter_drift_exception +from config_modules_vmware.services.apis.utils.exception_handlers import create_vcenter_get_config_exception +from config_modules_vmware.services.apis.utils.exception_handlers import create_vcenter_schema_exception +from config_modules_vmware.services.apis.utils.exception_handlers import create_vcenter_validate_exception logger = LoggerAdapter(logging.getLogger(__name__)) vcenter_router = APIRouter() -def _create_vcenter_get_config_exception(hostname: str, error_message: str) -> GetConfigResponsePayload: - """ - Create a vcenter get config exception. - :param hostname: the hostname of the vcenter. - :type str: str - :param error_message: the error message. - :type str: str - :return: get config response payload with error information. - :rtype: GetConfigResponsePayload - """ - current_time = utils.get_current_time() - return jsonable_encoder( - GetConfigResponsePayload( - description="Exception fetching configuration.", - status=GetCurrentConfigurationStatus.FAILED, - timestamp=current_time, - target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=hostname), - errors=[ - Error( - timestamp=current_time, - source=ErrorSource(endpoint=VC_GET_CONFIGURATION_V1), - error=Message(message=error_message), - ) - ], - ), - exclude_none=True, - ) - - -def _create_vcenter_drift_exception(hostname: str, error_message: str) -> DriftResponsePayload: - """ - Create a vcenter drift exception. - :param hostname: the hostname of the vcenter. - :type str: str - :param error_message: the error message. - :type str: str - :return: drift response payload with error information. - :rtype: DriftResponsePayload - """ - current_time = utils.get_current_time() - return jsonable_encoder( - DriftResponsePayload( - description="Exception fetching drifts.", - status=Status.FAILED, - timestamp=current_time, - target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=hostname), - errors=[ - Error( - timestamp=current_time, - source=ErrorSource(endpoint=VC_SCAN_DRIFTS_V1), - error=Message(message=error_message), - ) - ], - ), - exclude_none=True, - ) - - def _get_vcenter_context(target: RequestTarget) -> VcenterContext: """ Creates a VCenterContext object from the incoming request payload. @@ -126,6 +91,68 @@ def _get_vcenter_context(target: RequestTarget) -> VcenterContext: raise Exception(f"SSO auth missing for vcenter {target.hostname}") +@vcenter_router.post( + path=VC_GET_SCHEMA_V1, + response_model=SchemaResponsePayload, + responses={200: get_schema_vc_200_response_example, 500: get_schema_vc_500_response_example}, +) +def get_schema( + get_schema_request: Annotated[ + GetSchema, Body(openapi_examples={"default": get_schema_vc_9_request_payload_example}) + ] +) -> JSONResponse: + """Endpoint to get schema for a given product.""" + hostname = get_schema_request.target.hostname + with HostnameLoggingContext(hostname): + logger.info("Get schema API called.") + + try: + vcenter_context = _get_vcenter_context(get_schema_request.target) + except Exception as e: + logger.error(e) + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=create_vcenter_schema_exception(hostname, str(e)).to_dict(), + ) + + try: + controller_interface = ControllerInterface(vcenter_context) + schema_response = controller_interface.get_schema( + controller_type=ControllerMetadata.ControllerType.CONFIGURATION + ) + + if schema_response is None: + raise TypeError("Schema response is None.") + + # missing status in schema response + workflow_status = schema_response.get(consts.STATUS) + if workflow_status is None: + raise KeyError("Missing status in schema response.") + + if workflow_status is not GetSchemaStatus.SUCCESS: + raise Exception(schema_response.get(consts.MESSAGE)) + + # Extract schema from the interface response + json_schema = schema_response.get(consts.RESULT, {}) + if not json_schema or "schema" not in json_schema: + raise KeyError("Missing schema in schema response.") + + schema_response = SchemaResponsePayload( + description="Product schema", + status=schema_payload.Status.SUCCESS, + timestamp=utils.get_current_time(), + target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=vcenter_context.hostname), + result=schema_payload.Result(json_schema=json_schema.get("schema")), + ) + return JSONResponse(status_code=status.HTTP_200_OK, content=schema_response.to_dict()) + except Exception as e: + logger.error(e) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=create_vcenter_schema_exception(hostname, str(e)).to_dict(), + ) + + @vcenter_router.post( path=VC_GET_CONFIGURATION_V1, response_description="The filtered configuration spec.", @@ -137,74 +164,142 @@ def get_configuration( Body( openapi_examples={ "default": get_config_vc_request_payload_without_template_example, - "template": get_config_vc_request_payload_with_template_example, - "invalid": get_config_vc_request_payload_invalid_example, + "template_vc_8x": get_config_vc_8_request_payload_with_template_example, + "template_vc_9+": get_config_vc_9_request_payload_with_template_example, + "invalid": vc_request_payload_missing_hostname_body, } ), ] ) -> JSONResponse: """Endpoint to get vcenter configuration.""" hostname = get_configuration_request.target.hostname - logger.info(f"Getting vcenter configuration for {hostname}") + with HostnameLoggingContext(hostname): + logger.info("Getting vcenter configuration.") + + try: + vcenter_context = _get_vcenter_context(get_configuration_request.target) + except Exception as e: + logger.error(e) + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=create_vcenter_get_config_exception(hostname, str(e)).to_dict(), + ) + + try: + controller_interface = ControllerInterface(vcenter_context) + configuration = controller_interface.get_current_configuration( + controller_type=ControllerMetadata.ControllerType.CONFIGURATION, + template=get_configuration_request.template, + ) + if configuration is None: + raise TypeError("Retrieved configuration is None.") + # missing status in configuration response + workflow_status = configuration.get(consts.STATUS) + if workflow_status is None: + raise KeyError("Missing status in configuration response.") - try: - vcenter_context = _get_vcenter_context(get_configuration_request.target) - except Exception as e: - logger.error(e) - return JSONResponse( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - content=_create_vcenter_get_config_exception(hostname, str(e)), - ) + # success, skipped, partial, failed in get configuration workflow + current_time = utils.get_current_time() + return_response = GetConfigResponsePayload( + status=GetConfigStatus.SUCCESS, + timestamp=current_time, + target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=hostname), + ) + status_code = status.HTTP_200_OK + if workflow_status == GetCurrentConfigurationStatus.PARTIAL: + return_response.status = GetConfigStatus.PARTIAL + elif ( + workflow_status == GetCurrentConfigurationStatus.FAILED + or workflow_status == GetCurrentConfigurationStatus.SKIPPED + ): + return_response.status = GetConfigStatus.FAILED + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + if configuration.get(consts.MESSAGE) is not None: + return_response.errors = [ + Error( + timestamp=current_time, + source=ErrorSource(endpoint=VC_GET_CONFIGURATION_V1), + error=Message(message=configuration.get(consts.MESSAGE)), + ) + ] + if configuration.get(consts.RESULT) is not None: + return_response.result = configuration.get(consts.RESULT) + return JSONResponse(status_code=status_code, content=return_response.to_dict()) + except Exception as e: + logger.error(e) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=create_vcenter_get_config_exception(hostname, str(e)).to_dict(), + ) - try: - controller_interface = ControllerInterface(vcenter_context) - configuration = controller_interface.get_current_configuration( - controller_type=ControllerMetadata.ControllerType.CONFIGURATION, template=get_configuration_request.template - ) - if configuration is None: - raise TypeError("Retrieved configuration is None.") - # missing status in configuration response - workflow_status = configuration.get(consts.STATUS) - if workflow_status is None: - raise KeyError("Missing status in configuration response.") - # error in get configuration workflow - if workflow_status == GetCurrentConfigurationStatus.ERROR: - raise Exception(configuration.get(consts.MESSAGE)) +@vcenter_router.post( + path=VC_VALIDATE_CONFIGURATION_V1, + response_description="The configuration spec.", + response_model=ValidateResponsePayload, +) +def validate_configuration( + validate_configuration_request: Annotated[ + ValidateConfigurationRequest, + Body( + openapi_examples={ + "default": scan_drift_vc_9_request_payload_example, + "invalid": vc_request_payload_missing_hostname_body, + } + ), + ] +) -> JSONResponse: + """Endpoint to validate vcenter configuration.""" + hostname = validate_configuration_request.target.hostname + with HostnameLoggingContext(hostname): + logger.info("Validating vcenter configuration.") - # success, skipped, partial, failed in get configuration workflow - current_time = utils.get_current_time() - return_response = GetConfigResponsePayload( - status=GetConfigStatus.SUCCESS, - timestamp=current_time, - target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=hostname), - ) - status_code = status.HTTP_200_OK - if workflow_status == GetCurrentConfigurationStatus.PARTIAL: - return_response.status = GetConfigStatus.PARTIAL - elif ( - workflow_status == GetCurrentConfigurationStatus.FAILED - or workflow_status == GetCurrentConfigurationStatus.SKIPPED - ): - return_response.status = GetConfigStatus.FAILED - status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - if configuration.get(consts.MESSAGE) is not None: - return_response.errors = [ - Error( - timestamp=current_time, - source=ErrorSource(endpoint=VC_GET_CONFIGURATION_V1), - error=Message(message=configuration.get(consts.MESSAGE)), - ) - ] - if configuration.get(consts.RESULT) is not None: - return_response.result = configuration.get(consts.RESULT) - return JSONResponse(status_code=status_code, content=jsonable_encoder(return_response, exclude_none=True)) - except Exception as e: - logger.error(e) - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=_create_vcenter_get_config_exception(hostname, str(e)), - ) + try: + vcenter_context = _get_vcenter_context(validate_configuration_request.target) + except Exception as e: + logger.error(e) + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=create_vcenter_validate_exception(hostname, str(e)).to_dict(), + ) + + try: + controller_interface = ControllerInterface(vcenter_context) + validate_response = controller_interface.validate_configuration( + desired_state_spec=validate_configuration_request.input_spec, + controller_type=ControllerMetadata.ControllerType.CONFIGURATION, + ) + if validate_response is None: + raise TypeError("Validate response is None.") + # missing status in validate response + workflow_status = validate_response.get(consts.STATUS) + if workflow_status is None: + raise KeyError("Missing status in configuration response.") + + # error in validate configuration workflow + if ( + workflow_status == ValidateConfigurationStatus.FAILED + or workflow_status == ValidateConfigurationStatus.SKIPPED + ): + raise Exception(validate_response.get(consts.MESSAGE)) + + current_time = utils.get_current_time() + return_response = ValidateResponsePayload( + status=validate_payload.Status.VALID + if workflow_status == ValidateConfigurationStatus.VALID + else validate_payload.Status.INVALID, + timestamp=current_time, + target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=hostname), + ) + if validate_response.get(consts.RESULT) is not None: + return_response.result = ValidateResult(**validate_response.get(consts.RESULT)) + return JSONResponse(status_code=status.HTTP_200_OK, content=return_response.to_dict()) + except Exception as e: + logger.error(e) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=create_vcenter_validate_exception(hostname, str(e)).to_dict(), + ) @vcenter_router.post( @@ -214,55 +309,63 @@ def get_configuration( ) def scan_drifts( scan_drifts_request: Annotated[ - ScanDriftsRequest, Body(openapi_examples={"default": scan_drift_vc_request_payload_example}) + ScanDriftsRequest, + Body( + openapi_examples={ + "spec_vc_8x": scan_drift_vc_8_request_payload_example, + "spec_vc_9+": scan_drift_vc_9_request_payload_example, + } + ), ] ) -> JSONResponse: """Endpoint to get drifts against an input spec.""" - logger.info(f"Scan drifts initiated for {scan_drifts_request.target.hostname}") + hostname = scan_drifts_request.target.hostname + with HostnameLoggingContext(hostname): + logger.info("Scan drifts initiated.") - try: - vcenter_context = _get_vcenter_context(scan_drifts_request.target) - except Exception as e: - logger.error(e) - return JSONResponse( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - content=_create_vcenter_drift_exception(scan_drifts_request.target.hostname, str(e)), - ) - - try: - controller_interface = ControllerInterface(vcenter_context) - drift_response = controller_interface.check_compliance( - controller_type=ControllerMetadata.ControllerType.CONFIGURATION, - desired_state_spec=scan_drifts_request.input_spec, - ) - if drift_response is None: - raise TypeError("Drift response is None.") - # missing status in drift response - workflow_status = drift_response.get(consts.STATUS) - if workflow_status is None: - raise KeyError("Missing status in drift response.") - # error in compliance workflow - if workflow_status == ComplianceStatus.ERROR: - raise Exception(drift_response.get(consts.MESSAGE)) - - # mapping configuration drift response to API drift response spec. Currently, they are the same. try: - if consts.CHANGES in drift_response: - drift_response = drift_response.get(consts.CHANGES) - elif consts.MESSAGE in drift_response: - drift_response = drift_response.get(consts.MESSAGE) - drift_response = jsonable_encoder(DriftResponsePayload(**drift_response), exclude_none=True) + vcenter_context = _get_vcenter_context(scan_drifts_request.target) except Exception as e: - logger.error(f"Invalid field in drift response {drift_response}") - raise e + logger.error(e) + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=create_vcenter_drift_exception(hostname, str(e)).to_dict(), + ) + + try: + controller_interface = ControllerInterface(vcenter_context) + drift_response = controller_interface.check_compliance( + controller_type=ControllerMetadata.ControllerType.CONFIGURATION, + desired_state_spec=scan_drifts_request.input_spec, + ) + if drift_response is None: + raise TypeError("Drift response is None.") + # missing status in drift response + workflow_status = drift_response.get(consts.STATUS) + if workflow_status is None: + raise KeyError("Missing status in drift response.") + # error in compliance workflow + if workflow_status == ComplianceStatus.ERROR or workflow_status == ComplianceStatus.SKIPPED: + raise Exception(drift_response.get(consts.MESSAGE)) - if workflow_status == ComplianceStatus.FAILED: - return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=drift_response) - else: - return JSONResponse(status_code=status.HTTP_200_OK, content=drift_response) - except Exception as e: - logger.error(e) - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=_create_vcenter_drift_exception(scan_drifts_request.target.hostname, str(e)), - ) + # mapping configuration drift response to API drift response spec. Currently, they are the same. + try: + if consts.CHANGES in drift_response: + drift_response = drift_response.get(consts.CHANGES) + elif consts.MESSAGE in drift_response: + drift_response = drift_response.get(consts.MESSAGE) + drift_response = DriftResponsePayload(**drift_response).to_dict() + except Exception as e: + logger.error(f"Invalid field in drift response {drift_response}") + raise e + + if workflow_status == ComplianceStatus.FAILED: + return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=drift_response) + else: + return JSONResponse(status_code=status.HTTP_200_OK, content=drift_response) + except Exception as e: + logger.error(e) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=create_vcenter_drift_exception(hostname, str(e)).to_dict(), + ) diff --git a/config_modules_vmware/services/apis/models/base_response_model.py b/config_modules_vmware/services/apis/models/base_response_model.py new file mode 100644 index 0000000..6a9fe06 --- /dev/null +++ b/config_modules_vmware/services/apis/models/base_response_model.py @@ -0,0 +1,39 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import enum +from typing import List + +from pydantic import BaseModel + + +class BaseResponseModel(BaseModel): + """Class to represent a base response model.""" + + def to_dict(self, strip_null=True) -> dict: + """ + Returns the model properties as a dict + :param strip_null: remove attributes with null values + :rtype: dict + """ + result_dict = {} + for key, value in vars(self).items(): + if strip_null and value is None: + continue + if isinstance(value, List): + if len(value) > 0: + result_dict[key] = list(map(lambda x: x.to_dict(strip_null) if hasattr(x, "to_dict") else x, value)) + elif isinstance(value, enum.Enum): + result_dict[key] = value.value + elif hasattr(value, "to_dict"): + nested_dict_value = value.to_dict(strip_null) + if nested_dict_value: + result_dict[key] = nested_dict_value + elif isinstance(value, dict): + result_dict[key] = dict( + map( + lambda item: (item[0], item[1].to_dict(strip_null)) if hasattr(item[1], "to_dict") else item, + value.items(), + ) + ) + else: + result_dict[key] = str(value) + return result_dict diff --git a/config_modules_vmware/services/apis/models/drift_payload.py b/config_modules_vmware/services/apis/models/drift_payload.py index 8aef3ec..0dc2f37 100644 --- a/config_modules_vmware/services/apis/models/drift_payload.py +++ b/config_modules_vmware/services/apis/models/drift_payload.py @@ -1,18 +1,18 @@ # Copyright 2024 Broadcom. All Rights Reserved. from typing import Any -from pydantic import BaseModel from pydantic import Field from typing_extensions import List from config_modules_vmware.framework.models.output_models.configuration_drift_response import Status +from config_modules_vmware.services.apis.models.base_response_model import BaseResponseModel from config_modules_vmware.services.apis.models.error_model import Error from config_modules_vmware.services.apis.models.target_model import Target -VERSION = "1.0-DRAFT" +VERSION = "1.0" -class Config(BaseModel): +class Config(BaseResponseModel): """Class to represent a drifted configuration""" key: str = Field(description="The configuration property.") @@ -38,7 +38,7 @@ class ConfigModification(Config): desired_value: Any = Field(description="The desired value from the spec.") -class Result(BaseModel): +class Result(BaseResponseModel): """Class to represent the results of the scan drift API call.""" additions: List[ConfigAddition] = Field( @@ -52,7 +52,7 @@ class Result(BaseModel): ) -class DriftResponsePayload(BaseModel): +class DriftResponsePayload(BaseResponseModel): """Class to represent the response format of a scan drift API call.""" schema_version: str = Field(default=VERSION, description="The drift response spec.") diff --git a/config_modules_vmware/services/apis/models/error_model.py b/config_modules_vmware/services/apis/models/error_model.py index 5ce9c99..2bc62aa 100644 --- a/config_modules_vmware/services/apis/models/error_model.py +++ b/config_modules_vmware/services/apis/models/error_model.py @@ -1,11 +1,12 @@ # Copyright 2024 Broadcom. All Rights Reserved. import socket -from pydantic import BaseModel from pydantic import Field +from config_modules_vmware.services.apis.models.base_response_model import BaseResponseModel -class ErrorSource(BaseModel): + +class ErrorSource(BaseResponseModel): """Class to represent the error source.""" server: str = Field(default=socket.gethostbyname(""), description="The server hostname.") @@ -13,7 +14,7 @@ class ErrorSource(BaseModel): endpoint: str = Field(default=None, description="The endpoint for which error occurred.") -class Message(BaseModel): +class Message(BaseResponseModel): """Class to represent the message object.""" id: str = Field( @@ -23,7 +24,7 @@ class Message(BaseModel): message: str = Field(description="The message.") -class Error(BaseModel): +class Error(BaseResponseModel): """Class to represent the errors caught during the workflow.""" timestamp: str = Field(description="Timestamp of error occurrence in ISO format (YYYY-MM-DDTHH:MM:SS.mmm)") diff --git a/config_modules_vmware/services/apis/models/get_config_payload.py b/config_modules_vmware/services/apis/models/get_config_payload.py index 3da04f6..8aaa46f 100644 --- a/config_modules_vmware/services/apis/models/get_config_payload.py +++ b/config_modules_vmware/services/apis/models/get_config_payload.py @@ -1,14 +1,14 @@ # Copyright 2024 Broadcom. All Rights Reserved. from enum import Enum -from pydantic import BaseModel from pydantic import Field from typing_extensions import List +from config_modules_vmware.services.apis.models.base_response_model import BaseResponseModel from config_modules_vmware.services.apis.models.error_model import Error from config_modules_vmware.services.apis.models.target_model import Target -VERSION = "1.0-DRAFT" +VERSION = "1.0" class GetConfigStatus(str, Enum): @@ -21,7 +21,7 @@ class GetConfigStatus(str, Enum): FAILED = "FAILED" -class GetConfigResponsePayload(BaseModel): +class GetConfigResponsePayload(BaseResponseModel): """Class to represent the response format of a scan drift API call.""" schema_version: str = Field(default=VERSION, description="The get configuration spec.") diff --git a/config_modules_vmware/services/apis/models/openapi_examples.py b/config_modules_vmware/services/apis/models/openapi_examples.py index 2246e68..1b63ab0 100644 --- a/config_modules_vmware/services/apis/models/openapi_examples.py +++ b/config_modules_vmware/services/apis/models/openapi_examples.py @@ -17,9 +17,9 @@ } }, } -get_config_vc_request_payload_with_template_example = { - "summary": "With template", - "description": "This is to retrieve only vcenter configuration based on the template.", +get_config_vc_8_request_payload_with_template_example = { + "summary": "With template for VC 8.X", + "description": "This is to retrieve only vcenter configuration based on the template using VC Tech preview APIs.", "value": { "target": { "hostname": "vcenter-1.vsphere.local", @@ -38,7 +38,27 @@ }, }, } -get_config_vc_request_payload_invalid_example = { +get_config_vc_9_request_payload_with_template_example = { + "summary": "With template for VC 9+", + "description": "This is to retrieve only vcenter configuration based on the template using VC Public APIs.", + "value": { + "target": { + "hostname": "vcenter-1.vsphere.local", + "auth": [ + { + "username": "sso_username", + "password": "sso_password", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + }, + "template": { + "config": {"vcenter": {"inventory": {}}}, + }, + }, +} +vc_request_payload_missing_hostname_body = { "summary": "Missing required parameters", "description": "Same request body with missing hostname.", "value": { @@ -54,8 +74,8 @@ } }, } -scan_drift_vc_request_payload_example = { - "summary": "default", +scan_drift_vc_8_request_payload_example = { + "summary": "With spec for VC 8.X", "description": "Sample request body", "value": { "target": { @@ -84,12 +104,100 @@ }, }, } +scan_drift_vc_9_request_payload_example = { + "summary": "With spec for VC 9+", + "description": "Sample request body", + "value": { + "target": { + "hostname": "vcenter-1.vsphere.local", + "auth": [ + { + "username": "sso_username", + "password": "sso_password", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + }, + "input_spec": { + "config": { + "vcenter": { + "inventory": { + "folders": [{"path": "/vcqaDC/testNetworkFolder", "permissions": [], "type": "HOST"}], + "distributed_virtual_portgroups": [], + }, + }, + }, + }, + }, +} +get_schema_vc_9_request_payload_example = { + "summary": "default", + "description": "Sample request body", + "value": { + "target": { + "hostname": "vcenter-1.vsphere.local", + "auth": [ + { + "username": "sso_username", + "password": "sso_password", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + } + }, +} +get_schema_vc_200_response_example = { + "description": "Successful get schema response.", + "content": { + "application/json": { + "example": { + "schema_version": "1.0", + "id": "null", + "name": "Get Schema", + "timestamp": "2024-08-05T22:46:05.210604", + "description": "Schema", + "status": "SUCCESS", + "result": {"json_schema": {"sample": "test"}}, + "errors": "null", + "target": {"hostname": "10.168.160.16", "type": "vcenter"}, + } + } + }, +} +get_schema_vc_500_response_example = { + "description": "Internal error response.", + "content": { + "application/json": { + "example": { + "schema_version": "1.0", + "name": "Get Schema", + "timestamp": "2024-08-06T20:34:06.343944", + "description": "Exception fetching schema.", + "status": "FAILED", + "errors": [ + { + "timestamp": "2024-08-06T20:34:06.343944", + "source": { + "server": "192.168.1.1", + "type": "ConfigModules", + "endpoint": "/config-modules/api/vcenter/schema/v1/get", + }, + "error": {"message": "unable to reach vc"}, + } + ], + "target": {"hostname": "vcenter-1.vsphere.local", "type": "vcenter"}, + } + } + }, +} scan_drift_vc_200_response_example = { "description": "Successful drift workflow response.", "content": { "application/json": { "example": { - "schema_version": "1.0-DRAFT", + "schema_version": "1.0", "id": "2bcaa939-e6c2-4347-808f-ad90debc20ae", "name": "config_modules_vmware.controllers.vcenter.vc_profile", "timestamp": "2024-03-28T23:03:19.472Z", @@ -129,7 +237,7 @@ "content": { "application/json": { "example": { - "schema_version": "1.0-DRAFT", + "schema_version": "1.0", "id": "2bcaa939-e6c2-4347-808f-ad90debc20ae", "name": "config_modules_vmware.controllers.vcenter.vc_profile", "timestamp": "2024-03-28T23:03:19.472Z", diff --git a/config_modules_vmware/services/apis/models/request_payload.py b/config_modules_vmware/services/apis/models/request_payload.py index b5be26a..247467d 100644 --- a/config_modules_vmware/services/apis/models/request_payload.py +++ b/config_modules_vmware/services/apis/models/request_payload.py @@ -5,6 +5,12 @@ from config_modules_vmware.services.apis.models.target_model import RequestTarget +class GetSchema(BaseModel): + """Class to represent the request format of a get schema API call.""" + + target: RequestTarget = Field(description="The product target information.") + + class GetConfigurationRequest(BaseModel): """Class to represent the request format of a get configuration API call.""" @@ -17,3 +23,10 @@ class ScanDriftsRequest(BaseModel): target: RequestTarget = Field(description="The product target information.") input_spec: dict = Field(description="Desired state input spec, based on the product schema.") + + +class ValidateConfigurationRequest(BaseModel): + """Class to represent the request format of a validate configuration API call.""" + + target: RequestTarget = Field(description="The product target information.") + input_spec: dict = Field(description="Desired state input spec, based on the product schema.") diff --git a/config_modules_vmware/services/apis/models/schema_payload.py b/config_modules_vmware/services/apis/models/schema_payload.py new file mode 100644 index 0000000..df6242f --- /dev/null +++ b/config_modules_vmware/services/apis/models/schema_payload.py @@ -0,0 +1,40 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from enum import Enum + +from pydantic import Field +from typing_extensions import List + +from config_modules_vmware.services.apis.models.base_response_model import BaseResponseModel +from config_modules_vmware.services.apis.models.error_model import Error +from config_modules_vmware.services.apis.models.target_model import Target + +VERSION = "1.0" + + +class Status(str, Enum): + """ + Status enum + """ + + SUCCESS = "SUCCESS" + FAILED = "FAILED" + + +class Result(BaseResponseModel): + """Class to represent the results of the schema API call.""" + + json_schema: dict = Field(description="Product schema in json_schema format") + + +class SchemaResponsePayload(BaseResponseModel): + """Class to represent the response format of a schema API call.""" + + schema_version: str = Field(default=VERSION, description="The schema response spec.") + id: str = Field(default=None, description="The uuid if applicable.") + name: str = Field(default="Get Schema", description="The name of the function.") + timestamp: str = Field(description="The timestamp of drift calculation in ISO format (YYYY-MM-DDTHH:MM:SS.mmm)") + description: str = Field(default=None, description="The description of the function.") + status: Status = Field(description="The status of the function.") + result: Result = Field(default=None, description="The drifts.") + errors: List[Error] = Field(default=None, description="Errors during schema retrieval.") + target: Target = Field(description="The targeted product.") diff --git a/config_modules_vmware/services/apis/models/target_model.py b/config_modules_vmware/services/apis/models/target_model.py index 7207962..59e9f78 100644 --- a/config_modules_vmware/services/apis/models/target_model.py +++ b/config_modules_vmware/services/apis/models/target_model.py @@ -6,6 +6,7 @@ from typing_extensions import List from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.services.apis.models.base_response_model import BaseResponseModel class AuthType(str, Enum): @@ -27,7 +28,7 @@ class Auth(BaseModel): type: AuthType = Field(default=None, description="Type of authentication.") -class Target(BaseModel): +class Target(BaseResponseModel): """Class to represent a target.""" hostname: str = Field(description="The hostname of the product.") diff --git a/config_modules_vmware/services/apis/models/validate_payload.py b/config_modules_vmware/services/apis/models/validate_payload.py new file mode 100644 index 0000000..f2de4b0 --- /dev/null +++ b/config_modules_vmware/services/apis/models/validate_payload.py @@ -0,0 +1,43 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from enum import Enum + +from pydantic import Field +from typing_extensions import List + +from config_modules_vmware.services.apis.models.base_response_model import BaseResponseModel +from config_modules_vmware.services.apis.models.error_model import Error +from config_modules_vmware.services.apis.models.target_model import Target + +VERSION = "1.0" + + +class Status(str, Enum): + """ + Status enum + """ + + VALID = "VALID" + INVALID = "INVALID" + FAILED = "FAILED" + + +class ValidateResult(BaseResponseModel): + """Class to represent the result of the validate API""" + + warnings: List[dict] = Field(default=None, description="List of warnings") + errors: List[dict] = Field(default=None, description="List of errors") + info: List[dict] = Field(default=None, description="List of info") + + +class ValidateResponsePayload(BaseResponseModel): + """Class to represent the response format of the validate response API call.""" + + schema_version: str = Field(default=VERSION, description="The validate response spec.") + id: str = Field(default=None, description="The uuid if applicable.") + name: str = Field(default="Validate Configuration", description="The name of the function.") + timestamp: str = Field(description="The timestamp of validate operation in ISO format (YYYY-MM-DDTHH:MM:SS.mmm)") + description: str = Field(default=None, description="The description of the function.") + status: Status = Field(description="The status of the function.") + result: ValidateResult = Field(default=None, description="The validate response.") + errors: List[Error] = Field(default=None, description="Errors during validation.") + target: Target = Field(description="The targeted product.") diff --git a/config_modules_vmware/services/apis/utils/__init__.py b/config_modules_vmware/services/apis/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_modules_vmware/services/apis/utils/exception_handlers.py b/config_modules_vmware/services/apis/utils/exception_handlers.py new file mode 100644 index 0000000..b016d90 --- /dev/null +++ b/config_modules_vmware/services/apis/utils/exception_handlers.py @@ -0,0 +1,122 @@ +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.models.output_models.configuration_drift_response import Status +from config_modules_vmware.framework.models.output_models.get_current_response import GetCurrentConfigurationStatus +from config_modules_vmware.framework.utils import utils +from config_modules_vmware.services.apis.controllers.consts import VC_GET_CONFIGURATION_V1 +from config_modules_vmware.services.apis.controllers.consts import VC_GET_SCHEMA_V1 +from config_modules_vmware.services.apis.controllers.consts import VC_SCAN_DRIFTS_V1 +from config_modules_vmware.services.apis.controllers.consts import VC_VALIDATE_CONFIGURATION_V1 +from config_modules_vmware.services.apis.models import schema_payload +from config_modules_vmware.services.apis.models import validate_payload +from config_modules_vmware.services.apis.models.drift_payload import DriftResponsePayload +from config_modules_vmware.services.apis.models.error_model import Error +from config_modules_vmware.services.apis.models.error_model import ErrorSource +from config_modules_vmware.services.apis.models.error_model import Message +from config_modules_vmware.services.apis.models.get_config_payload import GetConfigResponsePayload +from config_modules_vmware.services.apis.models.schema_payload import SchemaResponsePayload +from config_modules_vmware.services.apis.models.target_model import Target +from config_modules_vmware.services.apis.models.validate_payload import ValidateResponsePayload + + +def create_vcenter_get_config_exception(hostname: str, error_message: str) -> GetConfigResponsePayload: + """ + Create a vcenter get config exception. + :param hostname: the hostname of the vcenter. + :type str: str + :param error_message: the error message. + :type str: str + :return: get config response payload with error information. + :rtype: GetConfigResponsePayload + """ + current_time = utils.get_current_time() + return GetConfigResponsePayload( + description="Exception fetching configuration.", + status=GetCurrentConfigurationStatus.FAILED, + timestamp=current_time, + target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=hostname) if hostname else None, + errors=[ + Error( + timestamp=current_time, + source=ErrorSource(endpoint=VC_GET_CONFIGURATION_V1), + error=Message(message=error_message), + ) + ], + ) + + +def create_vcenter_drift_exception(hostname: str, error_message: str) -> DriftResponsePayload: + """ + Create a vcenter drift exception. + :param hostname: the hostname of the vcenter. + :type str: str + :param error_message: the error message. + :type str: str + :return: drift response payload with error information. + :rtype: DriftResponsePayload + """ + current_time = utils.get_current_time() + return DriftResponsePayload( + description="Exception fetching drifts.", + status=Status.FAILED, + timestamp=current_time, + target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=hostname) if hostname else None, + errors=[ + Error( + timestamp=current_time, + source=ErrorSource(endpoint=VC_SCAN_DRIFTS_V1), + error=Message(message=error_message), + ) + ], + ) + + +def create_vcenter_schema_exception(hostname: str, error_message: str) -> SchemaResponsePayload: + """ + Create a vcenter schema exception. + :param hostname: the hostname of the vcenter. + :type str: str + :param error_message: the error message. + :type str: str + :return: schema response payload with error information. + :rtype: DriftResponsePayload + """ + current_time = utils.get_current_time() + return SchemaResponsePayload( + description="Exception fetching schema.", + status=schema_payload.Status.FAILED, + timestamp=current_time, + target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=hostname) if hostname else None, + errors=[ + Error( + timestamp=current_time, + source=ErrorSource(endpoint=VC_GET_SCHEMA_V1), + error=Message(message=error_message), + ) + ], + ) + + +def create_vcenter_validate_exception(hostname: str, error_message: str) -> ValidateResponsePayload: + """ + Create a vcenter validate exception. + :param hostname: the hostname of the vcenter. + :type str: str + :param error_message: the error message. + :type str: str + :return: validate response payload with error information. + :rtype: ValidateResponsePayload + """ + current_time = utils.get_current_time() + return ValidateResponsePayload( + description="Exception validating configuration.", + status=validate_payload.Status.FAILED, + timestamp=current_time, + target=Target(type=BaseContext.ProductEnum.VCENTER, hostname=hostname) if hostname else None, + errors=[ + Error( + timestamp=current_time, + source=ErrorSource(endpoint=VC_VALIDATE_CONFIGURATION_V1), + error=Message(message=error_message), + ) + ], + ) diff --git a/config_modules_vmware/services/mapper/control_config_mapping.json b/config_modules_vmware/services/mapper/control_config_mapping.json index aa7a316..8b0019d 100644 --- a/config_modules_vmware/services/mapper/control_config_mapping.json +++ b/config_modules_vmware/services/mapper/control_config_mapping.json @@ -37,6 +37,7 @@ "dvpg_excluded_reserved_vlan_policy": "config_modules_vmware.controllers.vcenter.dv_pg_reserved_vlan_exclusion_policy.DVPortGroupReservedVlanExclusionConfig", "dvpg_vlan_trunking_authorized_check": "config_modules_vmware.controllers.vcenter.dv_pg_vlan_trunking_authorized.DVPortGroupVlanTrunkingConfig", "dvpg_vmotion_traffic_isolation": "config_modules_vmware.controllers.vcenter.vmotion_port_group_config.VMotionPortGroupConfig", + "ip_based_storage_port_group_config": "config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig", "task_and_event_retention": "config_modules_vmware.controllers.vcenter.task_and_event_retention_policy.TaskAndEventRetentionPolicy", "vpx_host_password_length_policy": "config_modules_vmware.controllers.vcenter.vpx_user_host_password_length_policy.VpxUserPasswordLengthPolicy", "vpx_password_expiration_policy": "config_modules_vmware.controllers.vcenter.vpx_user_password_expiration_policy.VpxUserPasswordExpirationPolicy", @@ -84,6 +85,7 @@ "pg_vss_allow_promiscuous_mode": "config_modules_vmware.controllers.esxi.pg_vss_allow_promiscuous_mode.PgVssAllowPromiscuousMode", "suppress_hyperthread_warning": "config_modules_vmware.controllers.esxi.hyperthread_warning_policy.HyperthreadWarningPolicy", "suppress_shell_warning": "config_modules_vmware.controllers.esxi.suppress_shell_warning_policy.SuppressShellWarningPolicy", + "ntp_config": "config_modules_vmware.controllers.esxi.ntp_config.NtpConfig", "ntp_service_config": "config_modules_vmware.controllers.esxi.ntp_service_config.NtpServiceConfig", "ntp_service_startup_policy": "config_modules_vmware.controllers.esxi.ntp_service_startup_policy.NtpServiceStartupPolicy", "max_failed_login_attempts": "config_modules_vmware.controllers.esxi.max_failed_login_attempts.MaxFailedLoginAttempts", @@ -94,9 +96,18 @@ "ssh_login_banner": "config_modules_vmware.controllers.esxi.ssh_login_banner.SshLoginBanner", "ad_esx_admin_group_config": "config_modules_vmware.controllers.esxi.ad_esx_admin_group_config.AdEsxAdminGroupConfig", "ssh_fips_140_2_crypt_config": "config_modules_vmware.controllers.esxi.ssh_fips_140_2_crypt_config.SshFips140_2CryptConfig", + "snmp_config": "config_modules_vmware.controllers.esxi.snmp_config.SnmpConfig", "dcui_login_banner": "config_modules_vmware.controllers.esxi.dcui_login_banner.DcuiLoginBanner", "ssh_port_forwarding": "config_modules_vmware.controllers.esxi.ssh_port_forwarding_policy.SshPortForwardingPolicy", "ssh_ignore_rhosts": "config_modules_vmware.controllers.esxi.ssh_ignore_rhosts_policy.SshIgnoreRHostsPolicy", + "ssh_gateway_ports": "config_modules_vmware.controllers.esxi.ssh_gateway_ports_policy.SshGatewayPortsPolicy", + "ssh_host_based_authentication": "config_modules_vmware.controllers.esxi.ssh_host_based_auth_policy.SshHostBasedAuthPolicy", + "ssh_permit_tunnel": "config_modules_vmware.controllers.esxi.ssh_permit_tunnel_policy.SshPermitTunnelPolicy", + "ssh_permit_user_environment": "config_modules_vmware.controllers.esxi.ssh_permit_user_environment_policy.SshPermitUserEnvironmentPolicy", + "ssh_permit_empty_passwords": "config_modules_vmware.controllers.esxi.ssh_permit_empty_passwords_policy.SshPermitEmptyPasswordsPolicy", + "ssh_compression": "config_modules_vmware.controllers.esxi.ssh_compression_policy.SshCompressionPolicy", + "ssh_strict_mode": "config_modules_vmware.controllers.esxi.ssh_strict_mode_policy.SshStrictModePolicy", + "ssh_x11_forwarding": "config_modules_vmware.controllers.esxi.ssh_x11_forwarding_policy.SshX11ForwardingPolicy", "tls_version": "config_modules_vmware.controllers.esxi.tls_version.TlsVersion", "lockdown_mode": "config_modules_vmware.controllers.esxi.lockdown_mode_config.LockdownModeConfig", "lockdown_dcui_access_users": "config_modules_vmware.controllers.esxi.lockdown_dcui_access_users.LockdownDcuiAccessUsers", @@ -105,6 +116,7 @@ "host_client_session_idle_timeout": "config_modules_vmware.controllers.esxi.host_client_session_idle_timeout.HostClientSessionIdleTimeout", "interactive_shell_idle_timeout": "config_modules_vmware.controllers.esxi.interactive_shell_idle_timeout.InteractiveShellIdleTimeout", "dv_filter_bind_ip_config": "config_modules_vmware.controllers.esxi.dv_filter_bind_ip_config.DvFilterBindIpConfig", + "password_quality_config": "config_modules_vmware.controllers.esxi.password_quality_config.PasswordQualityConfig", "image_profile_acceptance_level": "config_modules_vmware.controllers.esxi.image_profile_acceptance_level.ImageProfileAcceptanceLevel", "mem_share_force_salting_config": "config_modules_vmware.controllers.esxi.mem_share_force_salting_config.MemShareForceSaltingConfig", "slp_service_policy": "config_modules_vmware.controllers.esxi.slp_service_policy.SlpServicePolicy", @@ -116,6 +128,7 @@ "syslog_strict_x509_compliance": "config_modules_vmware.controllers.esxi.syslog_strict_x509_compliance.SyslogStrictX509Compliance", "rhttpproxy_fips_140_2_crypt_config": "config_modules_vmware.controllers.esxi.rhttpproxy_fips_140_2_crypt_config.RHttpProxyFips140_2CryptConfig", "userworld_memory_zeroing_config": "config_modules_vmware.controllers.esxi.userworld_memory_zeroing_config.UserworldMemoryZeroingConfig", + "log_location_config": "config_modules_vmware.controllers.esxi.log_location_config.LogLocationConfig", "vim_api_session_timeout": "config_modules_vmware.controllers.esxi.vim_api_session_timeout.VimApiSessionTimeout" } } diff --git a/config_modules_vmware/services/workflows/compliance_operations.py b/config_modules_vmware/services/workflows/compliance_operations.py index 047ea8e..02104af 100644 --- a/config_modules_vmware/services/workflows/compliance_operations.py +++ b/config_modules_vmware/services/workflows/compliance_operations.py @@ -108,7 +108,7 @@ def operate( del result[consts.RESULT] return result - else: + elif operation == Operations.CHECK_COMPLIANCE or operation == Operations.REMEDIATE: # For CHECK_COMPLIANCE and REMEDIATE operation, validate desired input spec against schema # and perform check compliance/remediation calling iterate_desired_state if input_values is None: @@ -127,6 +127,10 @@ def operate( metadata_filter, overall_status, ) + else: + err_msg = f"{operation.name} is not a valid operation for compliance controls." + logger.error(err_msg) + raise Exception(err_msg) @classmethod def _get_current_items( @@ -287,12 +291,11 @@ def _iterate_desired_state( raise Exception("Value key is missing.") with ControllerMetadataLoggingContext(config_obj.metadata): control_result = operation_function(context, control_data[consts.VALUE]) - # Temporary patch to convert "errors" key to "message" for remediation skipped case - # Need to revisit this post we fix all controllers for which remediate is skipped. - if ( - operation == Operations.REMEDIATE.value - and control_result.get(consts.STATUS) == RemediateStatus.SKIPPED - and control_result.get(consts.ERRORS) + # For the controls which are skipped during compliance or remediation with errors set, + # Convert 'errors' key to 'message' key. + if consts.ERRORS in control_result and ( + control_result.get(consts.STATUS) == RemediateStatus.SKIPPED + or control_result.get(consts.STATUS) == ComplianceStatus.SKIPPED ): control_result[consts.MESSAGE] = control_result.get(consts.ERRORS) del control_result[consts.ERRORS] diff --git a/config_modules_vmware/services/workflows/configuration_operations.py b/config_modules_vmware/services/workflows/configuration_operations.py index e3d2187..705154e 100644 --- a/config_modules_vmware/services/workflows/configuration_operations.py +++ b/config_modules_vmware/services/workflows/configuration_operations.py @@ -9,7 +9,11 @@ from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus from config_modules_vmware.framework.models.output_models.get_current_response import GetCurrentConfigurationStatus +from config_modules_vmware.framework.models.output_models.get_schema_response import GetSchemaStatus from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.models.output_models.validate_configuration_response import ( + ValidateConfigurationStatus, +) from config_modules_vmware.services.mapper import mapper_utils from config_modules_vmware.services.workflows.operations_interface import Operations from config_modules_vmware.services.workflows.operations_interface import OperationsInterface @@ -42,7 +46,10 @@ def operate( :type metadata_filter: Callable[[ControllerMetadata], bool] """ config_template = mapper_utils.get_mapping_template(mapper_utils.CONFIGURATION_MAPPING_FILE) - if operation in (Operations.CHECK_COMPLIANCE, Operations.REMEDIATE) and input_values is None: + if ( + operation in (Operations.CHECK_COMPLIANCE, Operations.REMEDIATE, Operations.VALIDATE) + and input_values is None + ): err_msg = f"input_values cannot be None for {operation.name} operation." logger.error(err_msg) raise Exception(err_msg) @@ -86,13 +93,15 @@ def _invoke_product_configuration( Operations.CHECK_COMPLIANCE: ComplianceStatus.SKIPPED, Operations.REMEDIATE: RemediateStatus.SKIPPED, Operations.GET_CURRENT: GetCurrentConfigurationStatus.SKIPPED, + Operations.GET_SCHEMA: GetSchemaStatus.SKIPPED, + Operations.VALIDATE: ValidateConfigurationStatus.SKIPPED, }[operation] + # Supported product check if context.product_category.value not in config_template: msg = f"{context.product_category.value} is not a supported product configuration" logger.info(msg) result = {consts.STATUS: skipped_status, consts.MESSAGE: msg} else: - result = {} class_file = config_template[context.product_category.value] class_ref = mapper_utils.get_class(class_file) config_obj = class_ref() @@ -104,25 +113,30 @@ def _invoke_product_configuration( with ControllerMetadataLoggingContext(config_obj.metadata): output, errors = config_obj.get(context, input_values) if errors: + logger.error( + f"Get current configuration for {config_obj.metadata.name} returned errors - {errors}" + ) if len(errors) == 1 and errors[0] == consts.SKIPPED: - msg = f"Version [{context.product_version}] is not supported for product [{context.product_category}]" - logger.info(msg) - result = {consts.STATUS: skipped_status, consts.MESSAGE: msg} + result = { + consts.STATUS: GetCurrentConfigurationStatus.SKIPPED, + consts.MESSAGE: consts.UNSUPPORTED_VERSION_MESSAGE_FORMAT.format( + context.product_version, context.product_category + ), + } else: - logger.error( - f"Get current configuration for {config_obj.metadata.name} returned errors - {errors}" - ) result = { consts.STATUS: GetCurrentConfigurationStatus.FAILED, consts.MESSAGE: f"{errors[0]}" if len(errors) == 1 else f"{errors}", } else: result = {consts.STATUS: GetCurrentConfigurationStatus.SUCCESS, consts.RESULT: output} - elif operation == Operations.CHECK_COMPLIANCE: - with ControllerMetadataLoggingContext(config_obj.metadata): - result = config_obj.check_compliance(context, input_values) - elif operation == Operations.REMEDIATE: + else: + operation_function = getattr(config_obj, operation.value) + with ControllerMetadataLoggingContext(config_obj.metadata): - result = config_obj.remediate(context, input_values) + if operation == Operations.GET_SCHEMA: + result = operation_function(context) + else: + result = operation_function(context, input_values) result_config.update(result) diff --git a/config_modules_vmware/services/workflows/operations_interface.py b/config_modules_vmware/services/workflows/operations_interface.py index 3da886f..fae8084 100644 --- a/config_modules_vmware/services/workflows/operations_interface.py +++ b/config_modules_vmware/services/workflows/operations_interface.py @@ -17,6 +17,8 @@ class Operations(Enum): GET_CURRENT = "get_current" REMEDIATE = "remediate" CHECK_COMPLIANCE = "check_compliance" + GET_SCHEMA = "get_schema" + VALIDATE = "validate" class OperationsInterface(ABC): diff --git a/config_modules_vmware/tests/controllers/esxi/test_cim_service_policy.py b/config_modules_vmware/tests/controllers/esxi/test_cim_service_policy.py index 956c12e..9db058b 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_cim_service_policy.py +++ b/config_modules_vmware/tests/controllers/esxi/test_cim_service_policy.py @@ -95,6 +95,6 @@ def test_remediate_skipped_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_dv_filter_bind_ip_config.py b/config_modules_vmware/tests/controllers/esxi/test_dv_filter_bind_ip_config.py index 6d8e0ac..fcacda1 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_dv_filter_bind_ip_config.py +++ b/config_modules_vmware/tests/controllers/esxi/test_dv_filter_bind_ip_config.py @@ -129,6 +129,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value1) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_firewall_default_action_incoming.py b/config_modules_vmware/tests/controllers/esxi/test_firewall_default_action_incoming.py index 6b6975a..56f6eea 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_firewall_default_action_incoming.py +++ b/config_modules_vmware/tests/controllers/esxi/test_firewall_default_action_incoming.py @@ -85,6 +85,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_firewall_rulesets_config.py b/config_modules_vmware/tests/controllers/esxi/test_firewall_rulesets_config.py index 6eabd03..3d531b0 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_firewall_rulesets_config.py +++ b/config_modules_vmware/tests/controllers/esxi/test_firewall_rulesets_config.py @@ -234,3 +234,56 @@ def test_remediate_partial_status(self): assert result["errors"] == self.mock_9_expected_errors assert result["old"] == [self.remediate_mock_9_old] assert result["new"] == [self.remediate_mock_9_new] + + def test_remediate_update_ruleset_not_controllable(self): + mock_host_ref = MagicMock() + ruleset = MagicMock() + ruleset.key = "Test rule" + ruleset.enabled = False + ruleset.allowedHosts.allIp = False + ruleset.allowedHosts.ipAddress = [] + ruleset.allowedHosts.ipNetwork = [] + ruleset.rules = [] + ruleset.userControllable = False + mock_host_ref.configManager.firewallSystem.firewallInfo.ruleset = [ruleset] + mock_context = HostContext(host_ref=mock_host_ref) + desired_spec = { + "name": "Test rule", + "allow_all_ip": True, + "enabled": True, + "allowed_ips": { + "address": [], + "network": [] + }, + "rules": [] + } + result = self.controller.remediate(mock_context, [desired_spec]) + assert result["status"] == "PARTIAL" + assert result["errors"] == ["Manual intervention required for ruleset [Test rule]. Remediation not supported for configuration [enabled].True"] + + def test_remediate_update_ruleset_ip_not_configurable(self): + mock_host_ref = MagicMock() + ruleset = MagicMock() + ruleset.key = "Test rule" + ruleset.enabled = True + ruleset.allowedHosts.allIp = True + ruleset.allowedHosts.ipAddress = [] + ruleset.allowedHosts.ipNetwork = [] + ruleset.rules = [] + ruleset.userControllable = False + ruleset.ipListUserConfigurable = False + mock_host_ref.configManager.firewallSystem.firewallInfo.ruleset = [ruleset] + mock_context = HostContext(host_ref=mock_host_ref) + desired_spec = { + "name": "Test rule", + "allow_all_ip": False, + "enabled": True, + "allowed_ips": { + "address": [], + "network": [] + }, + "rules": [] + } + result = self.controller.remediate(mock_context, [desired_spec]) + assert result["status"] == "FAILED" + assert result["errors"] == ["Manual intervention required for ruleset [Test rule]. Remediation not supported for configuration [allow_all_ip].False"] diff --git a/config_modules_vmware/tests/controllers/esxi/test_image_profile_acceptance_level.py b/config_modules_vmware/tests/controllers/esxi/test_image_profile_acceptance_level.py index dc1c9f6..926f747 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_image_profile_acceptance_level.py +++ b/config_modules_vmware/tests/controllers/esxi/test_image_profile_acceptance_level.py @@ -84,6 +84,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_lockdown_dcui_access_users.py b/config_modules_vmware/tests/controllers/esxi/test_lockdown_dcui_access_users.py index 1832ade..243c4b8 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_lockdown_dcui_access_users.py +++ b/config_modules_vmware/tests/controllers/esxi/test_lockdown_dcui_access_users.py @@ -106,6 +106,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.desired_configs) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_lockdown_mode_config.py b/config_modules_vmware/tests/controllers/esxi/test_lockdown_mode_config.py index 4ed4b40..a989378 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_lockdown_mode_config.py +++ b/config_modules_vmware/tests/controllers/esxi/test_lockdown_mode_config.py @@ -70,6 +70,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.desired_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_lockdown_mode_exception_users.py b/config_modules_vmware/tests/controllers/esxi/test_lockdown_mode_exception_users.py index c3ae09d..00622ea 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_lockdown_mode_exception_users.py +++ b/config_modules_vmware/tests/controllers/esxi/test_lockdown_mode_exception_users.py @@ -90,6 +90,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.desired_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_log_location_config.py b/config_modules_vmware/tests/controllers/esxi/test_log_location_config.py new file mode 100644 index 0000000..a8aa6c1 --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_log_location_config.py @@ -0,0 +1,146 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from mock import MagicMock + +from config_modules_vmware.controllers.esxi.log_location_config import IS_PERSISTENT +from config_modules_vmware.controllers.esxi.log_location_config import LOG_LOCATION +from config_modules_vmware.controllers.esxi.log_location_config import LogLocationConfig +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + + +class TestLogLocationConfig: + def setup_method(self): + self.controller = LogLocationConfig() + self.compliant_value = {IS_PERSISTENT: True, LOG_LOCATION: "/scratch/logs"} + self.non_compliant_value = {IS_PERSISTENT: False, LOG_LOCATION: "/tmp/logs"} + self.invalid_value = {IS_PERSISTENT: True, LOG_LOCATION: "/invalid"} + self.cli_return_compliant_value = " Allow Vsan Backing: false\n" \ + " Local Log Output: /scratch/logs\n" \ + " Local Log Output Is Persistent: true\n" \ + " Log Level: error" + self.cli_return_non_compliant_value = " Allow Vsan Backing: false\n" \ + " Local Log Output: /tmp/logs\n" \ + " Local Log Output Is Persistent: false\n" \ + " Log Level: error" + mock_host_ref = MagicMock() + mock_host_ref.name = 'host-1' + self.esx_cli_client = MagicMock() + self.mock_host_context = HostContext(host_ref=mock_host_ref, esx_cli_client_func=self.esx_cli_client) + + def test_get_success(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_compliant_value, "", 0) + result, errors = self.controller.get(self.mock_host_context) + assert result == self.compliant_value + assert errors == [] + + def test_get_failed(self): + expected_error = "Test exception" + self.esx_cli_client().run_esx_cli_cmd.side_effect = Exception(expected_error) + result, errors = self.controller.get(self.mock_host_context) + assert result == {} + assert errors == [expected_error] + + def test_get_failed_cli_not_returning_log_location(self): + cli_return_value = " Allow Vsan Backing: false\n" \ + " Local Log Output Is Persistent: true\n" \ + " Log Level: error" + expected_error = "Unable to fetch log location using command esxcli system syslog config get" + self.esx_cli_client().run_esx_cli_cmd.return_value = (cli_return_value, "", 0) + result, errors = self.controller.get(self.mock_host_context) + assert result == {} + assert errors == [expected_error] + + def test_get_failed_cli_not_returning_log_persistent_flag(self): + cli_return_value = " Allow Vsan Backing: false\n" \ + " Local Log Output: /scratch/logs\n" \ + " Log Level: error" + expected_error = "Unable to fetch persistent flag using command esxcli system syslog config get" + self.esx_cli_client().run_esx_cli_cmd.return_value = (cli_return_value, "", 0) + result, errors = self.controller.get(self.mock_host_context) + assert result == {} + assert errors == [expected_error] + + def test_get_failed_cli_execution_failed(self): + expected_error = f"Command esxcli system syslog config get failed. Dummy_out Dummy_err" + self.esx_cli_client().run_esx_cli_cmd.side_effect = [(self.cli_return_non_compliant_value, "", 0), + ("Dummy_out", "Dummy_err", 1)] + result, errors = self.controller.get(self.mock_host_context) + assert errors == [expected_error] + assert result == {} + + def test_set_success(self): + self.cli_return_not_persistent_value = " Allow Vsan Backing: false\n" \ + " Local Log Output: /scratch/logs\n" \ + " Local Log Output Is Persistent: false\n" \ + " Log Level: error" + self.esx_cli_client().run_esx_cli_cmd.side_effect = [(self.cli_return_non_compliant_value, "", 0), + ("", "", 0), + (self.cli_return_compliant_value, "", 0)] + status, errors = self.controller.set(self.mock_host_context, self.compliant_value) + assert status == RemediateStatus.SUCCESS + assert errors == [] + + def test_set_failed(self): + out = "Failed to create directory /invalid: Operation not permitted." + expected_error = f"Command esxcli system syslog config set --logdir=/invalid failed. {out} Dummy_err" + self.esx_cli_client().run_esx_cli_cmd.side_effect = [(self.cli_return_non_compliant_value, "", 0), + (out, "Dummy_err", 1)] + status, errors = self.controller.set(self.mock_host_context, self.invalid_value) + assert errors == [expected_error] + assert status == RemediateStatus.FAILED + + def test_set_failed_conflict_desired_values(self): + conflict_desired_value = {IS_PERSISTENT: True, LOG_LOCATION: "/tmp/logs"} + cli_return_conflict_desired_values = " Allow Vsan Backing: false\n" \ + " Local Log Output: /tmp/logs\n" \ + " Local Log Output Is Persistent: false\n" \ + " Log Level: error" + expected_errors = ["'log_location: /tmp/logs' is not matching the desired criteria 'is_persistent: True'"] + self.esx_cli_client().run_esx_cli_cmd.side_effect = [(cli_return_conflict_desired_values, "", 0), + ("", "", 0), + (cli_return_conflict_desired_values, "", 0), + ("", "", 0)] + status, errors = self.controller.set(self.mock_host_context, conflict_desired_value) + assert errors == expected_errors + assert status == RemediateStatus.FAILED + + def test_check_compliance_compliant(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_compliant_value, "", 0) + expected_result = {consts.STATUS: ComplianceStatus.COMPLIANT} + result = self.controller.check_compliance(self.mock_host_context, self.compliant_value) + assert result == expected_result + + def test_check_compliance_non_compliant(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_non_compliant_value, "", 0) + expected_result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: self.non_compliant_value, + consts.DESIRED: self.compliant_value + } + result = self.controller.check_compliance(self.mock_host_context, self.compliant_value) + assert result == expected_result + + def test_remediate(self): + self.esx_cli_client().run_esx_cli_cmd.side_effect = [(self.cli_return_non_compliant_value, "", 0), + (self.cli_return_non_compliant_value, "", 0), + (self.cli_return_non_compliant_value, "", 0), + ("", "", 0), + (self.cli_return_compliant_value, "", 0)] + result = self.controller.remediate(self.mock_host_context, self.compliant_value) + expected_result = { + consts.STATUS: RemediateStatus.SUCCESS, + consts.OLD: self.non_compliant_value, + consts.NEW: self.compliant_value + } + assert result == expected_result + + def test_remediate_with_already_compliant(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_compliant_value, "", 0) + result = self.controller.remediate(self.mock_host_context, self.compliant_value) + expected_result = { + consts.STATUS: RemediateStatus.SKIPPED, + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] + } + assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_mem_share_force_salting_config.py b/config_modules_vmware/tests/controllers/esxi/test_mem_share_force_salting_config.py index 791b198..e2d06b6 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_mem_share_force_salting_config.py +++ b/config_modules_vmware/tests/controllers/esxi/test_mem_share_force_salting_config.py @@ -96,6 +96,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_ntp_config.py b/config_modules_vmware/tests/controllers/esxi/test_ntp_config.py new file mode 100644 index 0000000..22859cd --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_ntp_config.py @@ -0,0 +1,82 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from mock import MagicMock +from pyVmomi import vim + +from config_modules_vmware.controllers.esxi.ntp_config import NtpConfig +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +class TestNtpConfig: + def setup_method(self): + self.controller = NtpConfig() + self.compliant_value = {"protocol": "ntp", "servers": ["10.0.0.250", "10.0.0.251"]} + self.compliant_return_value = {"servers": ["10.0.0.250", "10.0.0.251"]} + self.non_compliant_value = {"protocol": "ntp", "servers": ["10.0.0.253"]} + self.non_compliant_return_value = {"servers": ["10.0.0.253"]} + self.mock_host_ref = MagicMock() + self.mock_host_ref.name = 'host-1' + self.dateTimeInfo = MagicMock(vim.HostDateTimeInfo) + self.dateTimeInfo.systemClockProtocol = "ntp" + self.dateTimeInfo.ntpConfig = MagicMock(vim.HostNtpConfig) + self.dateTimeInfo.ntpConfig.server = [ip for ip in self.compliant_value.get("servers")] + self.mock_host_ref.config.dateTimeInfo = self.dateTimeInfo + + def test_get_success(self): + result, errors = self.controller.get(HostContext(host_ref=self.mock_host_ref)) + assert result == self.compliant_value + assert errors == [] + + def test_get_failed(self): + self.mock_host_ref = -1 + result, errors = self.controller.get(HostContext(host_ref=self.mock_host_ref)) + assert result == {} + assert errors == ["'int' object has no attribute 'config'"] + + def test_set_success(self): + status, errors = self.controller.set(HostContext(host_ref=self.mock_host_ref), self.compliant_value) + assert status == RemediateStatus.SUCCESS + assert errors == [] + + def test_set_failed(self): + self.mock_host_ref.configManager.dateTimeSystem.UpdateDateTimeConfig.side_effect = Exception("Test exception") + self.dateTimeInfo.ntpConfig.server = [ip for ip in self.non_compliant_value.get("servers")] + status, errors = self.controller.set(HostContext(host_ref=self.mock_host_ref), self.compliant_value) + assert status == RemediateStatus.FAILED + assert errors == ["Test exception"] + + def test_check_compliance_compliant(self): + result = self.controller.check_compliance(HostContext(host_ref=self.mock_host_ref), self.compliant_value) + expected_result = { + consts.STATUS: ComplianceStatus.COMPLIANT + } + assert result == expected_result + + def test_check_compliance_non_compliant(self): + self.dateTimeInfo.ntpConfig.server = [ip for ip in self.non_compliant_value.get("servers")] + result = self.controller.check_compliance(HostContext(host_ref=self.mock_host_ref), self.compliant_value) + expected_result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: self.non_compliant_return_value, + consts.DESIRED: self.compliant_return_value + } + assert result == expected_result + + def test_remediate(self): + self.dateTimeInfo.ntpConfig.server = [ip for ip in self.non_compliant_value.get("servers")] + result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value) + expected_result = { + consts.STATUS: RemediateStatus.SUCCESS, + consts.OLD: self.non_compliant_return_value, + consts.NEW: self.compliant_return_value + } + assert result == expected_result + + def test_remediate_with_already_compliant(self): + result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value) + expected_result = { + consts.STATUS: RemediateStatus.SKIPPED, + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] + } + assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_ntp_service_config.py b/config_modules_vmware/tests/controllers/esxi/test_ntp_service_config.py index 5b807e1..b39d7c0 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_ntp_service_config.py +++ b/config_modules_vmware/tests/controllers/esxi/test_ntp_service_config.py @@ -96,6 +96,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_ntp_service_startup_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ntp_service_startup_policy.py index 33ba114..c8bf287 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_ntp_service_startup_policy.py +++ b/config_modules_vmware/tests/controllers/esxi/test_ntp_service_startup_policy.py @@ -95,6 +95,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_password_max_lifetime_policy.py b/config_modules_vmware/tests/controllers/esxi/test_password_max_lifetime_policy.py index 04b92b0..6f4e888 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_password_max_lifetime_policy.py +++ b/config_modules_vmware/tests/controllers/esxi/test_password_max_lifetime_policy.py @@ -115,7 +115,7 @@ def helper_test_remediate_on_already_compliant_system(controller, mock_host_ref, result = controller.remediate(HostContext(host_ref=mock_host_ref), desired_configs) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_password_quality_config.py b/config_modules_vmware/tests/controllers/esxi/test_password_quality_config.py new file mode 100644 index 0000000..404f10b --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_password_quality_config.py @@ -0,0 +1,165 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from mock import MagicMock +from pyVmomi import vim + +from config_modules_vmware.controllers.esxi.password_quality_config import PasswordQualityConfig +from config_modules_vmware.controllers.esxi.password_quality_config import SETTINGS_NAME +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +class TestPasswordQualityConfig: + def setup_method(self): + self.controller = PasswordQualityConfig() + self.compliant_value1 = { + "retry": 3, + "similar": "deny", + "min": "disabled,disabled,disabled,disabled,15", + "passphrase": 3 + } + self.desired_configs = { + "min": "disabled,disabled,disabled,disabled,15", + "passphrase": 3 + } + self.current_configs = { + "min": "disabled,disabled,disabled,7,7", + "passphrase": 10 + } + self.compliant_value2 = { + "retry": 3, + "similar": "deny", + "min": "disabled,disabled,disabled,disabled,15", + } + self.non_compliant_value = { + "retry": 3, + "min": "disabled,disabled,disabled,7,7", + "max": 40, + "passphrase": 10 + } + self.mock_host_ref = MagicMock() + self.mock_host_ref.name = 'host-1' + self.option_string = SETTINGS_NAME + + def test_get_success(self): + password_quality_control = self.controller._create_config_string(self.compliant_value1) + option_values = [vim.option.OptionValue(key=self.option_string, value=password_quality_control)] + self.mock_host_ref.configManager.advancedOption.QueryOptions.return_value = option_values + result, errors = self.controller.get(HostContext(host_ref=self.mock_host_ref)) + assert result == self.compliant_value1 + assert errors == [] + + def test_get_success2(self): + password_quality_control = self.controller._create_config_string(self.compliant_value2) + option_values = [vim.option.OptionValue(key=self.option_string, value=password_quality_control)] + self.mock_host_ref.configManager.advancedOption.QueryOptions.return_value = option_values + result, errors = self.controller.get(HostContext(host_ref=self.mock_host_ref)) + assert result == self.compliant_value2 + assert errors == [] + + def test_get_failed(self): + self.mock_host_ref.configManager.advancedOption.QueryOptions.side_effect = Exception("Test exception") + result, errors = self.controller.get(HostContext(host_ref=self.mock_host_ref)) + assert result == {} + assert errors == [f"Exception on querying advanced options: {self.option_string} for " + f"host: {self.mock_host_ref.name} with error msg: Test exception"] + + def test_get_failed_invalid_name(self): + self.mock_host_ref.configManager.advancedOption.QueryOptions.side_effect = vim.fault.InvalidName + result, errors = self.controller.get(HostContext(host_ref=self.mock_host_ref)) + assert result == {} + assert errors == [f"Invalid query param: {self.option_string} for advanced options " + f"for host: {self.mock_host_ref.name}"] + + def test_get_failed_empty_options(self): + self.mock_host_ref.configManager.advancedOption.QueryOptions.return_value = None + result, errors = self.controller.get(HostContext(host_ref=self.mock_host_ref)) + assert result == {} + assert errors == [f"Exception on querying advanced options: {self.option_string} for " + f"host: {self.mock_host_ref.name} with error msg: Invalid returned options"] + + def test_set_success(self): + self.mock_host_ref.configManager.advancedOption.UpdateOptions.return_value = None + status, errors = self.controller.set(HostContext(host_ref=self.mock_host_ref), self.compliant_value1) + assert status == RemediateStatus.SUCCESS + assert errors == [] + + def test_set_success2(self): + self.mock_host_ref.configManager.advancedOption.UpdateOptions.return_value = None + status, errors = self.controller.set(HostContext(host_ref=self.mock_host_ref), self.compliant_value2) + assert status == RemediateStatus.SUCCESS + assert errors == [] + + def test_set_failed(self): + self.mock_host_ref.configManager.advancedOption.UpdateOptions.side_effect = Exception("Test exception") + status, errors = self.controller.set(HostContext(host_ref=self.mock_host_ref), self.compliant_value1) + assert status == RemediateStatus.FAILED + assert errors == [f"Exception on updating advanced options " + f"for host: {self.mock_host_ref.name} with error msg: Test exception"] + + def test_check_compliance_compliant(self): + password_quality_control = self.controller._create_config_string(self.compliant_value1) + option_values = [vim.option.OptionValue(key=self.option_string, value=password_quality_control)] + self.mock_host_ref.configManager.advancedOption.QueryOptions.return_value = option_values + result = self.controller.check_compliance(HostContext(host_ref=self.mock_host_ref), self.compliant_value1) + expected_result = { + consts.STATUS: ComplianceStatus.COMPLIANT + } + assert result == expected_result + + def test_check_compliance_non_compliant(self): + password_quality_control = self.controller._create_config_string(self.non_compliant_value) + option_values = [vim.option.OptionValue(key=self.option_string, value=password_quality_control)] + self.mock_host_ref.configManager.advancedOption.QueryOptions.return_value = option_values + result = self.controller.check_compliance(HostContext(host_ref=self.mock_host_ref), self.compliant_value1) + expected_result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: self.current_configs, + consts.DESIRED: self.desired_configs + } + assert result == expected_result + + def test_check_compliance_failed(self): + self.mock_host_ref.configManager.advancedOption.QueryOptions.return_value = None + result = self.controller.check_compliance(HostContext(host_ref=self.mock_host_ref), self.compliant_value1) + expected_result = { + consts.STATUS: ComplianceStatus.FAILED, + consts.ERRORS: [f"Exception on querying advanced options: {self.option_string} for " + f"host: {self.mock_host_ref.name} with error msg: Invalid returned options"] + } + assert result == expected_result + + def test_remediate(self): + password_quality_control = self.controller._create_config_string(self.non_compliant_value) + option_values = [vim.option.OptionValue(key=self.option_string, value=password_quality_control)] + self.mock_host_ref.configManager.advancedOption.QueryOptions.return_value = option_values + result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value1) + expected_result = { + consts.STATUS: RemediateStatus.SUCCESS, + consts.OLD: self.current_configs, + consts.NEW: self.desired_configs + } + assert result == expected_result + + def test_remediate2(self): + password_quality_control = self.controller._create_config_string(self.non_compliant_value) + option_values = [vim.option.OptionValue(key=self.option_string, value=password_quality_control)] + self.mock_host_ref.configManager.advancedOption.QueryOptions.return_value = option_values + result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value2) + expected_result = { + consts.STATUS: RemediateStatus.SUCCESS, + consts.OLD: self.current_configs, + consts.NEW: self.desired_configs + } + assert result == expected_result + + def test_remediate_with_already_compliant(self): + password_quality_control = self.controller._create_config_string(self.compliant_value1) + option_values = [vim.option.OptionValue(key=self.option_string, value=password_quality_control)] + self.mock_host_ref.configManager.advancedOption.QueryOptions.return_value = option_values + result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value1) + expected_result = { + consts.STATUS: RemediateStatus.SKIPPED, + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] + } + assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_pg_vss_allow_promiscuous_mode.py b/config_modules_vmware/tests/controllers/esxi/test_pg_vss_allow_promiscuous_mode.py index b723f93..c5f331e 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_pg_vss_allow_promiscuous_mode.py +++ b/config_modules_vmware/tests/controllers/esxi/test_pg_vss_allow_promiscuous_mode.py @@ -76,6 +76,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_pg_vss_forged_transmits_accept.py b/config_modules_vmware/tests/controllers/esxi/test_pg_vss_forged_transmits_accept.py index 984e4a3..407eeb4 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_pg_vss_forged_transmits_accept.py +++ b/config_modules_vmware/tests/controllers/esxi/test_pg_vss_forged_transmits_accept.py @@ -86,6 +86,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_pg_vss_mac_change_accept.py b/config_modules_vmware/tests/controllers/esxi/test_pg_vss_mac_change_accept.py index f6be791..ddb868f 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_pg_vss_mac_change_accept.py +++ b/config_modules_vmware/tests/controllers/esxi/test_pg_vss_mac_change_accept.py @@ -76,6 +76,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_remote_log_server_config.py b/config_modules_vmware/tests/controllers/esxi/test_remote_log_server_config.py index fd280e8..cee8970 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_remote_log_server_config.py +++ b/config_modules_vmware/tests/controllers/esxi/test_remote_log_server_config.py @@ -98,6 +98,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.desired_configs) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_rhttpproxy_fips_140_2_crypt_config.py b/config_modules_vmware/tests/controllers/esxi/test_rhttpproxy_fips_140_2_crypt_config.py index 6786834..35bee53 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_rhttpproxy_fips_140_2_crypt_config.py +++ b/config_modules_vmware/tests/controllers/esxi/test_rhttpproxy_fips_140_2_crypt_config.py @@ -17,6 +17,7 @@ def setup_method(self): self.cli_return_non_compliant_value = " Enabled: false" mock_host_ref = MagicMock() mock_host_ref.name = 'host-1' + mock_host_ref.config.product.version = "7.0.3" self.esx_cli_client = MagicMock() self.mock_host_context = HostContext(host_ref=mock_host_ref, esx_cli_client_func=self.esx_cli_client) @@ -86,6 +87,30 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result + + def test_get_skipped(self): + mock_host_context = MagicMock() + mock_host_context.product_version = "8.0.0" + result, errors = self.controller.get(mock_host_context) + assert result is None + assert errors == [consts.SKIPPED] + + def test_set_skipped(self): + mock_host_context = MagicMock() + mock_host_context.product_version = "8.0.0" + status, errors = self.controller.set(mock_host_context, self.compliant_value) + assert status == RemediateStatus.SKIPPED + assert errors == [consts.CONTROL_NOT_APPLICABLE] + + def test_check_compliance_skipped(self): + mock_host_context = MagicMock() + mock_host_context.product_version = "8.0.0" + expected_result = { + consts.STATUS: ComplianceStatus.SKIPPED, + consts.ERRORS: [consts.CONTROL_NOT_APPLICABLE] + } + result = self.controller.check_compliance(mock_host_context, "no") + assert expected_result == result diff --git a/config_modules_vmware/tests/controllers/esxi/test_shell_service_policy.py b/config_modules_vmware/tests/controllers/esxi/test_shell_service_policy.py index cafeca9..c86aac1 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_shell_service_policy.py +++ b/config_modules_vmware/tests/controllers/esxi/test_shell_service_policy.py @@ -95,6 +95,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_slp_service_policy.py b/config_modules_vmware/tests/controllers/esxi/test_slp_service_policy.py index 38ae9c7..9c5071a 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_slp_service_policy.py +++ b/config_modules_vmware/tests/controllers/esxi/test_slp_service_policy.py @@ -95,6 +95,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_snmp_config.py b/config_modules_vmware/tests/controllers/esxi/test_snmp_config.py new file mode 100644 index 0000000..13ebf0b --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_snmp_config.py @@ -0,0 +1,145 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from mock import MagicMock + +from config_modules_vmware.controllers.esxi.snmp_config import SnmpConfig +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + + +class TestSnmpConfig: + def setup_method(self): + self.controller = SnmpConfig() + self.compliant_value = { + "enable": True, + "authentication": "SHA1", + "privacy": "AES128", + "communities": [ + "private", + "eastnoc", + "westnoc" + ], + "v3_targets": { + "hostname": "10.0.0.250", + "port": 169, + "userid": "tester1", + "security_level": "auth", + "message_type": "trap" + } + } + self.non_compliant_value = { + "enable": False, + "authentication": "none", + "privacy": "none", + "communities": [ + "public" + ], + "v3_targets": { + "hostname": "10.0.0.251", + "port": 168, + "userid": "tester2", + "security_level": "priv", + "message_type": "inform" + } + } + self.cli_return_compliant_value = "\ + Authentication: SHA1\n\ + Communities: private, eastnoc, westnoc\n\ + Enable: true\n\ + Engineid: 80001ADC0516409360261726011136\n\ + Hwsrc: indications\n\ + Largestorage: true\n\ + Loglevel: warning\n\ + Notraps:\n\ + Port: 161\n\ + Privacy: AES128\n\ + Remoteusers:\n\ + Syscontact:\n\ + Syslocation:\n\ + Targets:\n\ + Users:\n\ + V3targets: 10.0.0.250@169 tester1 auth trap" + self.cli_return_non_compliant_value = "\ + Authentication:\n\ + Communities: public\n\ + Enable: false\n\ + Engineid: 80001ADC0516409360261726011136\n\ + Hwsrc: indications\n\ + Largestorage: true\n\ + Loglevel: warning\n\ + Notraps:\n\ + Port: 161\n\ + Privacy:\n\ + Remoteusers:\n\ + Syscontact:\n\ + Syslocation:\n\ + Targets:\n\ + Users:\n\ + V3targets: 10.0.0.251@168 tester2 priv inform" + + mock_host_ref = MagicMock() + mock_host_ref.name = 'host-1' + self.esx_cli_client = MagicMock() + self.mock_host_context = HostContext(host_ref=mock_host_ref, esx_cli_client_func=self.esx_cli_client) + + def test_get_success(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_compliant_value, "", 0) + result, errors = self.controller.get(self.mock_host_context) + assert result == self.compliant_value + assert errors == [] + + def test_get_failed(self): + expected_error = "Test exception" + self.esx_cli_client().run_esx_cli_cmd.side_effect = Exception(expected_error) + result, errors = self.controller.get(self.mock_host_context) + assert result == {} + assert errors == [expected_error] + + def test_set_success(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_non_compliant_value, "", 0) + status, errors = self.controller.set(self.mock_host_context, self.compliant_value) + assert status == RemediateStatus.SUCCESS + assert errors == [] + + def test_set_failed(self): + expected_error = "Test exception" + self.esx_cli_client().run_esx_cli_cmd.side_effect = Exception(expected_error) + status, errors = self.controller.set(self.mock_host_context, self.compliant_value) + assert status == RemediateStatus.FAILED + assert errors == [expected_error] + + def test_check_compliance_compliant(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_compliant_value, "", 0) + expected_result = {consts.STATUS: ComplianceStatus.COMPLIANT} + result = self.controller.check_compliance(self.mock_host_context, self.compliant_value) + assert result == expected_result + + def test_check_compliance_non_compliant(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_non_compliant_value, "", 0) + expected_result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: self.non_compliant_value, + consts.DESIRED: self.compliant_value + } + result = self.controller.check_compliance(self.mock_host_context, self.compliant_value) + assert result == expected_result + + def test_remediate(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_non_compliant_value, "", 0) + result = self.controller.remediate(self.mock_host_context, self.compliant_value) + expected_result = { + consts.STATUS: RemediateStatus.SUCCESS, + consts.OLD: self.non_compliant_value, + consts.NEW: self.compliant_value + } + assert result == expected_result + + def test_remediate_with_already_compliant(self): + self.esx_cli_client().run_esx_cli_cmd.return_value = (self.cli_return_compliant_value, "", 0) + result = self.controller.remediate(self.mock_host_context, self.compliant_value) + expected_result = { + consts.STATUS: RemediateStatus.SKIPPED, + consts.ERRORS: ['Control already compliant'] + } + assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_snmp_service_policy.py b/config_modules_vmware/tests/controllers/esxi/test_snmp_service_policy.py index 052f8fe..3dd57b4 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_snmp_service_policy.py +++ b/config_modules_vmware/tests/controllers/esxi/test_snmp_service_policy.py @@ -95,6 +95,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_compression_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_compression_policy.py new file mode 100644 index 0000000..a14ed0a --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_compression_policy.py @@ -0,0 +1,70 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from mock import MagicMock +from mock import patch + +from config_modules_vmware.controllers.esxi.ssh_compression_policy import SshCompressionPolicy +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.tests.controllers.esxi.test_ssh_host_based_auth_policy import HelperTestSshConfigControls + + +class TestSshCompressionPolicy: + def setup_method(self): + self.controller = SshCompressionPolicy() + self.context = MagicMock() + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_true(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_false(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_empty(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped_8_0(self, mock_set_ssh_config_value): + self.context.product_version = "8.0.3" + status, errors = self.controller.set(self.context, "yes") + mock_set_ssh_config_value.assert_not_called() + assert status == RemediateStatus.SKIPPED + assert errors == [consts.REMEDIATION_SKIPPED_MESSAGE] + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped_7_0(self, mock_set_ssh_config_value): + self.context.product_version = "7.0.3" + status, errors = self.controller.set(self.context, "yes") + mock_set_ssh_config_value.assert_not_called() + assert status == RemediateStatus.SKIPPED + assert errors == [consts.CONTROL_NOT_AUTOMATED] + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_fips_140_2_crypt_config.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_fips_140_2_crypt_config.py index 5237eea..f695ea9 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_ssh_fips_140_2_crypt_config.py +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_fips_140_2_crypt_config.py @@ -77,6 +77,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_gateway_ports_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_gateway_ports_policy.py new file mode 100644 index 0000000..27ccc2b --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_gateway_ports_policy.py @@ -0,0 +1,65 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import mock +from mock import MagicMock +from mock import patch + +from config_modules_vmware.controllers.esxi.ssh_gateway_ports_policy import SshGatewayPortsPolicy +from config_modules_vmware.tests.controllers.esxi.test_ssh_host_based_auth_policy import HelperTestSshConfigControls + + +class TestSshGatewayPortsPolicy: + def setup_method(self): + self.controller = SshGatewayPortsPolicy() + self.context = MagicMock() + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_true(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_false(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_empty(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_success(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_success(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_failed(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_failed(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_skipped(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_host_based_auth_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_host_based_auth_policy.py new file mode 100644 index 0000000..dfc2bc8 --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_host_based_auth_policy.py @@ -0,0 +1,190 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import mock +from mock import MagicMock +from mock import patch + +from config_modules_vmware.controllers.esxi.ssh_host_based_auth_policy import SshHostBasedAuthPolicy +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + + +class HelperTestSshConfigControls: + @staticmethod + def get_default_failed_value(desired_configs): + if isinstance(desired_configs, bool): + return False + elif isinstance(desired_configs, int): + return -1 + elif isinstance(desired_configs, str): + return "" + elif isinstance(desired_configs, list): + return [] + else: + return None + + @staticmethod + def helper_test_get_true(controller, context, mock_get_ssh_config_value): + context.product_version = "8.0.1" + mock_get_ssh_config_value.return_value = "yes" + result, errors = controller.get(context) + assert result == "yes" + assert errors == [] + + @staticmethod + def helper_test_get_false(controller, context, mock_get_ssh_config_value): + context.product_version = "8.0.1" + mock_get_ssh_config_value.return_value = "no" + result, errors = controller.get(context) + assert result == "no" + assert errors == [] + + @staticmethod + def helper_test_get_empty(controller, context, mock_get_ssh_config_value): + context.product_version = "8.0.1" + mock_get_ssh_config_value.return_value = "" + result, errors = controller.get(context) + assert result == "" + assert errors == [] + + @staticmethod + def helper_test_get_failed(controller, context, mock_get_ssh_config_value): + context.product_version = "8.0.1" + mock_get_ssh_config_value.side_effect = Exception("Test exception") + result, errors = controller.get(context) + assert result == "" + assert errors == ["Test exception"] + + @staticmethod + def helper_test_get_skipped(controller, context, mock_get_ssh_config_value): + context.product_version = "7.0.3" + mock_get_ssh_config_value.return_value = "" + result, errors = controller.get(context) + assert result == "" + assert errors == [consts.SKIPPED] + + @staticmethod + def helper_test_set_success(controller, context, mock_set_ssh_config_value): + context.product_version = "8.0.1" + status, errors = controller.set(context, "yes") + mock_set_ssh_config_value.assert_called_once_with(context, mock.ANY, "yes") + assert status == RemediateStatus.SUCCESS + assert errors == [] + + @staticmethod + def helper_test_set_failed(controller, context, mock_set_ssh_config_value): + context.product_version = "8.0.1" + mock_set_ssh_config_value.side_effect = Exception("Test exception") + status, errors = controller.set(context, "yes") + assert status == RemediateStatus.FAILED + assert errors == ["Test exception"] + + @staticmethod + def helper_test_set_skipped(controller, context, mock_set_ssh_config_value): + context.product_version = "7.0.3" + status, errors = controller.set(context, "yes") + mock_set_ssh_config_value.assert_not_called() + assert status == RemediateStatus.SKIPPED + assert errors == [] + + @staticmethod + def helper_test_check_compliance_non_compliant(controller, context, mock_get_ssh_config_value): + context.product_version = "8.0.1" + mock_get_ssh_config_value.return_value = "yes" + expected_result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: "yes", + consts.DESIRED: "no" + } + result = controller.check_compliance(context, "no") + assert expected_result == result + + @staticmethod + def helper_test_check_compliance_compliant(controller, context, mock_get_ssh_config_value): + context.product_version = "8.0.1" + mock_get_ssh_config_value.return_value = "no" + expected_result = { + consts.STATUS: ComplianceStatus.COMPLIANT + } + result = controller.check_compliance(context, "no") + assert expected_result == result + + @staticmethod + def helper_test_check_compliance_failed(controller, context, mock_get_ssh_config_value): + context.product_version = "8.0.1" + mock_get_ssh_config_value.side_effect = Exception("Test exception") + expected_result = { + consts.STATUS: ComplianceStatus.FAILED, + consts.ERRORS: ["Test exception"] + } + result = controller.check_compliance(context, "no") + assert expected_result == result + + @staticmethod + def helper_test_check_compliance_skipped(controller, context, mock_get_ssh_config_value): + context.product_version = "7.0.3" + mock_get_ssh_config_value.return_value = "" + expected_result = { + consts.STATUS: ComplianceStatus.SKIPPED, + consts.ERRORS: [consts.CONTROL_NOT_AUTOMATED] + } + result = controller.check_compliance(context, "no") + assert expected_result == result + + +class TestSshHostBasedAuthPolicy: + def setup_method(self): + self.controller = SshHostBasedAuthPolicy() + self.context = MagicMock() + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_true(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_false(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_empty(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_success(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_success(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_failed(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_failed(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_skipped(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_ignore_rhosts_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_ignore_rhosts_policy.py index 249ac7b..6d30ef9 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_ssh_ignore_rhosts_policy.py +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_ignore_rhosts_policy.py @@ -4,8 +4,7 @@ from mock import patch from config_modules_vmware.controllers.esxi.ssh_ignore_rhosts_policy import SshIgnoreRHostsPolicy -from config_modules_vmware.framework.clients.common import consts -from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.tests.controllers.esxi.test_ssh_host_based_auth_policy import HelperTestSshConfigControls class TestSshIgnoreRHostsPolicy: @@ -15,64 +14,52 @@ def setup_method(self): @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_true(self, mock_get_ssh_config_value): - self.context.product_version = "8.0.1" - mock_get_ssh_config_value.return_value = "yes" - result, errors = self.controller.get(self.context) - assert result == "yes" - assert errors == [] + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_false(self, mock_get_ssh_config_value): - self.context.product_version = "8.0.1" - mock_get_ssh_config_value.return_value = "no" - result, errors = self.controller.get(self.context) - assert result == "no" - assert errors == [] + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_empty(self, mock_get_ssh_config_value): - self.context.product_version = "8.0.1" - mock_get_ssh_config_value.return_value = "" - result, errors = self.controller.get(self.context) - assert result == "" - assert errors == [] + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_failed(self, mock_get_ssh_config_value): - self.context.product_version = "8.0.1" - mock_get_ssh_config_value.side_effect = Exception("Test exception") - result, errors = self.controller.get(self.context) - assert result == "" - assert errors == ["Test exception"] + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_skipped(self, mock_get_ssh_config_value): - self.context.product_version = "7.0.3" - mock_get_ssh_config_value.return_value = "" - result, errors = self.controller.get(self.context) - assert result == "" - assert errors == [consts.SKIPPED] + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') def test_set_success(self, mock_set_ssh_config_value): - self.context.product_version = "8.0.1" - status, errors = self.controller.set(self.context, "yes") - mock_set_ssh_config_value.assert_called_once_with(self.context, mock.ANY, "yes") - assert status == RemediateStatus.SUCCESS - assert errors == [] + HelperTestSshConfigControls.helper_test_set_success(self.controller, self.context, mock_set_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') def test_set_failed(self, mock_set_ssh_config_value): - self.context.product_version = "8.0.1" - mock_set_ssh_config_value.side_effect = Exception("Test exception") - status, errors = self.controller.set(self.context, "yes") - assert status == RemediateStatus.FAILED - assert errors == ["Test exception"] + HelperTestSshConfigControls.helper_test_set_failed(self.controller, self.context, mock_set_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') def test_set_skipped(self, mock_set_ssh_config_value): - self.context.product_version = "7.0.3" - status, errors = self.controller.set(self.context, "yes") - mock_set_ssh_config_value.assert_not_called() - assert status == RemediateStatus.SKIPPED - assert errors == [] + HelperTestSshConfigControls.helper_test_set_skipped(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_permit_empty_passwords_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_permit_empty_passwords_policy.py new file mode 100644 index 0000000..4a51d6d --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_permit_empty_passwords_policy.py @@ -0,0 +1,70 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from mock import MagicMock +from mock import patch + +from config_modules_vmware.controllers.esxi.ssh_permit_empty_passwords_policy import SshPermitEmptyPasswordsPolicy +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.tests.controllers.esxi.test_ssh_host_based_auth_policy import HelperTestSshConfigControls + + +class TestSshPermitEmptyPasswordsPolicy: + def setup_method(self): + self.controller = SshPermitEmptyPasswordsPolicy() + self.context = MagicMock() + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_true(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_false(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_empty(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped_8_0(self, mock_set_ssh_config_value): + self.context.product_version = "8.0.3" + status, errors = self.controller.set(self.context, "yes") + mock_set_ssh_config_value.assert_not_called() + assert status == RemediateStatus.SKIPPED + assert errors == [consts.REMEDIATION_SKIPPED_MESSAGE] + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped_7_0(self, mock_set_ssh_config_value): + self.context.product_version = "7.0.3" + status, errors = self.controller.set(self.context, "yes") + mock_set_ssh_config_value.assert_not_called() + assert status == RemediateStatus.SKIPPED + assert errors == [consts.CONTROL_NOT_AUTOMATED] + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_permit_tunnel_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_permit_tunnel_policy.py new file mode 100644 index 0000000..7c13115 --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_permit_tunnel_policy.py @@ -0,0 +1,65 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import mock +from mock import MagicMock +from mock import patch + +from config_modules_vmware.controllers.esxi.ssh_permit_tunnel_policy import SshPermitTunnelPolicy +from config_modules_vmware.tests.controllers.esxi.test_ssh_host_based_auth_policy import HelperTestSshConfigControls + + +class TestSshPermitUserEnvironmentPolicy: + def setup_method(self): + self.controller = SshPermitTunnelPolicy() + self.context = MagicMock() + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_true(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_false(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_empty(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_success(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_success(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_failed(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_failed(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_skipped(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_permit_user_environment_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_permit_user_environment_policy.py new file mode 100644 index 0000000..ee4aaab --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_permit_user_environment_policy.py @@ -0,0 +1,65 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import mock +from mock import MagicMock +from mock import patch + +from config_modules_vmware.controllers.esxi.ssh_permit_user_environment_policy import SshPermitUserEnvironmentPolicy +from config_modules_vmware.tests.controllers.esxi.test_ssh_host_based_auth_policy import HelperTestSshConfigControls + + +class TestSshPermitUserEnvironmentPolicy: + def setup_method(self): + self.controller = SshPermitUserEnvironmentPolicy() + self.context = MagicMock() + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_true(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_false(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_empty(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_success(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_success(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_failed(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_failed(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped(self, mock_set_ssh_config_value): + HelperTestSshConfigControls.helper_test_set_skipped(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_port_forwarding_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_port_forwarding_policy.py index 41262b7..679701d 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_ssh_port_forwarding_policy.py +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_port_forwarding_policy.py @@ -4,8 +4,7 @@ from mock import patch from config_modules_vmware.controllers.esxi.ssh_port_forwarding_policy import SshPortForwardingPolicy -from config_modules_vmware.framework.clients.common import consts -from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.tests.controllers.esxi.test_ssh_host_based_auth_policy import HelperTestSshConfigControls class TestSshPortForwardingPolicy: @@ -15,64 +14,52 @@ def setup_method(self): @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_true(self, mock_get_ssh_config_value): - self.context.product_version = "8.0.1" - mock_get_ssh_config_value.return_value = "yes" - result, errors = self.controller.get(self.context) - assert result == "yes" - assert errors == [] + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_false(self, mock_get_ssh_config_value): - self.context.product_version = "8.0.1" - mock_get_ssh_config_value.return_value = "no" - result, errors = self.controller.get(self.context) - assert result == "no" - assert errors == [] + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_empty(self, mock_get_ssh_config_value): - self.context.product_version = "8.0.1" - mock_get_ssh_config_value.return_value = "" - result, errors = self.controller.get(self.context) - assert result == "" - assert errors == [] + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_failed(self, mock_get_ssh_config_value): - self.context.product_version = "8.0.1" - mock_get_ssh_config_value.side_effect = Exception("Test exception") - result, errors = self.controller.get(self.context) - assert result == "" - assert errors == ["Test exception"] + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') def test_get_skipped(self, mock_get_ssh_config_value): - self.context.product_version = "7.0.3" - mock_get_ssh_config_value.return_value = "" - result, errors = self.controller.get(self.context) - assert result == "" - assert errors == [consts.SKIPPED] + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') def test_set_success(self, mock_set_ssh_config_value): - self.context.product_version = "8.0.1" - status, errors = self.controller.set(self.context, "yes") - mock_set_ssh_config_value.assert_called_once_with(self.context, mock.ANY, "yes") - assert status == RemediateStatus.SUCCESS - assert errors == [] + HelperTestSshConfigControls.helper_test_set_success(self.controller, self.context, mock_set_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') def test_set_failed(self, mock_set_ssh_config_value): - self.context.product_version = "8.0.1" - mock_set_ssh_config_value.side_effect = Exception("Test exception") - status, errors = self.controller.set(self.context, "yes") - assert status == RemediateStatus.FAILED - assert errors == ["Test exception"] + HelperTestSshConfigControls.helper_test_set_failed(self.controller, self.context, mock_set_ssh_config_value) @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') def test_set_skipped(self, mock_set_ssh_config_value): - self.context.product_version = "7.0.3" - status, errors = self.controller.set(self.context, "yes") - mock_set_ssh_config_value.assert_not_called() - assert status == RemediateStatus.SKIPPED - assert errors == [] + HelperTestSshConfigControls.helper_test_set_skipped(self.controller, self.context, mock_set_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_service_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_service_policy.py index f0d999e..cbc2bd2 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_ssh_service_policy.py +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_service_policy.py @@ -94,6 +94,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_strict_mode_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_strict_mode_policy.py new file mode 100644 index 0000000..86e7234 --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_strict_mode_policy.py @@ -0,0 +1,70 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from mock import MagicMock +from mock import patch + +from config_modules_vmware.controllers.esxi.ssh_strict_mode_policy import SshStrictModePolicy +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.tests.controllers.esxi.test_ssh_host_based_auth_policy import HelperTestSshConfigControls + + +class TestSshStrictModePolicy: + def setup_method(self): + self.controller = SshStrictModePolicy() + self.context = MagicMock() + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_true(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_false(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_empty(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped_8_0(self, mock_set_ssh_config_value): + self.context.product_version = "8.0.3" + status, errors = self.controller.set(self.context, "yes") + mock_set_ssh_config_value.assert_not_called() + assert status == RemediateStatus.SKIPPED + assert errors == [consts.REMEDIATION_SKIPPED_MESSAGE] + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped_7_0(self, mock_set_ssh_config_value): + self.context.product_version = "7.0.3" + status, errors = self.controller.set(self.context, "yes") + mock_set_ssh_config_value.assert_not_called() + assert status == RemediateStatus.SKIPPED + assert errors == [consts.CONTROL_NOT_AUTOMATED] + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_ssh_x11_forwarding_policy.py b/config_modules_vmware/tests/controllers/esxi/test_ssh_x11_forwarding_policy.py new file mode 100644 index 0000000..7361bfa --- /dev/null +++ b/config_modules_vmware/tests/controllers/esxi/test_ssh_x11_forwarding_policy.py @@ -0,0 +1,70 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +from mock import MagicMock +from mock import patch + +from config_modules_vmware.controllers.esxi.ssh_x11_forwarding_policy import SshX11ForwardingPolicy +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.tests.controllers.esxi.test_ssh_host_based_auth_policy import HelperTestSshConfigControls + + +class TestSshPermitEmptyPasswordsPolicy: + def setup_method(self): + self.controller = SshX11ForwardingPolicy() + self.context = MagicMock() + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_true(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_true(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_false(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_false(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_empty(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_empty(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_failed(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_get_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_get_skipped(self.controller, self.context, mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped_8_0(self, mock_set_ssh_config_value): + self.context.product_version = "8.0.3" + status, errors = self.controller.set(self.context, "yes") + mock_set_ssh_config_value.assert_not_called() + assert status == RemediateStatus.SKIPPED + assert errors == [consts.REMEDIATION_SKIPPED_MESSAGE] + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.set_ssh_config_value') + def test_set_skipped_7_0(self, mock_set_ssh_config_value): + self.context.product_version = "7.0.3" + status, errors = self.controller.set(self.context, "yes") + mock_set_ssh_config_value.assert_not_called() + assert status == RemediateStatus.SKIPPED + assert errors == [consts.CONTROL_NOT_AUTOMATED] + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_non_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_non_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_compliant(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_compliant(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_failed(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_failed(self.controller, self.context, + mock_get_ssh_config_value) + + @patch('config_modules_vmware.controllers.esxi.utils.esxi_ssh_config_utils.get_ssh_config_value') + def test_check_compliance_skipped(self, mock_get_ssh_config_value): + HelperTestSshConfigControls.helper_test_check_compliance_skipped(self.controller, self.context, + mock_get_ssh_config_value) diff --git a/config_modules_vmware/tests/controllers/esxi/test_syslog_enforce_ssl_certificates.py b/config_modules_vmware/tests/controllers/esxi/test_syslog_enforce_ssl_certificates.py index f493f6f..561a335 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_syslog_enforce_ssl_certificates.py +++ b/config_modules_vmware/tests/controllers/esxi/test_syslog_enforce_ssl_certificates.py @@ -90,6 +90,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_syslog_strict_x509_compliance.py b/config_modules_vmware/tests/controllers/esxi/test_syslog_strict_x509_compliance.py index 8769550..e79b7dd 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_syslog_strict_x509_compliance.py +++ b/config_modules_vmware/tests/controllers/esxi/test_syslog_strict_x509_compliance.py @@ -90,6 +90,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(self.mock_host_context, self.compliant_value) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/esxi/test_tls_version.py b/config_modules_vmware/tests/controllers/esxi/test_tls_version.py index 0408e9b..98757f2 100644 --- a/config_modules_vmware/tests/controllers/esxi/test_tls_version.py +++ b/config_modules_vmware/tests/controllers/esxi/test_tls_version.py @@ -107,6 +107,6 @@ def test_remediate_with_already_compliant(self): result = self.controller.remediate(HostContext(host_ref=self.mock_host_ref), self.desired_configs) expected_result = { consts.STATUS: RemediateStatus.SKIPPED, - consts.ERRORS: ['Control already compliant'] + consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/sample/test_sample_controller.py b/config_modules_vmware/tests/controllers/sample/test_sample_controller.py index b638f26..e547270 100644 --- a/config_modules_vmware/tests/controllers/sample/test_sample_controller.py +++ b/config_modules_vmware/tests/controllers/sample/test_sample_controller.py @@ -3,6 +3,7 @@ from config_modules_vmware.controllers.sample.sample_controller import SampleController from config_modules_vmware.framework.clients.common import consts from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.get_schema_response import GetSchemaStatus from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus @@ -141,7 +142,7 @@ def test_check_compliance_failed(self, mock_vc_rest_client, mock_vc_context): def test_remediate_skipped_already_desired(self, mock_vc_rest_client, mock_vc_context): # Setup Mock objects for current value already being the desired value. desired_value = 123 - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_rest_client.get_helper.return_value = desired_value mock_vc_context.vc_rest_client.return_value = mock_vc_rest_client @@ -202,3 +203,9 @@ def test_remediate_set_failed(self, mock_vc_rest_client, mock_vc_context): # Assert expected results. assert result == expected_result + + @patch('config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext') + def test_get_schema(self, mock_vc_context): + expected_result = {consts.RESULT: {}, consts.STATUS: GetSchemaStatus.SUCCESS} + result = self.controller.get_schema(mock_vc_context) + assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/sddc_manager/test_auto_rotate_config.py b/config_modules_vmware/tests/controllers/sddc_manager/test_auto_rotate_config.py index cdd709e..56ddb7f 100644 --- a/config_modules_vmware/tests/controllers/sddc_manager/test_auto_rotate_config.py +++ b/config_modules_vmware/tests/controllers/sddc_manager/test_auto_rotate_config.py @@ -386,7 +386,7 @@ def test_check_compliance_failed(self, mock_sddc_manager_rest_client, mock_sddc_ @patch('config_modules_vmware.framework.auth.contexts.sddc_manager_context.SDDCManagerContext') @patch('config_modules_vmware.framework.clients.sddc_manager.sddc_manager_rest_client.SDDCManagerRestClient') def test_remediate_skipped_already_desired(self, mock_sddc_manager_rest_client, mock_sddc_manager_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_sddc_manager_rest_client.get_base_url.return_value = self.sddc_base_url mock_sddc_manager_rest_client.get_helper.return_value = self.get_helper_values diff --git a/config_modules_vmware/tests/controllers/sddc_manager/test_backup_config.py b/config_modules_vmware/tests/controllers/sddc_manager/test_backup_config.py index a2f6126..5b54d85 100644 --- a/config_modules_vmware/tests/controllers/sddc_manager/test_backup_config.py +++ b/config_modules_vmware/tests/controllers/sddc_manager/test_backup_config.py @@ -327,7 +327,7 @@ def test_check_compliance_failed(self, mock_sddc_manager_rest_client, mock_sddc_ @patch('config_modules_vmware.framework.auth.contexts.sddc_manager_context.SDDCManagerContext') @patch('config_modules_vmware.framework.clients.sddc_manager.sddc_manager_rest_client.SDDCManagerRestClient') def test_remediate_skipped_already_desired(self, mock_sddc_manager_rest_client, mock_sddc_manager_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_sddc_manager_rest_client.get_base_url.return_value = self.sddc_base_url mock_sddc_manager_rest_client.get_helper.return_value = self.get_helper_values diff --git a/config_modules_vmware/tests/controllers/sddc_manager/test_depot_config.py b/config_modules_vmware/tests/controllers/sddc_manager/test_depot_config.py index feaa73f..55d9d86 100644 --- a/config_modules_vmware/tests/controllers/sddc_manager/test_depot_config.py +++ b/config_modules_vmware/tests/controllers/sddc_manager/test_depot_config.py @@ -135,7 +135,7 @@ def test_check_compliance_failed(self, mock_sddc_manager_rest_client, mock_sddc_ @patch('config_modules_vmware.framework.auth.contexts.sddc_manager_context.SDDCManagerContext') @patch('config_modules_vmware.framework.clients.sddc_manager.sddc_manager_rest_client.SDDCManagerRestClient') def test_remediate_skipped_already_desired(self, mock_sddc_manager_rest_client, mock_sddc_manager_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_sddc_manager_rest_client.get_base_url.return_value = self.sddc_base_url mock_sddc_manager_rest_client.get_helper.return_value = self.get_helper_values diff --git a/config_modules_vmware/tests/controllers/sddc_manager/test_dns_config.py b/config_modules_vmware/tests/controllers/sddc_manager/test_dns_config.py index b8e74d4..520678f 100644 --- a/config_modules_vmware/tests/controllers/sddc_manager/test_dns_config.py +++ b/config_modules_vmware/tests/controllers/sddc_manager/test_dns_config.py @@ -152,7 +152,7 @@ def test_check_compliance_failed(self, mock_sddc_manager_rest_client, mock_sddc_ @patch('config_modules_vmware.framework.auth.contexts.sddc_manager_context.SDDCManagerContext') @patch('config_modules_vmware.framework.clients.sddc_manager.sddc_manager_rest_client.SDDCManagerRestClient') def test_remediate_skipped_already_desired(self, mock_sddc_manager_rest_client, mock_sddc_manager_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_sddc_manager_rest_client.get_base_url.return_value = self.sddc_base_url mock_sddc_manager_rest_client.get_helper.return_value = self.get_helper_values diff --git a/config_modules_vmware/tests/controllers/sddc_manager/test_ntp_config.py b/config_modules_vmware/tests/controllers/sddc_manager/test_ntp_config.py index 35da60a..ed1e43f 100644 --- a/config_modules_vmware/tests/controllers/sddc_manager/test_ntp_config.py +++ b/config_modules_vmware/tests/controllers/sddc_manager/test_ntp_config.py @@ -140,7 +140,7 @@ def test_check_compliance_failed(self, mock_sddc_manager_rest_client, mock_sddc_ @patch('config_modules_vmware.framework.auth.contexts.sddc_manager_context.SDDCManagerContext') @patch('config_modules_vmware.framework.clients.sddc_manager.sddc_manager_rest_client.SDDCManagerRestClient') def test_remediate_skipped_already_desired(self, mock_sddc_manager_rest_client, mock_sddc_manager_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_sddc_manager_rest_client.get_base_url.return_value = self.sddc_base_url mock_sddc_manager_rest_client.get_helper.side_effect = [self.get_helper_values] diff --git a/config_modules_vmware/tests/controllers/sddc_manager/test_proxy_config.py b/config_modules_vmware/tests/controllers/sddc_manager/test_proxy_config.py index df4cd50..40fa219 100644 --- a/config_modules_vmware/tests/controllers/sddc_manager/test_proxy_config.py +++ b/config_modules_vmware/tests/controllers/sddc_manager/test_proxy_config.py @@ -211,7 +211,7 @@ def test_check_compliance_failed_file(self, mock_open, mock_sddc_manager_context @patch('config_modules_vmware.framework.auth.contexts.sddc_manager_context.SDDCManagerContext') @patch('config_modules_vmware.framework.clients.sddc_manager.sddc_manager_rest_client.SDDCManagerRestClient') def test_remediate_skipped_already_desired_api(self, mock_sddc_manager_rest_client, mock_sddc_manager_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_sddc_manager_rest_client.get_base_url.return_value = self.sddc_base_url mock_sddc_manager_rest_client.get_helper.side_effect = [self.get_helper_values] @@ -224,7 +224,7 @@ def test_remediate_skipped_already_desired_api(self, mock_sddc_manager_rest_clie @patch('config_modules_vmware.framework.auth.contexts.sddc_manager_context.SDDCManagerContext') @patch("builtins.open", new_callable=mock_open, read_data="lcm.depot.adapter.proxyEnabled=false\nlcm.depot.adapter.proxyHost=10.0.0.250\nlcm.depot.adapter.proxyPort=3128") def test_remediate_skipped_already_desired_file(self, mock_open, mock_sddc_manager_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_sddc_manager_context.product_version = VCF_4_X_VERSION result = self.controller.remediate(mock_sddc_manager_context, self.compliant_values) diff --git a/config_modules_vmware/tests/controllers/vcenter/test_alarm_remote_syslog_failure_config.py b/config_modules_vmware/tests/controllers/vcenter/test_alarm_remote_syslog_failure_config.py index 0d8f4de..726a729 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_alarm_remote_syslog_failure_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_alarm_remote_syslog_failure_config.py @@ -11,14 +11,21 @@ class TestAlarmRemoteSyslogFailureConfig: - mock_alarm_def = MagicMock() - def setup_method(self): self.controller = AlarmRemoteSyslogFailureConfig() - self.mock_alarm_def.info.name = "Mocked Alarm" - self.mock_alarm_def.info.description = "Mocked Alarm Description" - self.mock_alarm_def.info.enabled = True - self.mock_alarm_def.info.actionFrequency = 60 + + self.mock_alarm_def1 = MagicMock() + self.mock_alarm_def1.info.name = "Mocked Alarm1" + self.mock_alarm_def1.info.description = "Mocked Alarm with expression eventType esx.problem.vmsyslogd.remote.failure" + self.mock_alarm_def1.info.enabled = True + self.mock_alarm_def1.info.actionFrequency = 60 + + self.mock_alarm_def2 = MagicMock() + self.mock_alarm_def2.info.name = "Mocked Alarm2" + self.mock_alarm_def2.info.description = "Mocked Alarm with no expression" + self.mock_alarm_def2.info.enabled = True + self.mock_alarm_def2.info.actionFrequency = 60 + self.mock_alarm_def2.info.expression = None mock_expression = MagicMock() mock_expression.status = "red" @@ -31,7 +38,7 @@ def setup_method(self): mock_comparison.operator = 'endsWith' mock_comparison.value = 'A' mock_expression.comparisons = [mock_comparison] - self.mock_alarm_def.info.expression.expression = [mock_expression] + self.mock_alarm_def1.info.expression.expression = [mock_expression] mock_email_action = MagicMock() mock_email_action.transitionSpecs[0].repeats = True @@ -60,19 +67,19 @@ def setup_method(self): mock_script_action.transitionSpecs[0].finalState = "green" mock_script_action.action.script = "Mocked Script" - self.mock_alarm_def.info.action.action = [mock_email_action, mock_snmp_action, mock_script_action] + self.mock_alarm_def1.info.action.action = [mock_email_action, mock_snmp_action, mock_script_action] self.mock_vc_context = MagicMock() self.mock_alarm_manager = MagicMock() - self.mock_alarm_manager.GetAlarm.return_value = [self.mock_alarm_def] + self.mock_alarm_manager.GetAlarm.return_value = [self.mock_alarm_def1, self.mock_alarm_def2] self.mock_vc_context.vc_vmomi_client().content.alarmManager = self.mock_alarm_manager self.expected_alarms = [{ - 'alarm_name': 'Mocked Alarm', - 'alarm_description': 'Mocked Alarm Description', + 'alarm_name': 'Mocked Alarm1', + 'alarm_description': 'Mocked Alarm with expression eventType esx.problem.vmsyslogd.remote.failure', 'enabled': True, 'target_type': 'VCENTER', 'rule_expressions': [ @@ -134,7 +141,7 @@ def test_set_failed_exception(self): self.mock_vc_context.vc_vmomi_client.side_effect = Exception("Test exception") status, errors = self.controller.set(self.mock_vc_context, self.expected_alarms) assert status == RemediateStatus.FAILED - assert errors == ["Error during Mocked Alarm creation with error Test exception."] + assert errors == ["Error during Mocked Alarm1 creation with error Test exception."] def test_set_failed_exception_duplicate_name(self): mock_vc_context = MagicMock() @@ -143,7 +150,7 @@ def test_set_failed_exception_duplicate_name(self): mock_alarm_manager.CreateAlarm.side_effect = vim.fault.DuplicateName status, errors = self.controller.set(mock_vc_context, self.expected_alarms) assert status == RemediateStatus.FAILED - assert errors == ["An alarm with same name 'Mocked Alarm' already exists.", + assert errors == ["An alarm with same name 'Mocked Alarm1' already exists.", 'Manual remediation required for an update or deletion.', 'Please either update/delete that alarm manually or choose different alarm name.'] @@ -162,8 +169,8 @@ def test_check_compliance_non_compliant(self, mock_get_target_type): result = self.controller.check_compliance(self.mock_vc_context, self.expected_alarms) expected_result = { consts.STATUS: ComplianceStatus.NON_COMPLIANT, - consts.CURRENT: [{'enabled': True, 'alarm_name': 'Mocked Alarm'}], - consts.DESIRED: [{'enabled': False, 'alarm_name': 'Mocked Alarm'}] + consts.CURRENT: [{'enabled': True, 'alarm_name': 'Mocked Alarm1'}], + consts.DESIRED: [{'enabled': False, 'alarm_name': 'Mocked Alarm1'}] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/vcenter/test_alarm_sso_config.py b/config_modules_vmware/tests/controllers/vcenter/test_alarm_sso_config.py index 7d5d6a9..d47b684 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_alarm_sso_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_alarm_sso_config.py @@ -12,14 +12,21 @@ class TestAlarmSSOConfig: - mock_alarm_def = MagicMock() - def setup_method(self): self.controller = AlarmSSOConfig() - self.mock_alarm_def.info.name = "Mocked Alarm" - self.mock_alarm_def.info.description = "Mocked Alarm Description" - self.mock_alarm_def.info.enabled = True - self.mock_alarm_def.info.actionFrequency = 60 + + self.mock_alarm_def1 = MagicMock() + self.mock_alarm_def1.info.name = "Mocked Alarm1" + self.mock_alarm_def1.info.description = "Mocked Alarm with expression eventType com.vmware.sso.PrincipalManagement" + self.mock_alarm_def1.info.enabled = True + self.mock_alarm_def1.info.actionFrequency = 60 + + self.mock_alarm_def2 = MagicMock() + self.mock_alarm_def2.info.name = "Mocked Alarm2" + self.mock_alarm_def2.info.description = "Mocked Alarm with no expression" + self.mock_alarm_def2.info.enabled = True + self.mock_alarm_def2.info.actionFrequency = 60 + self.mock_alarm_def2.info.expression = None mock_expression = MagicMock() mock_expression.status = "red" @@ -32,7 +39,7 @@ def setup_method(self): mock_comparison.operator = 'startsWith' mock_comparison.value = 'A' mock_expression.comparisons = [mock_comparison] - self.mock_alarm_def.info.expression.expression = [mock_expression] + self.mock_alarm_def1.info.expression.expression = [mock_expression] mock_email_action = MagicMock() mock_email_action.transitionSpecs[0].repeats = True @@ -61,19 +68,19 @@ def setup_method(self): mock_script_action.transitionSpecs[0].finalState = "green" mock_script_action.action.script = "Mocked Script" - self.mock_alarm_def.info.action.action = [mock_email_action, mock_snmp_action, mock_script_action] + self.mock_alarm_def1.info.action.action = [mock_email_action, mock_snmp_action, mock_script_action] self.mock_vc_context = MagicMock() self.mock_alarm_manager = MagicMock() - self.mock_alarm_manager.GetAlarm.return_value = [self.mock_alarm_def] + self.mock_alarm_manager.GetAlarm.return_value = [self.mock_alarm_def1, self.mock_alarm_def2] self.mock_vc_context.vc_vmomi_client().content.alarmManager = self.mock_alarm_manager self.expected_alarms = [{ - 'alarm_name': 'Mocked Alarm', - 'alarm_description': 'Mocked Alarm Description', + 'alarm_name': 'Mocked Alarm1', + 'alarm_description': 'Mocked Alarm with expression eventType com.vmware.sso.PrincipalManagement', 'enabled': True, 'target_type': 'VCENTER', 'rule_expressions': [ @@ -135,7 +142,7 @@ def test_set_failed_exception(self): self.mock_vc_context.vc_vmomi_client.side_effect = Exception("Test exception") status, errors = self.controller.set(self.mock_vc_context, self.expected_alarms) assert status == RemediateStatus.FAILED - assert errors == ["Error during Mocked Alarm creation with error Test exception."] + assert errors == ["Error during Mocked Alarm1 creation with error Test exception."] def test_set_failed_exception_duplicate_name(self): mock_vc_context = MagicMock() @@ -144,7 +151,7 @@ def test_set_failed_exception_duplicate_name(self): mock_alarm_manager.CreateAlarm.side_effect = vim.fault.DuplicateName status, errors = self.controller.set(mock_vc_context, self.expected_alarms) assert status == RemediateStatus.FAILED - assert errors == ["An alarm with same name 'Mocked Alarm' already exists.", + assert errors == ["An alarm with same name 'Mocked Alarm1' already exists.", 'Manual remediation required for an update or deletion.', 'Please either update/delete that alarm manually or choose different alarm name.'] @@ -163,8 +170,8 @@ def test_check_compliance_non_compliant(self, mock_get_target_type): result = self.controller.check_compliance(self.mock_vc_context, self.expected_alarms) expected_result = { consts.STATUS: ComplianceStatus.NON_COMPLIANT, - consts.CURRENT: [{'enabled': True, 'alarm_name': 'Mocked Alarm'}], - consts.DESIRED: [{'enabled': False, 'alarm_name': 'Mocked Alarm'}] + consts.CURRENT: [{'enabled': True, 'alarm_name': 'Mocked Alarm1'}], + consts.DESIRED: [{'enabled': False, 'alarm_name': 'Mocked Alarm1'}] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/vcenter/test_backup_schedule_config.py b/config_modules_vmware/tests/controllers/vcenter/test_backup_schedule_config.py index ff89135..b2e2726 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_backup_schedule_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_backup_schedule_config.py @@ -251,7 +251,7 @@ def test_check_compliance_failed(self, mock_vc_rest_client, mock_vc_context): @patch('config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext') @patch('config_modules_vmware.framework.clients.vcenter.vc_rest_client.VcRestClient') def test_remediate_skipped_already_desired(self, mock_vc_rest_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_rest_client.get_base_url.return_value = self.vc_base_url mock_vc_rest_client.get_helper.return_value = self.rest_api_get_compliant_daily_schedule diff --git a/config_modules_vmware/tests/controllers/vcenter/test_cert_config.py b/config_modules_vmware/tests/controllers/vcenter/test_cert_config.py index d805498..8e96d40 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_cert_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_cert_config.py @@ -69,7 +69,7 @@ def test_get_no_issuer(self, mock_vc_rest_client, mock_vc_context): def test_set_skipped(self, mock_vc_rest_client, mock_vc_context): # Setup Mock objects for successfully changing the value. current_value = ["OU=VMwareEngineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CA"] - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ["Set is not implemented as this control requires manual intervention"]} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.REMEDIATION_SKIPPED_MESSAGE]} mock_vc_rest_client.get_helper.return_value = current_value mock_vc_context.vc_rest_client.return_value = mock_vc_rest_client @@ -86,8 +86,7 @@ def test_remediate_skipped(self, mock_vc_rest_client, mock_vc_context): # Setup Mock objects for successfully changing the value. current_value = ["OU=VMwareEngineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CA"] - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [ - "Set is not implemented as this control requires manual intervention"], + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.REMEDIATION_SKIPPED_MESSAGE], consts.DESIRED: self.control_desired_value, consts.CURRENT: self.non_compliant_display_value} mock_vc_rest_client.get_base_url.return_value = self.vc_base_url diff --git a/config_modules_vmware/tests/controllers/vcenter/test_datastore_unique_name_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_datastore_unique_name_policy.py index 6d804db..ce8bd1a 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_datastore_unique_name_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_datastore_unique_name_policy.py @@ -167,7 +167,7 @@ def test_remediate_success(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = self.compliant_datastore_mocks mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dns_config.py b/config_modules_vmware/tests/controllers/vcenter/test_dns_config.py index c90c187..6c9f013 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dns_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dns_config.py @@ -135,7 +135,7 @@ def test_check_compliance_failed(self, mock_vc_rest_client, mock_vc_context): @patch('config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext') @patch('config_modules_vmware.framework.clients.vcenter.vc_rest_client.VcRestClient') def test_remediate_skipped_already_desired(self, mock_vc_rest_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_rest_client.get_base_url.return_value = self.vc_base_url mock_vc_rest_client.get_helper.return_value = self.compliant_value diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_forged_transmits_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_forged_transmits_policy.py index 90a96fb..54db36d 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_forged_transmits_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_forged_transmits_policy.py @@ -159,7 +159,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): expected_get_object_result = self.compliant_dv_pg_pyvmomi_mocks - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = expected_get_object_result mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_mac_address_change_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_mac_address_change_policy.py index c3a7649..f67b267 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_mac_address_change_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_mac_address_change_policy.py @@ -203,7 +203,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): expected_get_object_result = self.compliant_dv_pg_pyvmomi_mocks - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = expected_get_object_result mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_native_vlan_exclusion_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_native_vlan_exclusion_policy.py index 9a32765..c49c78b 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_native_vlan_exclusion_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_native_vlan_exclusion_policy.py @@ -194,7 +194,7 @@ def test_set_success(self, mock_vc_vmomi_client, mock_vc_context): result, errors = self.controller.set(mock_vc_context, self.compliant_value) assert result == RemediateStatus.SKIPPED - assert errors == ['Remediation is not implemented as this control requires manual intervention.'] + assert errors == [consts.REMEDIATION_SKIPPED_MESSAGE] @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") @@ -264,7 +264,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): expected_get_object_result = self.compliant_dv_pg_pyvmomi_mocks - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = expected_get_object_result mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client @@ -301,8 +301,7 @@ def test_remediate_success(self, mock_vc_vmomi_client, mock_vc_context): } ] expected_result = { - 'errors': ['Remediation is not implemented as this control requires manual ' - 'intervention.'], + 'errors': [consts.REMEDIATION_SKIPPED_MESSAGE], consts.STATUS: RemediateStatus.SKIPPED, consts.DESIRED: self.compliant_value, consts.CURRENT: non_compliant_configs diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_promiscuous_mode_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_promiscuous_mode_policy.py index 4403d28..5ee0145 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_promiscuous_mode_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_promiscuous_mode_policy.py @@ -155,7 +155,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = self.compliant_dv_pg_mock_pyvmomi mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_reserved_vlan_exclusion_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_reserved_vlan_exclusion_policy.py index e8b7acb..928668d 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_reserved_vlan_exclusion_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_reserved_vlan_exclusion_policy.py @@ -194,7 +194,7 @@ def test_set_success(self, mock_vc_vmomi_client, mock_vc_context): result, errors = self.controller.set(mock_vc_context, self.compliant_value) assert result == RemediateStatus.SKIPPED - assert errors == ['Remediation is not implemented as this control requires manual intervention.'] + assert errors == [consts.REMEDIATION_SKIPPED_MESSAGE] @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") @@ -264,7 +264,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): expected_get_object_result = self.compliant_dv_pg_pyvmomi_mocks - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = expected_get_object_result mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client @@ -301,8 +301,7 @@ def test_remediate_success(self, mock_vc_vmomi_client, mock_vc_context): } ] expected_result = { - 'errors': ['Remediation is not implemented as this control requires manual ' - 'intervention.'], + 'errors': [consts.REMEDIATION_SKIPPED_MESSAGE], consts.STATUS: RemediateStatus.SKIPPED, consts.DESIRED: self.compliant_value, consts.CURRENT: non_compliant_configs diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_vlan_trunking_authorized.py b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_vlan_trunking_authorized.py index af0e6ba..a2ecb12 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_vlan_trunking_authorized.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dv_pg_vlan_trunking_authorized.py @@ -256,8 +256,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): def test_remediate_success(self, mock_vc_vmomi_client, mock_vc_context): current_value = self.non_compliant_dv_pg_pyvmomi_mocks expected_result = { - 'errors': ['Remediation is not implemented as this control requires manual ' - 'intervention.'], + 'errors': [consts.REMEDIATION_SKIPPED_MESSAGE], consts.STATUS: RemediateStatus.SKIPPED, consts.DESIRED: self.compliant_value, consts.CURRENT: self.non_compliant_get_values diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dvs_health_check_config.py b/config_modules_vmware/tests/controllers/vcenter/test_dvs_health_check_config.py index bc4f290..a542f98 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dvs_health_check_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dvs_health_check_config.py @@ -184,7 +184,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = self.compliant_dvs_mocks mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dvs_network_io_control_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_dvs_network_io_control_policy.py index 2e555df..67ff508 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dvs_network_io_control_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dvs_network_io_control_policy.py @@ -150,7 +150,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = self.compliant_dvs_mocks mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_dvs_pg_netflow_config.py b/config_modules_vmware/tests/controllers/vcenter/test_dvs_pg_netflow_config.py index 1306268..7a8937a 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_dvs_pg_netflow_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_dvs_pg_netflow_config.py @@ -306,7 +306,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): expected_get_object_result = self.compliant_dvs_pyvmomi_mocks - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = expected_get_object_result mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_h5_client_session_timeout_config.py b/config_modules_vmware/tests/controllers/vcenter/test_h5_client_session_timeout_config.py index 464d92a..c9de8c2 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_h5_client_session_timeout_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_h5_client_session_timeout_config.py @@ -188,7 +188,7 @@ def test_check_compliance_failed(self, mock_execute_shell_cmd, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.utils.utils.run_shell_cmd") def test_remediate_skipped_already_desired(self, mock_execute_shell_cmd, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_execute_shell_cmd.return_value = self.compliant_shell_cmd_return_val diff --git a/config_modules_vmware/tests/controllers/vcenter/test_ip_based_storage_port_group_config.py b/config_modules_vmware/tests/controllers/vcenter/test_ip_based_storage_port_group_config.py new file mode 100644 index 0000000..a42ef4e --- /dev/null +++ b/config_modules_vmware/tests/controllers/vcenter/test_ip_based_storage_port_group_config.py @@ -0,0 +1,615 @@ +import json +from typing import List + +from mock import MagicMock +from mock import patch +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config import ALLOW_MIX_TRAFFIC_TYPE +from config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config import GLOBAL +from config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config import IPBasedStoragePortGroupConfig +from config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config import IS_DEDICATED_VLAN +from config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config import OVERRIDES +from config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config import PORT_GROUP_NAME +from config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config import SWITCH_NAME +from config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config import VLAN_INFO +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + + +class TestIPBasedStoragePortGroupConfig: + def setup_method(self): + self.controller = IPBasedStoragePortGroupConfig() + self.desired_configs = { + GLOBAL: { + IS_DEDICATED_VLAN: True, + ALLOW_MIX_TRAFFIC_TYPE: False + }, + OVERRIDES: [ + { + ALLOW_MIX_TRAFFIC_TYPE: False, + SWITCH_NAME: "Switch1", + PORT_GROUP_NAME: "PortGroup1", + IS_DEDICATED_VLAN: True + }, + { + SWITCH_NAME: "Switch1", + PORT_GROUP_NAME: "PortGroup2", + IS_DEDICATED_VLAN: True + } + ] + } + self.desired_configs2 = { + GLOBAL: { + IS_DEDICATED_VLAN: True + }, + OVERRIDES: [ + { + IS_DEDICATED_VLAN: False, + SWITCH_NAME: "Switch1", + PORT_GROUP_NAME: "PortGroup1" + }, + { + SWITCH_NAME: "Switch1", + PORT_GROUP_NAME: "PortGroup2", + IS_DEDICATED_VLAN: True + } + ] + } + self.incorrect_desired_configs = { + GLOBAL: { + IS_DEDICATED_VLAN: True + }, + OVERRIDES: [ + { + IS_DEDICATED_VLAN: False, + SWITCH_NAME: "Switch3", + PORT_GROUP_NAME: "PortGroup5" + }, + { + SWITCH_NAME: "Switch1", + PORT_GROUP_NAME: "PortGroup2", + IS_DEDICATED_VLAN: True + } + ] + } + self.current_vsan_portgroups = [ + { + "switch_name": "Switch1", + "port_group_name": "PortGroup1", + "is_dedicated_vlan": True, + "ports": [ + { + "host_name": "host1", + "device": "vmk1", + "services": ["vsan"] + }, + { + "host_name": "host2", + "device": "vmk2", + "services": ["vsan"] + } + ] + }, + { + "switch_name": "Switch1", + "port_group_name": "PortGroup2", + "is_dedicated_vlan": True, + "ports": [ + { + "host_name": "host1", + "device": "vmk3", + "services": ["vsan"] + }, + { + "host_name": "host2", + "device": "vmk4", + "services": ["vmotion"] + } + ] + } + ] + self.current_vsan_portgroups_all_traffic = [ + { + "switch_name": "Switch1", + "port_group_name": "PortGroup1", + "is_dedicated_vlan": True, + "ports": [ + { + "host_name": "host1", + "device": "vmk1", + "services": ["vmotion"] + }, + { + "host_name": "host2", + "device": "vmk2", + "services": ["vmotion"] + } + ] + }, + { + "switch_name": "Switch1", + "port_group_name": "PortGroup2", + "is_dedicated_vlan": True, + "ports": [ + { + "host_name": "host1", + "device": "vmk3", + "services": ["vsan"] + }, + { + "host_name": "host2", + "device": "vmk4", + "services": ["vmotion"] + } + ] + } + ] + self.current_vsan_portgroups_skip_uplink = [ + { + "switch_name": "Switch1", + "port_group_name": "PortGroup1", + "is_dedicated_vlan": True, + "ports": [ + { + "host_name": "host1", + "device": "vmk1", + "services": ["vsan"] + }, + { + "host_name": "host2", + "device": "vmk2", + "services": ["vsan"] + } + ] + } + ] + self.context_mock = MagicMock() + self.dv_pgs = [ + { + "switch_name": "Switch1", + "port_group_name": "PortGroup1", + "vlan": 5, + }, + { + "switch_name": "Switch1", + "port_group_name": "PortGroup2", + "vlan": 6, + }, + { + "switch_name": "Switch1", + "port_group_name": "PortGroup3", + "pvlanId": 300, + }, + { + "switch_name": "Switch1", + "port_group_name": "PortGroup4", + "vlan": ["10-100", "105-110", "200-250"], + } + ] + + dvs_1_mock = MagicMock() + dvs_1_mock.portgroup = [self.get_dv_port_group_mock_obj(pg_spec) for pg_spec in self.dv_pgs] + dvs_1_mock.name = 'Switch1' + self.dvs = dvs_1_mock + + self.port_group_1_ports = [self.get_port_mock_obj(host_name='host1', device_name='vmk1'), + self.get_port_mock_obj(host_name='host2', device_name='vmk2')] + self.port_group_2_ports = [self.get_port_mock_obj(host_name='host1', device_name='vmk3'), + self.get_port_mock_obj(host_name='host2', device_name='vmk4')] + self.port_group_3_ports = [self.get_port_mock_obj(host_name='host1', device_name='vmk5')] + self.port_group_4_ports = [self.get_port_mock_obj(host_name='host1', device_name='vmk6', connectee=False)] + self.ports = self.port_group_1_ports + self.context_mock.vc_vmomi_client.return_value.get_objects_by_vimtype.return_value = [dvs_1_mock] + self.nfs_portgroup_compliant_obj = self.get_port_group_mock_obj(dvs_1_mock.portgroup, "Switch1", "PortGroup1") + self.nfs_portgroup_non_compliant_obj = self.get_port_group_mock_obj(dvs_1_mock.portgroup, "Switch1", "PortGroup3") + self.portgroup_2_obj = self.get_port_group_mock_obj(dvs_1_mock.portgroup, "Switch1", "PortGroup2") + self.iscsi_compliant_vmknics = ["vmk3", "vmk4"] + self.iscsi_non_compliant_vmknics = ["vmk5"] + self.vsan_cluster_configs = [ + { + "datacenter_name": "SDDC-Datacenter", + "cluster_name": "SDDC-Cluster-1", + }, + { + "datacenter_name": "SDDCt-Datacenter", + "cluster_name": "SDDC-Cluster-2", + }, + { + "datacenter_name": "test-Datacenter", + "cluster_name": "SDDC-Cluster-1", + } + ] + self.all_vsan_enabled_mock_cluster_refs = \ + self.create_mock_objs_all_vsan_clusters(self.vsan_cluster_configs) + + def get_port_group_mock_obj(self, portgroup_objs, switch_name, port_group_name): + for port_group_obj in portgroup_objs: + if port_group_obj.name == port_group_name and port_group_obj.config.distributedVirtualSwitch.name == switch_name: + return port_group_obj + return None + + def get_dv_port_group_mock_obj(self, pg_spec, create_bad_mock=False): + """ + Create mock object for DV port group based on port group spec + """ + dv_pg_mock = MagicMock() + dv_pg_mock.name = pg_spec.get("port_group_name") + dv_pg_mock.portKeys = pg_spec.get("port_group_name") + dv_pg_mock.config = MagicMock() + dv_pg_mock.config.uplink = False + dv_pg_mock.config.defaultPortConfig = vim.dvs.VmwareDistributedVirtualSwitch.VmwarePortConfigPolicy() + vlan_config = pg_spec.get("vlan") + + if isinstance(vlan_config, List): + dv_pg_mock.config.defaultPortConfig.vlan = vim.dvs.VmwareDistributedVirtualSwitch.TrunkVlanSpec() + vlan_ranges = [] + for vlan in vlan_config: + if "-" in vlan: + ranges = vlan.split("-") + start = int(ranges[0]) + end = int(ranges[1]) + vlan_range = vim.NumericRange(start=start, end=end) + vlan_ranges.append(vlan_range) + else: + vlan_id = vim.NumericRange(start=int(vlan), end=int(vlan)) + vlan_ranges.append(vlan_id) + # for vlan in vlan_ranges: + dv_pg_mock.config.defaultPortConfig.vlan.vlanId = vlan_ranges + elif isinstance(vlan_config, int): + dv_pg_mock.config.defaultPortConfig.vlan = vim.dvs.VmwareDistributedVirtualSwitch.VlanIdSpec() + dv_pg_mock.config.defaultPortConfig.vlan.vlanId = vlan_config + else: + pvt_vlan_config = pg_spec.get("pvlanId") + dv_pg_mock.config.defaultPortConfig.vlan = vim.dvs.VmwareDistributedVirtualSwitch.PvlanSpec() + dv_pg_mock.config.defaultPortConfig.vlan.pvlanId = pvt_vlan_config + + if not create_bad_mock: + dv_pg_mock.config.configVersion = "24" + dv_pg_mock.config.distributedVirtualSwitch.name = pg_spec.get("switch_name") + return dv_pg_mock + + def get_port_mock_obj(self, host_name, device_name, connectee=True): + mock_port = vim.dvs.DistributedVirtualPort() + if connectee: + connectee_mock = vim.dvs.PortConnectee() + host_mock = MagicMock(spec=vim.HostSystem) + host_mock.name = host_name + connectee_mock.connectedEntity = host_mock + connectee_mock.nicKey = device_name + else: + connectee_mock = None + mock_port.connectee = connectee_mock + return mock_port + + def create_mock_objs_all_vsan_clusters(self, cluster_specs): + """ + Create pyvmomi like mock object for all clusters + :param cluster_specs: + :return: + """ + all_vsan_cluster_refs = [] + for cluster_spec in cluster_specs: + cluster_ref = MagicMock() + cluster_ref.name = cluster_spec.get("cluster_name") + cluster_ref.parent.parent.name = cluster_spec.get("datacenter_name") + all_vsan_cluster_refs.append(cluster_ref) + return all_vsan_cluster_refs + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_enabled_services") + def test_get_success(self, mock_get_enabled_services, mock_get_dv_ports): + mock_get_enabled_services.side_effect = [['vsan'], ['vsan'], + ['vsan'], ['vmotion'], + ['vmotion'], ['management']] + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_2_ports, self.port_group_3_ports, self.port_group_4_ports] + result, errors = self.controller.get(self.context_mock) + assert result == self.current_vsan_portgroups + assert errors == [] + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_enabled_services") + def test_get_success_skip_uplink(self, mock_get_enabled_services, mock_get_dv_ports): + mock_get_enabled_services.side_effect = [['vsan'], ['vsan'], + ['vmotion'], ['vmotion'], + ['vmotion'], ['management']] + self.portgroup_2_obj.config.uplink = True + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_3_ports, self.port_group_4_ports] + result, errors = self.controller.get(self.context_mock) + assert result == self.current_vsan_portgroups_skip_uplink + assert errors == [] + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_enabled_services") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_iscsi_vmknics") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_nfs_networks") + def test_get_success_all_traffic(self, mock_get_nfs_networks, mock_get_iscsi_vmknics, mock_get_enabled_services, mock_get_dv_ports): + mock_get_enabled_services.side_effect = [['vmotion'], ['vmotion'], + ['vsan'], ['vmotion'], + ['vmotion'], ['management']] + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_2_ports, self.port_group_3_ports, self.port_group_4_ports] + mock_get_nfs_networks.return_value = [self.nfs_portgroup_compliant_obj] + mock_get_iscsi_vmknics.return_value = self.iscsi_compliant_vmknics + result, errors = self.controller.get(self.context_mock) + assert result == self.current_vsan_portgroups_all_traffic + assert errors == [] + + @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") + def test_get_failed(self, mock_vc_context): + mock_vc_context.vc_vmomi_client.side_effect = Exception("Test exception") + + result, errors = self.controller.get(mock_vc_context) + assert result == [] + assert errors == ["Test exception"] + + @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") + def test_set_skipped(self, mock_vc_context): + status, errors = self.controller.set(mock_vc_context, self.desired_configs) + + # Assert expected results. + assert status == RemediateStatus.SKIPPED + assert errors == [consts.REMEDIATION_SKIPPED_MESSAGE] + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config." + "IPBasedStoragePortGroupConfig._get_enabled_services") + def test_check_compliance_compliant(self, mock_enabled_services, mock_get_dv_ports): + mock_enabled_services.side_effect = [['vsan'], ['vsan'], + ['vmotion'], ['vmotion'], + ['vmotion'], ['management']] + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_2_ports, self.port_group_3_ports, self.port_group_4_ports] + result = self.controller.check_compliance(self.context_mock, self.desired_configs) + expected_result = {consts.STATUS: ComplianceStatus.COMPLIANT} + assert result == expected_result + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config." + "IPBasedStoragePortGroupConfig._get_enabled_services") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_iscsi_vmknics") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_nfs_networks") + def test_check_compliance_compliant_all_traffic(self, mock_get_nfs_networks, mock_get_iscsi_vmknics, mock_enabled_services, mock_get_dv_ports): + mock_enabled_services.side_effect = [['vsan'], ['vsan'], + ['vsan'], ['vsan'], + ['vmotion'], ['management']] + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_2_ports, self.port_group_3_ports, self.port_group_4_ports] + mock_get_nfs_networks.return_value = [self.nfs_portgroup_compliant_obj] + mock_get_iscsi_vmknics.return_value = self.iscsi_compliant_vmknics + result = self.controller.check_compliance(self.context_mock, self.desired_configs) + expected_result = {consts.STATUS: ComplianceStatus.COMPLIANT} + assert result == expected_result + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config." + "IPBasedStoragePortGroupConfig._get_enabled_services") + def test_check_compliance_non_compliant(self, mock_enabled_services, mock_get_dv_ports): + mock_enabled_services.side_effect = [['vsan'], ['vsan'], ['vsan'], ['vmotion'], ['vmotion'], ['management']] + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_2_ports, self.port_group_3_ports, self.port_group_4_ports] + result = self.controller.check_compliance(self.context_mock, self.desired_configs) + expected_result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: [ + { + 'ports': [{'host_name': 'host2', 'device': 'vmk4', 'services': ['vmotion']}], + 'switch_name': 'Switch1', + 'port_group_name': 'PortGroup2', + 'allow_mix_traffic_type': False, + } + ], + consts.DESIRED: [ + { + '__GLOBAL__': { + 'is_dedicated_vlan': True, + 'allow_mix_traffic_type': False, + }, + }, + { + 'switch_name': 'Switch1', + 'port_group_name': 'PortGroup2', + 'allow_mix_traffic_type': False, + } + ] + } + assert result == expected_result + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config." + "IPBasedStoragePortGroupConfig._get_enabled_services") + def test_check_compliance_non_compliant2(self, mock_enabled_services, mock_get_dv_ports): + mock_enabled_services.side_effect = [['vsan'], ['vsan'], ['vsan'], ['vmotion'], ['vmotion'], ['management']] + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_2_ports, self.port_group_3_ports, self.port_group_4_ports] + result = self.controller.check_compliance(self.context_mock, self.desired_configs2) + expected_result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: [ + { + 'is_dedicated_vlan': True, + 'port_group_name': 'PortGroup1', + 'switch_name': 'Switch1', + } + ], + consts.DESIRED: [ + { + '__GLOBAL__': { + 'is_dedicated_vlan': True, + }, + }, + { + 'is_dedicated_vlan': False, + 'port_group_name': 'PortGroup1', + 'switch_name': 'Switch1', + } + ] + } + assert result == expected_result + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config." + "IPBasedStoragePortGroupConfig._get_enabled_services") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_iscsi_vmknics") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_nfs_networks") + def test_check_compliance_non_compliant_all_traffic(self, mock_get_nfs_networks, mock_get_iscsi_vmknics, mock_enabled_services, mock_get_dv_ports): + mock_enabled_services.side_effect = [['vsan'], ['vsan'], ['vsan'], ['vmotion'], ['vmotion'], ['management']] + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_2_ports, self.port_group_3_ports, self.port_group_4_ports] + mock_get_nfs_networks.return_value = [self.nfs_portgroup_non_compliant_obj] + mock_get_iscsi_vmknics.return_value = self.iscsi_non_compliant_vmknics + result = self.controller.check_compliance(self.context_mock, self.desired_configs) + expected_result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: [ + { + 'ports': [{'host_name': 'host2', 'device': 'vmk4', 'services': ['vmotion']}], + 'allow_mix_traffic_type': False, + 'switch_name': 'Switch1', + 'port_group_name': 'PortGroup2' + }, + { + 'is_dedicated_vlan': False, + 'ports': [{'host_name': 'host1', 'device': 'vmk5', 'services': ['vmotion']}], + 'allow_mix_traffic_type': False, + 'switch_name': 'Switch1', + 'port_group_name': 'PortGroup3' + } + ], + consts.DESIRED: [ + { + '__GLOBAL__': { + 'is_dedicated_vlan': True, + 'allow_mix_traffic_type': False, + }, + }, + { + 'allow_mix_traffic_type': False, + 'switch_name': 'Switch1', + 'port_group_name': 'PortGroup2' + }, + ] + } + assert result == expected_result + + @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") + def test_check_compliance_failed(self, mock_vc_context): + mock_vc_context.vc_vmomi_client.side_effect = Exception("Test exception") + result = self.controller.check_compliance(mock_vc_context, self.desired_configs) + expected_result = { + consts.STATUS: ComplianceStatus.FAILED, + consts.ERRORS: ["Test exception"] + } + assert result == expected_result + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config." + "IPBasedStoragePortGroupConfig._get_enabled_services") + def test_check_compliance_failed2(self, mock_enabled_services, mock_get_dv_ports): + mock_enabled_services.side_effect = [['vsan'], ['vsan'], ['vsan'], ['vmotion'], ['vmotion'], ['management']] + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_2_ports, self.port_group_3_ports, self.port_group_4_ports] + result = self.controller.check_compliance(self.context_mock, self.incorrect_desired_configs) + expected_result = { + consts.STATUS: ComplianceStatus.FAILED, + consts.ERRORS: ["Port group provided in desired config overrides does not exist PortGroup5 in switch Switch3"] + } + assert result == expected_result + + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig._get_dv_ports") + @patch("config_modules_vmware.controllers.vcenter.ip_based_storage_port_group_config." + "IPBasedStoragePortGroupConfig._get_enabled_services") + def test_remediation_skipped(self, mock_enabled_services, mock_get_dv_ports): + mock_enabled_services.side_effect = [['vsan'], ['vsan'], ['vsan'], ['vmotion'], ['vmotion'], ['management']] + mock_get_dv_ports.side_effect = [self.port_group_1_ports, self.port_group_2_ports, self.port_group_3_ports] + result = self.controller.remediate(self.context_mock, self.desired_configs) + + # Assert expected results. + expected_result = { + consts.STATUS: RemediateStatus.SKIPPED, + consts.ERRORS: [consts.REMEDIATION_SKIPPED_MESSAGE], + } + assert result == expected_result + + def test_get_enabled_services(self): + host_mock = MagicMock(spec=vim.HostSystem) + host_mock.name = 'host1' + + vnic1_mgr_config = vim.host.VirtualNicManager.NetConfig() + vnic1_mgr_config.selectedVnic = ['VirtualNic-vmk1'] + vnic1_mgr_config.nicType = 'vsan' + vnic2_mgr_config = vim.host.VirtualNicManager.NetConfig() + vnic2_mgr_config.selectedVnic = ['VirtualNic-vmk2'] + vnic2_mgr_config.nicType = 'vmotion' + vnic3_mgr_config = vim.host.VirtualNicManager.NetConfig() + vnic3_mgr_config.selectedVnic = ['VirtualNic-vmk1'] + vnic3_mgr_config.nicType = 'management' + + host_mock.config.virtualNicManagerInfo.netConfig = [vnic1_mgr_config, vnic2_mgr_config, vnic3_mgr_config] + + device = 'vmk1' + hosts_nic_port_cache = {} + result = self.controller._get_enabled_services(host_mock, device, hosts_nic_port_cache) + assert result == ['vsan', 'management'] + assert hosts_nic_port_cache == {('host1', 'vmk1'): ['vsan', 'management'], ('host1', 'vmk2'): ['vmotion']} + + device = 'vmk2' + hosts_nic_port_cache = {} + result = self.controller._get_enabled_services(host_mock, device, hosts_nic_port_cache) + assert result == ['vmotion'] + assert hosts_nic_port_cache == {('host1', 'vmk1'): ['vsan', 'management'], ('host1', 'vmk2'): ['vmotion']} + + def test_get_enabled_services2(self): + host_mock = MagicMock(spec=vim.HostSystem) + host_mock.name = 'host1' + + vnic1_mgr_config = vim.host.VirtualNicManager.NetConfig() + vnic1_mgr_config.selectedVnic = ['VirtualNic-vmk1'] + vnic1_mgr_config.nicType = 'vsan' + vnic2_mgr_config = vim.host.VirtualNicManager.NetConfig() + vnic2_mgr_config.selectedVnic = ['VirtualNic-nonexistnic2'] + vnic2_mgr_config.nicType = 'vmotion' + + host_mock.config.virtualNicManagerInfo.netConfig = [vnic1_mgr_config, vnic2_mgr_config] + + device = 'vmk2' + hosts_nic_port_cache = {} + error = None + try: + result = self.controller._get_enabled_services(host_mock, device, hosts_nic_port_cache) + except Exception as e: + error = str(e) + + assert error == "Incorrect selected vnic!!" + + def test_get_dv_ports(self): + dvs = self.dvs + setattr(dvs, "FetchDVPorts", MagicMock(return_value=self.ports)) + result = self.controller._get_dv_ports(dvs, "port_keys") + assert result == self.ports + + @patch("config_modules_vmware.framework.clients.vcenter.vc_vsan_vmomi_client.VcVsanVmomiClient") + @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") + def test_get_iscsi_vmknics(self, mock_vc_context, mock_vc_vsan_vmomi_client): + mock_vc_vsan_vmomi_client.get_all_vsan_enabled_clusters.return_value = self.all_vsan_enabled_mock_cluster_refs + mock_vc_context.vc_vsan_vmomi_client.return_value = mock_vc_vsan_vmomi_client + iscsi_config = MagicMock() + iscsi_config.enable = True + iscsi_config.defaultConfig.networkInterface = "vmk1" + mock_vc_vsan_vmomi_client.get_vsan_iscsi_targets_config_for_cluster.return_value = iscsi_config + iscsi_target = MagicMock() + iscsi_target.networkInterface = "vmk2" + iscsi_targets = [iscsi_target] + cluster_iscsi_targets = MagicMock() + setattr(cluster_iscsi_targets, "GetIscsiTargets", MagicMock(return_value=iscsi_targets)) + mock_vc_vsan_vmomi_client.get_vsan_iscsi_targets_for_cluster.return_value = cluster_iscsi_targets + + result = self.controller._get_iscsi_vmknics(mock_vc_context) + assert result == {"vmk1", "vmk2"} + + @patch("config_modules_vmware.framework.clients.vcenter.vc_vsan_vmomi_client.VcVsanVmomiClient") + @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") + def test_get_nfs_portgroups(self, mock_vc_context, mock_vc_vsan_vmomi_client): + mock_vc_vsan_vmomi_client.get_all_vsan_enabled_clusters.return_value = self.all_vsan_enabled_mock_cluster_refs + mock_vc_context.vc_vsan_vmomi_client.return_value = mock_vc_vsan_vmomi_client + vsan_configs = MagicMock() + vsan_configs.fileServiceConfig.network = self.nfs_portgroup_compliant_obj + vsan_config_system = MagicMock() + setattr(vsan_config_system, "VsanClusterGetConfig", MagicMock(return_value=vsan_configs)) + mock_vc_vsan_vmomi_client.get_vsan_cluster_config_system.return_value = vsan_config_system + result = self.controller._get_nfs_networks(mock_vc_context) + assert result == {self.nfs_portgroup_compliant_obj} diff --git a/config_modules_vmware/tests/controllers/vcenter/test_login_banner_config.py b/config_modules_vmware/tests/controllers/vcenter/test_login_banner_config.py index 89ed7f1..77ea4d4 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_login_banner_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_login_banner_config.py @@ -85,7 +85,7 @@ def test_remediate_failed(self, mock_execute_shell_cmd, mock_vc_context): def test_remediate_skipped_already_compliant(self, mock_execute_shell_cmd, mock_vc_context): # Setup Mock objects for successfully changing the value. - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_execute_shell_cmd.return_value = self.shell_cmd_set_return_success diff --git a/config_modules_vmware/tests/controllers/vcenter/test_ntp_config.py b/config_modules_vmware/tests/controllers/vcenter/test_ntp_config.py index c451130..4236ff4 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_ntp_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_ntp_config.py @@ -149,7 +149,7 @@ def test_check_compliance_failed(self, mock_vc_rest_client, mock_vc_context): @patch('config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext') @patch('config_modules_vmware.framework.clients.vcenter.vc_rest_client.VcRestClient') def test_remediate_skipped_already_desired(self, mock_vc_rest_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_rest_client.get_base_url.return_value = self.vc_base_url mock_vc_rest_client.get_helper.side_effect = [self.compliant_ntp_servers, self.compliant_ntp_mode] diff --git a/config_modules_vmware/tests/controllers/vcenter/test_snmp_v3_config.py b/config_modules_vmware/tests/controllers/vcenter/test_snmp_v3_config.py index a9a09b3..604972d 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_snmp_v3_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_snmp_v3_config.py @@ -204,7 +204,7 @@ def test_check_compliance_failed(self, mock_execute_shell_cmd, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.utils.utils.run_shell_cmd") def test_remediate_skipped_already_desired(self, mock_execute_shell_cmd, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_execute_shell_cmd.return_value = self.compliant_shell_cmd_return_val diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_authentication_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_authentication_policy.py index 5fc3e65..0ad376e 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_authentication_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_authentication_policy.py @@ -119,7 +119,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_all_domains.return_value = self.compliant_domain_mock_obj mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_ldaps_enabled_config.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_ldaps_enabled_config.py index 711555f..0559c69 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_ldaps_enabled_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_ldaps_enabled_config.py @@ -169,7 +169,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_all_domains.return_value = self.compliant_domain_mock_obj mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_auto_unlock_interval.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_auto_unlock_interval.py index aa2ca3a..8cf590d 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_auto_unlock_interval.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_auto_unlock_interval.py @@ -107,7 +107,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_auto_unlock_interval.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_bash_shell_authorized_members_config.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_bash_shell_authorized_members_config.py index 18267ea..deffba1 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_bash_shell_authorized_members_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_bash_shell_authorized_members_config.py @@ -162,7 +162,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_system_domain.return_value = self.system_domain mock_vc_vmomi_sso_client._get_group.return_value = self.group_mock diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_failed_login_attempt_interval.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_failed_login_attempt_interval.py index 183a800..574804d 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_failed_login_attempt_interval.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_failed_login_attempt_interval.py @@ -108,7 +108,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_interval_between_login_failures.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_max_failed_login_attempts.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_max_failed_login_attempts.py index 3b23a22..5b8d01b 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_max_failed_login_attempts.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_max_failed_login_attempts.py @@ -108,7 +108,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_max_failed_login_attempts.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_max_lifetime_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_max_lifetime_policy.py index 150e595..b4b8d2c 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_max_lifetime_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_max_lifetime_policy.py @@ -108,7 +108,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_password_lifetime_days.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_lowercase_character_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_lowercase_character_policy.py index e0dac82..ee4f80e 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_lowercase_character_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_lowercase_character_policy.py @@ -108,7 +108,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_min_number_of_lower_characters.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_numeric_character_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_numeric_character_policy.py index 40beb08..e315a56 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_numeric_character_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_numeric_character_policy.py @@ -108,7 +108,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_min_number_of_numeric_characters.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_special_character_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_special_character_policy.py index 5be9c55..5bac69c 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_special_character_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_special_character_policy.py @@ -108,7 +108,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_minimum_number_of_special_characters.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_uppercase_character_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_uppercase_character_policy.py index 2a29823..202f5c4 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_uppercase_character_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_uppercase_character_policy.py @@ -108,7 +108,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_min_number_of_upper_characters.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_minimum_length_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_minimum_length_policy.py index 2ac735b..79fae2f 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_minimum_length_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_minimum_length_policy.py @@ -108,7 +108,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_minimum_password_length.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_reuse_restriction_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_reuse_restriction_policy.py index a8ce973..1b1b923 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_password_reuse_restriction_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_password_reuse_restriction_policy.py @@ -108,7 +108,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_password_reuse_restriction.return_value = self.compliant_value mock_vc_context.vc_vmomi_sso_client.return_value = mock_vc_vmomi_sso_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_sso_trusted_admins_authorized_members_config.py b/config_modules_vmware/tests/controllers/vcenter/test_sso_trusted_admins_authorized_members_config.py index 80575ae..99b705f 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_sso_trusted_admins_authorized_members_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_sso_trusted_admins_authorized_members_config.py @@ -163,7 +163,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_sso_client, mock_vc_context @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client.VcVmomiSSOClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_sso_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_sso_client.get_system_domain.return_value = self.system_domain mock_vc_vmomi_sso_client._get_group.return_value = self.group_mock diff --git a/config_modules_vmware/tests/controllers/vcenter/test_syslog_config.py b/config_modules_vmware/tests/controllers/vcenter/test_syslog_config.py index 63cb0b5..3666f89 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_syslog_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_syslog_config.py @@ -171,7 +171,7 @@ def test_check_compliance_failed(self, mock_vc_rest_client, mock_vc_context): @patch('config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext') @patch('config_modules_vmware.framework.clients.vcenter.vc_rest_client.VcRestClient') def test_remediate_skipped_already_desired(self, mock_vc_rest_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_rest_client.get_base_url.return_value = self.vc_base_url mock_vc_rest_client.get_helper.return_value = self.get_helper_compliant_value diff --git a/config_modules_vmware/tests/controllers/vcenter/test_task_and_event_retention_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_task_and_event_retention_policy.py index f8b0421..485b1da 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_task_and_event_retention_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_task_and_event_retention_policy.py @@ -132,7 +132,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_vpxd_option_value.side_effect = [ self.compliant_value[DESIRED_TASK_CLEANUP_ENABLED_KEY], diff --git a/config_modules_vmware/tests/controllers/vcenter/test_tls_version_config.py b/config_modules_vmware/tests/controllers/vcenter/test_tls_version_config.py index 890734c..ced0867 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_tls_version_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_tls_version_config.py @@ -352,7 +352,7 @@ def test_check_compliance_for_versions_not_applicable(self, mock_vc_context): result = self.controller.check_compliance(mock_vc_context, self.desired_values_only_global) expected_result = { consts.STATUS: ComplianceStatus.SKIPPED, - consts.MESSAGE: "Control is not applicable on this product version" + consts.ERRORS: [consts.CONTROL_NOT_APPLICABLE] } assert result == expected_result diff --git a/config_modules_vmware/tests/controllers/vcenter/test_vm_migrate_encryption_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_vm_migrate_encryption_policy.py index 42b450f..bf7d12b 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_vm_migrate_encryption_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_vm_migrate_encryption_policy.py @@ -94,7 +94,7 @@ def create_all_vm_mock_refs(self, vm_configs, create_bad_mock=False): vm_ref = MagicMock() if not create_bad_mock: vm_ref.name = vm_config.get("vm_name") - vm_ref.config = vim.vm.ConfigSpec() + vm_ref.config = vim.vm.ConfigInfo() vm_ref.config.migrateEncryption = vm_config.get("migrate_encryption_policy") setattr(vm_ref, "ReconfigVM_Task", MagicMock(return_value=True)) all_vm_mock_refs.append(vm_ref) @@ -248,7 +248,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_objects_by_vimtype.return_value = self.mocked_vm_refs_compliant mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client @@ -286,6 +286,69 @@ def test_remediate_success(self, mock_vc_vmomi_client, mock_vc_context): result = self.controller.remediate(mock_vc_context, self.compliant_value) assert result == expected_result + @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") + @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") + @patch("config_modules_vmware.controllers.vcenter.vm_migrate_encryption_policy.VmMigrateEncryptionPolicy._get_resource_pool") + def test_remediate_template_success(self, mock_get_resource_pool, mock_vc_vmomi_client, mock_vc_context): + non_compliant_configs = self.controller._VmMigrateEncryptionPolicy__get_non_compliant_configs( + self.non_compliant_vm_configs, + self.compliant_value) + + expected_result = { + consts.STATUS: RemediateStatus.SUCCESS, + consts.OLD: non_compliant_configs, + consts.NEW: self.compliant_value, + } + # Mark VM as template + for vm_ref in self.mocked_vm_refs_non_compliant: + vm_ref.config.template = True + mock_get_resource_pool.return_value = MagicMock(), [] + mock_vc_vmomi_client.get_objects_by_vimtype.return_value = self.mocked_vm_refs_non_compliant + mock_vc_vmomi_client.get_vm_path_in_datacenter.side_effect = ["SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/db_workloads/ms-sql", + "SDDC-Datacenter/vm/db_workloads/ms-sql", + "SDDC-Datacenter/vm/dev", + "SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/db_workloads/ms-sql", + "SDDC-Datacenter/vm/db_workloads/ms-sql", + "SDDC-Datacenter/vm/dev", + "SDDC-Datacenter/vm/Management VMs"] + mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client + + result = self.controller.remediate(mock_vc_context, self.compliant_value) + assert result == expected_result + + @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") + @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") + @patch("config_modules_vmware.controllers.vcenter.vm_migrate_encryption_policy.VmMigrateEncryptionPolicy._get_resource_pool") + def test_remediate_template_failed(self, mock_get_resource_pool, mock_vc_vmomi_client, mock_vc_context): + expected_error = [["Resource pool not found"], ["Resource pool not found"], ["Resource pool not found"], ["Resource pool not found"], ["Resource pool not found"]] + expected_result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: expected_error} + # Mark VM as template + for vm_ref in self.mocked_vm_refs_non_compliant: + vm_ref.config.template = True + mock_get_resource_pool.return_value = None, ["Resource pool not found"] + mock_vc_vmomi_client.get_objects_by_vimtype.return_value = self.mocked_vm_refs_non_compliant + mock_vc_vmomi_client.get_vm_path_in_datacenter.side_effect = ["SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/db_workloads/ms-sql", + "SDDC-Datacenter/vm/db_workloads/ms-sql", + "SDDC-Datacenter/vm/dev", + "SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/Management VMs", + "SDDC-Datacenter/vm/db_workloads/ms-sql", + "SDDC-Datacenter/vm/db_workloads/ms-sql", + "SDDC-Datacenter/vm/dev", + "SDDC-Datacenter/vm/Management VMs"] + mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client + + result = self.controller.remediate(mock_vc_context, self.compliant_value) + assert result == expected_result + @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_get_failed(self, mock_vc_vmomi_client, mock_vc_context): diff --git a/config_modules_vmware/tests/controllers/vcenter/test_vpx_log_level_config.py b/config_modules_vmware/tests/controllers/vcenter/test_vpx_log_level_config.py index 201ac99..d717232 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_vpx_log_level_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_vpx_log_level_config.py @@ -107,7 +107,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_vpxd_option_value.return_value = self.compliant_log_level mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_vpx_sddc_deployed_compliance_kit_config.py b/config_modules_vmware/tests/controllers/vcenter/test_vpx_sddc_deployed_compliance_kit_config.py index b72cc34..90781d0 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_vpx_sddc_deployed_compliance_kit_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_vpx_sddc_deployed_compliance_kit_config.py @@ -107,7 +107,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_vpxd_option_value.return_value = self.compliant_sddc_compliance_kit mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_vpx_syslog_enablement_config.py b/config_modules_vmware/tests/controllers/vcenter/test_vpx_syslog_enablement_config.py index 1e90a68..ba87965 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_vpx_syslog_enablement_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_vpx_syslog_enablement_config.py @@ -107,7 +107,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_vpxd_option_value.return_value = self.compliant_value mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_vpx_user_host_password_length_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_vpx_user_host_password_length_policy.py index fa61938..03cd23c 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_vpx_user_host_password_length_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_vpx_user_host_password_length_policy.py @@ -109,7 +109,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_vpxd_option_value.return_value = self.compliant_value mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_vpx_user_password_expiration_policy.py b/config_modules_vmware/tests/controllers/vcenter/test_vpx_user_password_expiration_policy.py index 4cf02a7..0e537ab 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_vpx_user_password_expiration_policy.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_vpx_user_password_expiration_policy.py @@ -110,7 +110,7 @@ def test_check_compliance_failed(self, mock_vc_vmomi_client, mock_vc_context): @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vmomi_client.VcVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vmomi_client.get_vpxd_option_value.return_value = self.compliant_value mock_vc_context.vc_vmomi_client.return_value = mock_vc_vmomi_client diff --git a/config_modules_vmware/tests/controllers/vcenter/test_vsan_hcl_proxy_config.py b/config_modules_vmware/tests/controllers/vcenter/test_vsan_hcl_proxy_config.py index e0b527f..cab2d71 100644 --- a/config_modules_vmware/tests/controllers/vcenter/test_vsan_hcl_proxy_config.py +++ b/config_modules_vmware/tests/controllers/vcenter/test_vsan_hcl_proxy_config.py @@ -203,7 +203,7 @@ def test_check_compliance_failed(self, mock_vc_vsan_vmomi_client, mock_vc_contex @patch("config_modules_vmware.framework.auth.contexts.vc_context.VcenterContext") @patch("config_modules_vmware.framework.clients.vcenter.vc_vsan_vmomi_client.VcVsanVmomiClient") def test_remediate_skipped_already_desired(self, mock_vc_vsan_vmomi_client, mock_vc_context): - expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: ['Control already compliant']} + expected_result = {consts.STATUS: RemediateStatus.SKIPPED, consts.ERRORS: [consts.CONTROL_ALREADY_COMPLIANT]} mock_vc_vsan_vmomi_client.get_all_vsan_enabled_clusters.return_value = self.all_vsan_enabled_mock_cluster_refs mock_vc_vsan_vmomi_client.get_vsan_proxy_config_for_cluster.side_effect = [ diff --git a/config_modules_vmware/tests/framework/auth/contexts/test_esxi_context.py b/config_modules_vmware/tests/framework/auth/contexts/test_esxi_context.py index 7b41d89..0d2bbe9 100644 --- a/config_modules_vmware/tests/framework/auth/contexts/test_esxi_context.py +++ b/config_modules_vmware/tests/framework/auth/contexts/test_esxi_context.py @@ -42,6 +42,7 @@ def test_esx_vc_context_rest_client(self, mock_rest_client): with self.context: assert self.context.vc_rest_client() is not None args = mock_rest_client.call_args.args + self.context._vc_rest_client._rest_client_session = None assert self.hostname in args assert self.username in args assert self.password in args diff --git a/config_modules_vmware/tests/framework/auth/contexts/test_vc_context.py b/config_modules_vmware/tests/framework/auth/contexts/test_vc_context.py index c8dc0a4..1568bc0 100644 --- a/config_modules_vmware/tests/framework/auth/contexts/test_vc_context.py +++ b/config_modules_vmware/tests/framework/auth/contexts/test_vc_context.py @@ -47,6 +47,7 @@ def test_vc_context_rest_client(self, mock_rest_client): with self.context: assert self.context.vc_rest_client() is not None args = mock_rest_client.call_args.args + self.context._vc_rest_client._rest_client_session = None assert self.hostname in args assert self.username in args assert self.password in args diff --git a/config_modules_vmware/tests/framework/models/output_models/test_validate_configuration_response.py b/config_modules_vmware/tests/framework/models/output_models/test_validate_configuration_response.py new file mode 100644 index 0000000..bc1b90f --- /dev/null +++ b/config_modules_vmware/tests/framework/models/output_models/test_validate_configuration_response.py @@ -0,0 +1,37 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import pytest + +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.models.output_models.validate_configuration_response import ValidateConfigurationResponse +from config_modules_vmware.framework.models.output_models.validate_configuration_response import ValidateConfigurationStatus + + +class TestValidateConfigurationResponse: + + @pytest.fixture + def validate_configuration_response(self): + return ValidateConfigurationResponse() + + def test_initialization(self, validate_configuration_response): + assert validate_configuration_response.result == {} + assert validate_configuration_response.status == ValidateConfigurationStatus.VALID + + def test_to_dict_method(self, validate_configuration_response): + validate_configuration_response.status = ValidateConfigurationStatus.INVALID + validate_configuration_response.result = {"current": "value1", "desired": "value2"} + + expected_dict = { + consts.STATUS: validate_configuration_response.status, + consts.RESULT: validate_configuration_response.result + } + + assert validate_configuration_response.to_dict() == expected_dict + + def test_to_dict_method_without_result(self, validate_configuration_response): + validate_configuration_response.status = ValidateConfigurationStatus.VALID + + expected_dict = { + consts.STATUS: validate_configuration_response.status + } + + assert validate_configuration_response.to_dict() == expected_dict diff --git a/config_modules_vmware/tests/interfaces/test_controller_interface.py b/config_modules_vmware/tests/interfaces/test_controller_interface.py index 09d7827..a634ce2 100644 --- a/config_modules_vmware/tests/interfaces/test_controller_interface.py +++ b/config_modules_vmware/tests/interfaces/test_controller_interface.py @@ -7,7 +7,10 @@ from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus from config_modules_vmware.framework.models.output_models.get_current_response import GetCurrentConfigurationStatus +from config_modules_vmware.framework.models.output_models.get_schema_response import GetSchemaStatus from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.models.output_models.validate_configuration_response import \ + ValidateConfigurationStatus from config_modules_vmware.interfaces.controller_interface import ControllerInterface from config_modules_vmware.services.workflows.operations_interface import Operations @@ -137,7 +140,7 @@ def test_get_current_configuration_exception(self, compliance_operation_operate_ # Assert expected results compliance_operation_operate_mock.assert_called_once_with( self.context_mock, Operations.GET_CURRENT, input_values=None, metadata_filter=None) - assert result == {'status': ComplianceStatus.ERROR, 'message': 'Test Exception'} + assert result == {'status': GetCurrentConfigurationStatus.FAILED, 'message': 'Test Exception'} @patch('config_modules_vmware.services.workflows.configuration_operations.ConfigurationOperations.operate') def test_get_current_configuration_configuration_operation(self, configuration_operation_operate_mock): @@ -285,3 +288,62 @@ def test_remediate_configuration_operation(self, configuration_operation_operate configuration_operation_operate_mock.assert_called_once_with( self.context_mock, Operations.REMEDIATE, input_values=self.desired_state_spec, metadata_filter=None) assert result == expected_remediation_response + + @patch('config_modules_vmware.services.workflows.configuration_operations.ConfigurationOperations.operate') + def test_get_schema_exception(self, configuration_operation_operate_mock): + configuration_operation_operate_mock.side_effect = Exception('Test Exception') + result = self.control_config.get_schema(controller_type=ControllerMetadata.ControllerType.CONFIGURATION) + + # Assert expected results + configuration_operation_operate_mock.assert_called_once_with( + self.context_mock, Operations.GET_SCHEMA, input_values={}, metadata_filter=None) + assert result == {'status': GetSchemaStatus.FAILED, 'message': 'Test Exception'} + + @patch('config_modules_vmware.services.workflows.configuration_operations.ConfigurationOperations.operate') + def test_get_schema_configuration_operation(self, configuration_operation_operate_mock): + expected_get_schema_response = { + 'status': GetSchemaStatus.SUCCESS, + 'result': {"foo": "bar"} + } + configuration_operation_operate_mock.return_value = { + 'status': GetSchemaStatus.SUCCESS, + 'result': expected_get_schema_response.get(consts.RESULT) + } + + result = self.control_config.get_schema(controller_type=ControllerMetadata.ControllerType.CONFIGURATION) + + # Assert expected results + configuration_operation_operate_mock.assert_called_once_with( + self.context_mock, Operations.GET_SCHEMA, input_values={}, metadata_filter=None) + assert result == expected_get_schema_response + + @patch('config_modules_vmware.services.workflows.configuration_operations.ConfigurationOperations.operate') + def test_validate_configuration_operation(self, configuration_operation_operate_mock): + expected_validate_response = { + 'status': ValidateConfigurationStatus.VALID + } + configuration_operation_operate_mock.return_value = expected_validate_response + desired_state = {"foo": "bar"} + result = self.control_config.validate_configuration( + desired_state_spec=desired_state, + controller_type=ControllerMetadata.ControllerType.CONFIGURATION + ) + + # Assert expected results + configuration_operation_operate_mock.assert_called_once_with( + self.context_mock, Operations.VALIDATE, input_values=desired_state, metadata_filter=None) + assert result == expected_validate_response + + @patch('config_modules_vmware.services.workflows.configuration_operations.ConfigurationOperations.operate') + def test_validate_exception(self, configuration_operation_operate_mock): + configuration_operation_operate_mock.side_effect = Exception('Test Exception') + desired_state = {"foo": "bar"} + result = self.control_config.validate_configuration( + desired_state_spec=desired_state, + controller_type=ControllerMetadata.ControllerType.CONFIGURATION + ) + + # Assert expected results + configuration_operation_operate_mock.assert_called_once_with( + self.context_mock, Operations.VALIDATE, input_values=desired_state, metadata_filter=None) + assert result == {'status': ValidateConfigurationStatus.FAILED, 'message': 'Test Exception'} diff --git a/config_modules_vmware/tests/interfaces/test_metadata_interface.py b/config_modules_vmware/tests/interfaces/test_metadata_interface.py index 5ff2ea8..6d069ca 100644 --- a/config_modules_vmware/tests/interfaces/test_metadata_interface.py +++ b/config_modules_vmware/tests/interfaces/test_metadata_interface.py @@ -521,4 +521,4 @@ def test_validate_custom_metadata_invalid_metadata_field_type(self): } } with pytest.raises(TypeError): - ControllerMetadataInterface.validate_custom_metadata(custom_metadata) \ No newline at end of file + ControllerMetadataInterface.validate_custom_metadata(custom_metadata) diff --git a/config_modules_vmware/tests/services/apis/controllers/test_vcenter.py b/config_modules_vmware/tests/services/apis/controllers/test_vcenter.py index e49fb93..05a3e74 100644 --- a/config_modules_vmware/tests/services/apis/controllers/test_vcenter.py +++ b/config_modules_vmware/tests/services/apis/controllers/test_vcenter.py @@ -10,8 +10,13 @@ from config_modules_vmware.framework.auth.contexts.base_context import BaseContext from config_modules_vmware.framework.clients.common import consts from config_modules_vmware.framework.models.output_models.configuration_drift_response import Status +from config_modules_vmware.framework.models.output_models.get_schema_response import GetSchemaStatus +from config_modules_vmware.framework.models.output_models.validate_configuration_response import \ + ValidateConfigurationStatus from config_modules_vmware.services.apis.controllers.consts import VC_GET_CONFIGURATION_V1 +from config_modules_vmware.services.apis.controllers.consts import VC_GET_SCHEMA_V1 from config_modules_vmware.services.apis.controllers.consts import VC_SCAN_DRIFTS_V1 +from config_modules_vmware.services.apis.controllers.consts import VC_VALIDATE_CONFIGURATION_V1 from config_modules_vmware.services.apis.models.get_config_payload import GetConfigResponsePayload from config_modules_vmware.services.apis.models.get_config_payload import GetConfigStatus from config_modules_vmware.services.apis.models.target_model import Target @@ -258,7 +263,7 @@ def test_scan_drifts_api(self, controller_interface): drift_response_mock = { "status": "NON_COMPLIANT", "changes": { - "schema_version": "1.0-DRAFT", + "schema_version": "1.0", "id": "2bcaa939-e6c2-4347-808f-ad90debc20ae", "name": "config_modules_vmware.controllers.vcenter.vc_profile", "timestamp": "2024-03-28T23:03:19.472Z", @@ -276,15 +281,15 @@ def test_scan_drifts_api(self, controller_interface): { "key": "appliance/user_account_settings/local_accounts_policy/max_days", "category": "user_account_settings", - "current_value": 90, - "desired_value": 120, + "current_value": "90", + "desired_value": "120", } ], "deletions": [ { "key": "appliance/syslog/1", "category": "syslog", - "value": {"hostname": "8.8.1.1", "protocol": "TLS", "port": 90}, + "value": '{"hostname": "8.8.1.1", "protocol": "TLS", "port": 90}', } ], }, @@ -450,7 +455,7 @@ def test_scan_drifts_missing_status_response(self, controller_interface): # test missing status in response from product interface controller_interface.return_value = { "changes": { - "schema_version": "1.0-DRAFT", + "schema_version": "1.0", "id": "2bcaa939-e6c2-4347-808f-ad90debc20ae", "name": "config_modules_vmware.controllers.vcenter.vc_profile", "timestamp": "2024-03-28T23:03:19.472Z", @@ -492,7 +497,7 @@ def test_scan_drifts_invalid_fields(self, controller_interface): controller_interface.return_value = { "status": "FAILED", "changes": { - "schema_version": "1.0-DRAFT", + "schema_version": "1.0", "id": "2bcaa939-e6c2-4347-808f-ad90debc20ae", "name": "config_modules_vmware.controllers.vcenter.vc_profile", "timestamp": "2024-03-28T23:03:19.472Z", @@ -560,7 +565,7 @@ def test_scan_drifts_failed_response(self, controller_interface): controller_interface.return_value = { "status": "FAILED", "message": { - "schema_version": "1.0-DRAFT", + "schema_version": "1.0", "id": "2bcaa939-e6c2-4347-808f-ad90debc20ae", "name": "config_modules_vmware.controllers.vcenter.vc_profile", "timestamp": "2024-03-28T23:03:19.472Z", @@ -667,3 +672,305 @@ def test_scan_drifts_exception(self, controller_interface): assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR assert response.content is not None assert response.json()['status'] == Status.FAILED + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.get_schema') + def test_get_schema_api(self, controller_interface): + get_schema_mock = {consts.STATUS: GetSchemaStatus.SUCCESS, consts.RESULT: {"schema": {"config_1": "value_1"}}} + controller_interface.return_value = get_schema_mock + response = self.client.post(VC_GET_SCHEMA_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + } + }) + assert response.status_code == status.HTTP_200_OK + assert response.content is not None + assert response.json().get("result").get("json_schema") == get_schema_mock.get(consts.RESULT).get("schema") + + def test_get_schema_api_missing_required_property(self): + response = self.client.post(VC_GET_SCHEMA_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + } + }) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.get_schema') + def test_get_schema_api_no_response(self, controller_interface): + get_schema_mock = None + controller_interface.return_value = get_schema_mock + response = self.client.post(VC_GET_SCHEMA_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + } + }) + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert "Schema response is None." in response.json()[consts.ERRORS][0]["error"]["message"] + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.get_schema') + def test_get_schema_api_failed(self, controller_interface): + error_msg = "Exception in workflow" + get_schema_mock = {consts.STATUS: GetSchemaStatus.FAILED, consts.MESSAGE: error_msg} + controller_interface.return_value = get_schema_mock + response = self.client.post(VC_GET_SCHEMA_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + } + }) + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert error_msg in response.json()[consts.ERRORS][0]["error"]["message"] + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.get_schema') + def test_get_schema_api_skipped(self, controller_interface): + error_msg = "Product not supported" + get_schema_mock = {consts.STATUS: GetSchemaStatus.SKIPPED, consts.MESSAGE: error_msg} + controller_interface.return_value = get_schema_mock + response = self.client.post(VC_GET_SCHEMA_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + } + }) + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert error_msg in response.json()[consts.ERRORS][0]["error"]["message"] + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.get_schema') + def test_get_schema_api_missing_response(self, controller_interface): + get_schema_mock = {consts.RESULT: {"schema": {"config_1": "value_1"}}} + controller_interface.return_value = get_schema_mock + response = self.client.post(VC_GET_SCHEMA_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + } + }) + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert "Missing status in schema response." in response.json()[consts.ERRORS][0]["error"]["message"] + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.get_schema') + def test_get_schema_api_missing_schema(self, controller_interface): + get_schema_mock = {consts.STATUS: GetSchemaStatus.SUCCESS, consts.RESULT: {"invalid": {"config_1": "value_1"}}} + controller_interface.return_value = get_schema_mock + response = self.client.post(VC_GET_SCHEMA_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + } + }) + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert "Missing schema in schema response." in response.json()[consts.ERRORS][0]["error"]["message"] + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.validate_configuration') + def test_validate_api(self, controller_interface): + validate_mock = {consts.STATUS: ValidateConfigurationStatus.VALID} + controller_interface.return_value = validate_mock + response = self.client.post(VC_VALIDATE_CONFIGURATION_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + }, + "input_spec": { + "property": "value" + } + }) + assert response.status_code == status.HTTP_200_OK + assert response.content is not None + assert response.json().get(consts.STATUS) == validate_mock.get(consts.STATUS).name + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.validate_configuration') + def test_validate_api_invalid_configuration(self, controller_interface): + validate_mock = { + consts.STATUS: ValidateConfigurationStatus.INVALID, + consts.RESULT: { + "errors": [{ + "id": "/vcenter/authmgmt/token_policy/clock_tolerance", + "message": { + "args": [], + "default_message": "Required property missing: clock_tolerance", + "id": "/vcenter/authmgmt/token_policy/clock_tolerance" + } + }] + } + } + controller_interface.return_value = validate_mock + response = self.client.post(VC_VALIDATE_CONFIGURATION_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + }, + "input_spec": { + "property": "value" + } + }) + assert response.status_code == status.HTTP_200_OK + assert response.content is not None + assert response.json().get(consts.STATUS) == validate_mock.get(consts.STATUS).name + result = response.json().get(consts.RESULT) + assert result is not None + assert result == validate_mock.get(consts.RESULT) + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.validate_configuration') + def test_validate_api_missing_status(self, controller_interface): + validate_mock = { + consts.RESULT: { + "errors": [{ + "id": "/vcenter/authmgmt/token_policy/clock_tolerance", + "message": { + "args": [], + "default_message": "Required property missing: clock_tolerance", + "id": "/vcenter/authmgmt/token_policy/clock_tolerance" + } + }] + } + } + controller_interface.return_value = validate_mock + response = self.client.post(VC_VALIDATE_CONFIGURATION_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + }, + "input_spec": { + "property": "value" + } + }) + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert response.content is not None + assert response.json()[consts.STATUS] == ValidateConfigurationStatus.FAILED.value + assert "Missing status in configuration response." in response.json()[consts.ERRORS][0]["error"]["message"] + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.validate_configuration') + def test_validate_no_response(self, controller_interface): + get_schema_mock = None + controller_interface.return_value = get_schema_mock + response = self.client.post(VC_VALIDATE_CONFIGURATION_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + }, + "input_spec": { + "property": "value" + } + }) + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert "Validate response is None." in response.json()[consts.ERRORS][0]["error"]["message"] + + def test_validate_api_missing_required_property(self): + response = self.client.post(VC_VALIDATE_CONFIGURATION_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + }, + "input_spec": { + "property": "value" + } + }) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + @patch('config_modules_vmware.interfaces.controller_interface.ControllerInterface.validate_configuration') + def test_validate_api_failed(self, controller_interface): + error_msg = "Exception in workflow" + validate_mock = {consts.STATUS: ValidateConfigurationStatus.FAILED, consts.MESSAGE: error_msg} + controller_interface.return_value = validate_mock + response = self.client.post(VC_VALIDATE_CONFIGURATION_V1, json={ + "target": { + "hostname": "some_host", + "auth": [ + { + "username": "sso_username", + "password": "sso_password!", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD...", + } + ], + }, + "input_spec": { + "property": "value" + } + }) + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert error_msg in response.json()[consts.ERRORS][0]["error"]["message"] diff --git a/config_modules_vmware/tests/services/workflows/test_compliance_operations.py b/config_modules_vmware/tests/services/workflows/test_compliance_operations.py index b8b96d5..f2be14c 100644 --- a/config_modules_vmware/tests/services/workflows/test_compliance_operations.py +++ b/config_modules_vmware/tests/services/workflows/test_compliance_operations.py @@ -1290,3 +1290,12 @@ def test_remediate_metadata_addition(self, ntp_config): } } self.compliance_workflow_with_metadata(remediate_response, ntp_config, Operations.REMEDIATE) + + @patch('config_modules_vmware.services.mapper.mapper_utils.get_mapping_template') + @patch('logging.Logger.info') + def test_operate_unsupported_operation(self, get_mapping_template_mock, logger_error_mock): + self.context_mock.product_category.value = "vcenter" + get_mapping_template_mock.return_value = self.config_template + with pytest.raises(Exception): + ComplianceOperations.operate(self.context_mock, Operations.GET_SCHEMA, {}, None) + logger_error_mock.assert_called_once_with("GET_SCHEMA is not a valid operation for compliance controls.") diff --git a/config_modules_vmware/tests/services/workflows/test_configuration_operations.py b/config_modules_vmware/tests/services/workflows/test_configuration_operations.py index e2a9fa1..d14e9e6 100644 --- a/config_modules_vmware/tests/services/workflows/test_configuration_operations.py +++ b/config_modules_vmware/tests/services/workflows/test_configuration_operations.py @@ -7,6 +7,7 @@ from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus from config_modules_vmware.framework.models.output_models.get_current_response import GetCurrentConfigurationStatus +from config_modules_vmware.framework.models.output_models.get_schema_response import GetSchemaStatus from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus from config_modules_vmware.services.workflows.configuration_operations import ConfigurationOperations from config_modules_vmware.services.workflows.operations_interface import Operations @@ -181,13 +182,22 @@ def test_operate_no_matching_product(self, get_mapping_template_mock): assert result == expected_result @patch('config_modules_vmware.services.mapper.mapper_utils.get_mapping_template') - def test_operate_no_matching_product_version(self, get_mapping_template_mock): + @patch('config_modules_vmware.services.mapper.mapper_utils.get_class') + def test_operate_get_schema(self, get_class_mock, get_mapping_template_mock): get_mapping_template_mock.return_value = self.config_template - self.context_mock.product_version = "0.0.0" + + class MockControllerGetSchema: + metadata = ControllerMetadata(status=ControllerMetadata.ControllerStatus.ENABLED) + + @staticmethod + def get_schema(context): + return {'status': GetSchemaStatus.SUCCESS, 'result': {"foo": "bar"}} + + get_class_mock.return_value = MockControllerGetSchema expected_result = { - consts.STATUS: GetCurrentConfigurationStatus.SKIPPED, - consts.MESSAGE: f"Version [0.0.0] is not supported for product [{self.context_mock.product_category}]" + consts.STATUS: GetSchemaStatus.SUCCESS, + consts.RESULT: {"foo": "bar"}, } - result = ConfigurationOperations.operate(self.context_mock, Operations.GET_CURRENT, self.input_values) + result = ConfigurationOperations.operate(self.context_mock, Operations.GET_SCHEMA, self.input_values) assert result == expected_result diff --git a/devops/scripts/run_reorder_imports.sh b/devops/scripts/run_reorder_imports.sh index 6b4be4c..f8aa316 100755 --- a/devops/scripts/run_reorder_imports.sh +++ b/devops/scripts/run_reorder_imports.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh set -e echo "=============================================" @@ -6,20 +6,52 @@ echo "Run reorder-python-imports for config-modules" echo "=============================================" python3 -m pip install reorder-python-imports -if [ $# -eq 0 ] -then - echo "No files passed. Running reorder-python-imports on all .py files" - FILES_TO_SCAN=$(find . -name "*.py" ) -else - FILES_TO_SCAN="$*" -fi -while read -r line; do - filearray=("$line") - for file in $filearray; do - if [[ "$file" == *.py && "$file" != *"config_modules_vmware/framework/clients/vcenter/dependencies/"* ]] - then - reorder-python-imports --exit-zero-even-if-changed "$file" +run_all_files() +{ + FILES_TO_SCAN=$(find . -name "*.py") + echo "$FILES_TO_SCAN" | while IFS= read -r file; do + if [ "${file##*.}" = "py" ] && [ "${file#*config_modules_vmware/framework/clients/vcenter/dependencies/}" = "$file" ]; then + changed_file=$( (reorder-python-imports --exit-zero-even-if-changed "$file" 1>&2) 2>&1) + if [ -n "$changed_file" ] + then + echo "$changed_file" + fi + fi + done +} + +run_passed_files() +{ + for file in "$@" + do + if [ "${file##*.}" = "py" ] && [ "${file#*config_modules_vmware/framework/clients/vcenter/dependencies/}" = "$file" ]; then + changed_file=$( (reorder-python-imports --exit-zero-even-if-changed "$file" 1>&2) 2>&1) + if [ -n "$changed_file" ] + then + echo "$changed_file" + fi fi done -done <<< "$FILES_TO_SCAN" +} + +if [ $# -eq 0 ]; then + echo "No files passed. Running reorder-python-imports on all .py files" + function_output=$(run_all_files) + if [ -n "$function_output" ] + then + echo Files that failed: + echo "$function_output" + exit 1 + fi + +else + echo "Files passed as arguments. Running reorder-python-imports on the passed in files" + function_output=$(run_passed_files "$@") + if [ -n "$function_output" ] + then + echo Files that failed: + echo "$function_output" + exit 1 + fi +fi diff --git a/docs/compliance-schema-documentation.md b/docs/compliance-schema-documentation.md new file mode 100644 index 0000000..55deefe --- /dev/null +++ b/docs/compliance-schema-documentation.md @@ -0,0 +1,136 @@ +## Compliance Schema Documentation + +Compliance Schema link: [Compliance Schema](../config_modules_vmware/schemas/compliance_reference_schema.json) + +1. Schema serves as a contract between the customer provided desired state value and the controller class inside the config-module. +2. Config-module validates the spec provided by the customer against the compliance reference schema. This validation is done before the spec reaches any controller class. +3. The compliance reference schema is based on [JSONSchema specification](https://json-schema.org/specification). +4. All controls in the schema are grouped based on the product type (vcenter, sddc_manager, etc.) +5. Each control spec has a 'metadata' property, which is purely used to identity the control. This is not used by config-modules. +6. The 'value' property inside a control spec is what a controller class receives and operates on. + +##### Sample compliance schema with one control and explanation +``` +{ + "$schema": "http://json-schema.org/draft-07/schema#", ----> JSONSchema version + "title": "Compliance Reference Schema", + "description": "Version 1.0.0; last updated 4-Jan-2024", + "type": "object", + "properties": { + "compliance_config": { + "type": "object", + "title": "Compliance configuration", + "description": "Includes all products and compliance controls under those products", + "properties": { + "vcenter": { ----> Controls are grouped by product type + "type": "object", + "title": "Compliance controls related to vcenter", + "properties": { + "syslog": { ----> syslog vcenter control. + "type": "object", + "properties": { + "metadata": { ----> Every control includes a metadata section to identify the control + "$ref": "#/definitions/metadata" + }, + "value": { ----> Corresponding control class acts on this value + "type": "object", + "properties": { + "servers": { + "type": "array", + "description": "A valid list of syslog servers", + "items": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "port": { + "type": "number" + }, + "protocol": { + "enum": [ + "TLS", + "UDP", + "RELP", + "TCP" + ] + } + }, + "required": [ + "hostname", + "port", + "protocol" + ], + "additionalProperties": false + }, + "minItems": 1 ----> JSONSchema validator mentioning that the array should have min 1 item + } + }, + "required": [ + "servers" + ], + "additionalProperties": false + } + }, + "required": [ + "value" + ], + "additionalProperties": false + } + } + } + } + } + }, + "required": [ ----> JSONSchema validator mentioning a required property + "compliance_config" + ], + "additionalProperties": false, ----> validator mentioning that no additional properties can be specified + "definitions": { ----> common definitions used across _properties_ + "metadata": { + "type": "object", + "title": "Metadata", + "description": "Metadata for a configuration control", + "properties": { + "configuration_id": { + "type": "string", + "description": "This is the configuration ID listed in the compliance kit" + }, + "configuration_title": { + "type": "string", + "description": "This is the configuration title listed in the compliance kit" + } + }, + "required": [ + "configuration_id", + "configuration_title" + ] + } + } +} +``` + +##### Sample desired spec based on the above sample compliance schema: +```json +{ + "compliance_config": { + "vcenter": { + "syslog": { + "value": { + "servers": [ + { + "hostname": "10.193.2.105", + "port": 514, + "protocol": "UDP" + } + ] + }, + "metadata": { + "configuration_id": "1218", + "configuration_title": "Configure the appliance to send logs to a central log server." + } + } + } + } +} +``` diff --git a/docs/controllers/esxi/esxi.log_location_config.rst b/docs/controllers/esxi/esxi.log_location_config.rst new file mode 100644 index 0000000..66939f8 --- /dev/null +++ b/docs/controllers/esxi/esxi.log_location_config.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.log_location_config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.ntp_config.rst b/docs/controllers/esxi/esxi.ntp_config.rst new file mode 100644 index 0000000..8ff788d --- /dev/null +++ b/docs/controllers/esxi/esxi.ntp_config.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.ntp_config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.password_quality_config.rst b/docs/controllers/esxi/esxi.password_quality_config.rst new file mode 100644 index 0000000..5391635 --- /dev/null +++ b/docs/controllers/esxi/esxi.password_quality_config.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.password_quality_config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.rst b/docs/controllers/esxi/esxi.rst index f6fe58e..e8198e9 100644 --- a/docs/controllers/esxi/esxi.rst +++ b/docs/controllers/esxi/esxi.rst @@ -30,12 +30,15 @@ Submodules esxi.lockdown_dcui_access_users esxi.lockdown_mode_config esxi.lockdown_mode_exception_users + esxi.log_location_config esxi.managed_object_browser esxi.max_failed_login_attempts esxi.mem_share_force_salting_config + esxi.ntp_config esxi.ntp_service_config esxi.ntp_service_startup_policy esxi.password_max_lifetime_policy + esxi.password_quality_config esxi.password_reuse_restriction_policy esxi.pg_vss_allow_promiscuous_mode esxi.pg_vss_forged_transmits_accept @@ -44,13 +47,22 @@ Submodules esxi.rhttpproxy_fips_140_2_crypt_config esxi.shell_service_policy esxi.slp_service_policy + esxi.snmp_config esxi.snmp_service_policy + esxi.ssh_compression_policy esxi.ssh_daemon_login_banner esxi.ssh_fips_140_2_crypt_config + esxi.ssh_gateway_ports_policy + esxi.ssh_host_based_auth_policy esxi.ssh_ignore_rhosts_policy esxi.ssh_login_banner + esxi.ssh_permit_empty_passwords_policy + esxi.ssh_permit_tunnel_policy + esxi.ssh_permit_user_environment_policy esxi.ssh_port_forwarding_policy esxi.ssh_service_policy + esxi.ssh_strict_mode_policy + esxi.ssh_x11_forwarding_policy esxi.suppress_shell_warning_policy esxi.syslog_enforce_ssl_certificates esxi.syslog_strict_x509_compliance diff --git a/docs/controllers/esxi/esxi.snmp_config.rst b/docs/controllers/esxi/esxi.snmp_config.rst new file mode 100644 index 0000000..97102d9 --- /dev/null +++ b/docs/controllers/esxi/esxi.snmp_config.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.snmp_config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.ssh_compression_policy.rst b/docs/controllers/esxi/esxi.ssh_compression_policy.rst new file mode 100644 index 0000000..f8b6adb --- /dev/null +++ b/docs/controllers/esxi/esxi.ssh_compression_policy.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.ssh_compression_policy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.ssh_gateway_ports_policy.rst b/docs/controllers/esxi/esxi.ssh_gateway_ports_policy.rst new file mode 100644 index 0000000..864ddf4 --- /dev/null +++ b/docs/controllers/esxi/esxi.ssh_gateway_ports_policy.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.ssh_gateway_ports_policy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.ssh_host_based_auth_policy.rst b/docs/controllers/esxi/esxi.ssh_host_based_auth_policy.rst new file mode 100644 index 0000000..f8e01d9 --- /dev/null +++ b/docs/controllers/esxi/esxi.ssh_host_based_auth_policy.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.ssh_host_based_auth_policy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.ssh_permit_empty_passwords_policy.rst b/docs/controllers/esxi/esxi.ssh_permit_empty_passwords_policy.rst new file mode 100644 index 0000000..fe52987 --- /dev/null +++ b/docs/controllers/esxi/esxi.ssh_permit_empty_passwords_policy.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.ssh_permit_empty_passwords_policy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.ssh_permit_tunnel_policy.rst b/docs/controllers/esxi/esxi.ssh_permit_tunnel_policy.rst new file mode 100644 index 0000000..397dcf3 --- /dev/null +++ b/docs/controllers/esxi/esxi.ssh_permit_tunnel_policy.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.ssh_permit_tunnel_policy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.ssh_permit_user_environment_policy.rst b/docs/controllers/esxi/esxi.ssh_permit_user_environment_policy.rst new file mode 100644 index 0000000..5c843e1 --- /dev/null +++ b/docs/controllers/esxi/esxi.ssh_permit_user_environment_policy.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.ssh_permit_user_environment_policy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.ssh_strict_mode_policy.rst b/docs/controllers/esxi/esxi.ssh_strict_mode_policy.rst new file mode 100644 index 0000000..5a2ee93 --- /dev/null +++ b/docs/controllers/esxi/esxi.ssh_strict_mode_policy.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.ssh_strict_mode_policy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/esxi/esxi.ssh_x11_forwarding_policy.rst b/docs/controllers/esxi/esxi.ssh_x11_forwarding_policy.rst new file mode 100644 index 0000000..9f10fb8 --- /dev/null +++ b/docs/controllers/esxi/esxi.ssh_x11_forwarding_policy.rst @@ -0,0 +1,4 @@ +.. automodule:: esxi.ssh_x11_forwarding_policy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/markdown/esxi/esxi.log_location_config.md b/docs/controllers/markdown/esxi/esxi.log_location_config.md new file mode 100644 index 0000000..e8f6a5d --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.log_location_config.md @@ -0,0 +1,57 @@ +### *class* LogLocationConfig + +Bases: `BaseController` + +ESXi controller to get/set/check_compliance/remediate persistent log location config. + +Config Id - 136 +
+Config Title - Configure a persistent log location for all locally stored logs. +
+ +Controller Metadata +```json +{ + "name": "log_location_config", + "configuration_id": "136", + "path_in_schema": "compliance_config.esxi.log_location_config", + "title": "Configure a persistent log location for all locally stored logs", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": null, + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get persistent log location config for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESXi context instance. +* **Returns:** + Tuple of dictionary with keys “log_location” and “is_persistent” and a list of errors. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set persistent log location config for esxi host. +It sets the log location and verifies if the log location persistent criteria matches with desired or not. +If it does not, then reverts to the original log location and report error + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*dict*) – dictionary with keys “log_location” and “is_persistent” +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple diff --git a/docs/controllers/markdown/esxi/esxi.md b/docs/controllers/markdown/esxi/esxi.md index e695a95..08f0221 100644 --- a/docs/controllers/markdown/esxi/esxi.md +++ b/docs/controllers/markdown/esxi/esxi.md @@ -59,6 +59,9 @@ * [`LockdownModeExceptionUsers.get()`](esxi.lockdown_mode_exception_users.md#esxi.lockdown_mode_exception_users.LockdownModeExceptionUsers.get) * [`LockdownModeExceptionUsers.set()`](esxi.lockdown_mode_exception_users.md#esxi.lockdown_mode_exception_users.LockdownModeExceptionUsers.set) * [`LockdownModeExceptionUsers.check_compliance()`](esxi.lockdown_mode_exception_users.md#esxi.lockdown_mode_exception_users.LockdownModeExceptionUsers.check_compliance) +* [`LogLocationConfig`](esxi.log_location_config.md) + * [`LogLocationConfig.get()`](esxi.log_location_config.md#esxi.log_location_config.LogLocationConfig.get) + * [`LogLocationConfig.set()`](esxi.log_location_config.md#esxi.log_location_config.LogLocationConfig.set) * [`ManagedObjectBrowser`](esxi.managed_object_browser.md) * [`ManagedObjectBrowser.get()`](esxi.managed_object_browser.md#esxi.managed_object_browser.ManagedObjectBrowser.get) * [`ManagedObjectBrowser.set()`](esxi.managed_object_browser.md#esxi.managed_object_browser.ManagedObjectBrowser.set) @@ -68,6 +71,9 @@ * [`MemShareForceSaltingConfig`](esxi.mem_share_force_salting_config.md) * [`MemShareForceSaltingConfig.get()`](esxi.mem_share_force_salting_config.md#esxi.mem_share_force_salting_config.MemShareForceSaltingConfig.get) * [`MemShareForceSaltingConfig.set()`](esxi.mem_share_force_salting_config.md#esxi.mem_share_force_salting_config.MemShareForceSaltingConfig.set) +* [`NtpConfig`](esxi.ntp_config.md) + * [`NtpConfig.get()`](esxi.ntp_config.md#esxi.ntp_config.NtpConfig.get) + * [`NtpConfig.set()`](esxi.ntp_config.md#esxi.ntp_config.NtpConfig.set) * [`NtpServiceConfig`](esxi.ntp_service_config.md) * [`NtpServiceConfig.get()`](esxi.ntp_service_config.md#esxi.ntp_service_config.NtpServiceConfig.get) * [`NtpServiceConfig.set()`](esxi.ntp_service_config.md#esxi.ntp_service_config.NtpServiceConfig.set) @@ -77,6 +83,10 @@ * [`PasswordMaxLifetimePolicy`](esxi.password_max_lifetime_policy.md) * [`PasswordMaxLifetimePolicy.get()`](esxi.password_max_lifetime_policy.md#esxi.password_max_lifetime_policy.PasswordMaxLifetimePolicy.get) * [`PasswordMaxLifetimePolicy.set()`](esxi.password_max_lifetime_policy.md#esxi.password_max_lifetime_policy.PasswordMaxLifetimePolicy.set) +* [`PasswordQualityConfig`](esxi.password_quality_config.md) + * [`PasswordQualityConfig.get()`](esxi.password_quality_config.md#esxi.password_quality_config.PasswordQualityConfig.get) + * [`PasswordQualityConfig.set()`](esxi.password_quality_config.md#esxi.password_quality_config.PasswordQualityConfig.set) + * [`PasswordQualityConfig.check_compliance()`](esxi.password_quality_config.md#esxi.password_quality_config.PasswordQualityConfig.check_compliance) * [`PasswordReuseRestrictionPolicy`](esxi.password_reuse_restriction_policy.md) * [`PasswordReuseRestrictionPolicy.get()`](esxi.password_reuse_restriction_policy.md#esxi.password_reuse_restriction_policy.PasswordReuseRestrictionPolicy.get) * [`PasswordReuseRestrictionPolicy.set()`](esxi.password_reuse_restriction_policy.md#esxi.password_reuse_restriction_policy.PasswordReuseRestrictionPolicy.set) @@ -98,15 +108,23 @@ * [`RHttpProxyFips140_2CryptConfig`](esxi.rhttpproxy_fips_140_2_crypt_config.md) * [`RHttpProxyFips140_2CryptConfig.get()`](esxi.rhttpproxy_fips_140_2_crypt_config.md#esxi.rhttpproxy_fips_140_2_crypt_config.RHttpProxyFips140_2CryptConfig.get) * [`RHttpProxyFips140_2CryptConfig.set()`](esxi.rhttpproxy_fips_140_2_crypt_config.md#esxi.rhttpproxy_fips_140_2_crypt_config.RHttpProxyFips140_2CryptConfig.set) + * [`RHttpProxyFips140_2CryptConfig.check_compliance()`](esxi.rhttpproxy_fips_140_2_crypt_config.md#esxi.rhttpproxy_fips_140_2_crypt_config.RHttpProxyFips140_2CryptConfig.check_compliance) * [`ShellServicePolicy`](esxi.shell_service_policy.md) * [`ShellServicePolicy.get()`](esxi.shell_service_policy.md#esxi.shell_service_policy.ShellServicePolicy.get) * [`ShellServicePolicy.set()`](esxi.shell_service_policy.md#esxi.shell_service_policy.ShellServicePolicy.set) * [`SlpServicePolicy`](esxi.slp_service_policy.md) * [`SlpServicePolicy.get()`](esxi.slp_service_policy.md#esxi.slp_service_policy.SlpServicePolicy.get) * [`SlpServicePolicy.set()`](esxi.slp_service_policy.md#esxi.slp_service_policy.SlpServicePolicy.set) +* [`SnmpConfig`](esxi.snmp_config.md) + * [`SnmpConfig.get()`](esxi.snmp_config.md#esxi.snmp_config.SnmpConfig.get) + * [`SnmpConfig.set()`](esxi.snmp_config.md#esxi.snmp_config.SnmpConfig.set) * [`SnmpServicePolicy`](esxi.snmp_service_policy.md) * [`SnmpServicePolicy.get()`](esxi.snmp_service_policy.md#esxi.snmp_service_policy.SnmpServicePolicy.get) * [`SnmpServicePolicy.set()`](esxi.snmp_service_policy.md#esxi.snmp_service_policy.SnmpServicePolicy.set) +* [`SshCompressionPolicy`](esxi.ssh_compression_policy.md) + * [`SshCompressionPolicy.get()`](esxi.ssh_compression_policy.md#esxi.ssh_compression_policy.SshCompressionPolicy.get) + * [`SshCompressionPolicy.set()`](esxi.ssh_compression_policy.md#esxi.ssh_compression_policy.SshCompressionPolicy.set) + * [`SshCompressionPolicy.check_compliance()`](esxi.ssh_compression_policy.md#esxi.ssh_compression_policy.SshCompressionPolicy.check_compliance) * [`SshDaemonLoginBanner`](esxi.ssh_daemon_login_banner.md) * [`SshDaemonLoginBanner.get()`](esxi.ssh_daemon_login_banner.md#esxi.ssh_daemon_login_banner.SshDaemonLoginBanner.get) * [`SshDaemonLoginBanner.set()`](esxi.ssh_daemon_login_banner.md#esxi.ssh_daemon_login_banner.SshDaemonLoginBanner.set) @@ -114,18 +132,48 @@ * [`SshFips140_2CryptConfig`](esxi.ssh_fips_140_2_crypt_config.md#esxi.ssh_fips_140_2_crypt_config.SshFips140_2CryptConfig) * [`SshFips140_2CryptConfig.get()`](esxi.ssh_fips_140_2_crypt_config.md#esxi.ssh_fips_140_2_crypt_config.SshFips140_2CryptConfig.get) * [`SshFips140_2CryptConfig.set()`](esxi.ssh_fips_140_2_crypt_config.md#esxi.ssh_fips_140_2_crypt_config.SshFips140_2CryptConfig.set) +* [`SshGatewayPortsPolicy`](esxi.ssh_gateway_ports_policy.md) + * [`SshGatewayPortsPolicy.get()`](esxi.ssh_gateway_ports_policy.md#esxi.ssh_gateway_ports_policy.SshGatewayPortsPolicy.get) + * [`SshGatewayPortsPolicy.set()`](esxi.ssh_gateway_ports_policy.md#esxi.ssh_gateway_ports_policy.SshGatewayPortsPolicy.set) + * [`SshGatewayPortsPolicy.check_compliance()`](esxi.ssh_gateway_ports_policy.md#esxi.ssh_gateway_ports_policy.SshGatewayPortsPolicy.check_compliance) +* [`SshHostBasedAuthPolicy`](esxi.ssh_host_based_auth_policy.md) + * [`SshHostBasedAuthPolicy.get()`](esxi.ssh_host_based_auth_policy.md#esxi.ssh_host_based_auth_policy.SshHostBasedAuthPolicy.get) + * [`SshHostBasedAuthPolicy.set()`](esxi.ssh_host_based_auth_policy.md#esxi.ssh_host_based_auth_policy.SshHostBasedAuthPolicy.set) + * [`SshHostBasedAuthPolicy.check_compliance()`](esxi.ssh_host_based_auth_policy.md#esxi.ssh_host_based_auth_policy.SshHostBasedAuthPolicy.check_compliance) * [`SshIgnoreRHostsPolicy`](esxi.ssh_ignore_rhosts_policy.md) * [`SshIgnoreRHostsPolicy.get()`](esxi.ssh_ignore_rhosts_policy.md#esxi.ssh_ignore_rhosts_policy.SshIgnoreRHostsPolicy.get) * [`SshIgnoreRHostsPolicy.set()`](esxi.ssh_ignore_rhosts_policy.md#esxi.ssh_ignore_rhosts_policy.SshIgnoreRHostsPolicy.set) + * [`SshIgnoreRHostsPolicy.check_compliance()`](esxi.ssh_ignore_rhosts_policy.md#esxi.ssh_ignore_rhosts_policy.SshIgnoreRHostsPolicy.check_compliance) * [`SshLoginBanner`](esxi.ssh_login_banner.md) * [`SshLoginBanner.get()`](esxi.ssh_login_banner.md#esxi.ssh_login_banner.SshLoginBanner.get) * [`SshLoginBanner.set()`](esxi.ssh_login_banner.md#esxi.ssh_login_banner.SshLoginBanner.set) +* [`SshPermitEmptyPasswordsPolicy`](esxi.ssh_permit_empty_passwords_policy.md) + * [`SshPermitEmptyPasswordsPolicy.get()`](esxi.ssh_permit_empty_passwords_policy.md#esxi.ssh_permit_empty_passwords_policy.SshPermitEmptyPasswordsPolicy.get) + * [`SshPermitEmptyPasswordsPolicy.set()`](esxi.ssh_permit_empty_passwords_policy.md#esxi.ssh_permit_empty_passwords_policy.SshPermitEmptyPasswordsPolicy.set) + * [`SshPermitEmptyPasswordsPolicy.check_compliance()`](esxi.ssh_permit_empty_passwords_policy.md#esxi.ssh_permit_empty_passwords_policy.SshPermitEmptyPasswordsPolicy.check_compliance) +* [`SshPermitTunnelPolicy`](esxi.ssh_permit_tunnel_policy.md) + * [`SshPermitTunnelPolicy.get()`](esxi.ssh_permit_tunnel_policy.md#esxi.ssh_permit_tunnel_policy.SshPermitTunnelPolicy.get) + * [`SshPermitTunnelPolicy.set()`](esxi.ssh_permit_tunnel_policy.md#esxi.ssh_permit_tunnel_policy.SshPermitTunnelPolicy.set) + * [`SshPermitTunnelPolicy.check_compliance()`](esxi.ssh_permit_tunnel_policy.md#esxi.ssh_permit_tunnel_policy.SshPermitTunnelPolicy.check_compliance) +* [`SshPermitUserEnvironmentPolicy`](esxi.ssh_permit_user_environment_policy.md) + * [`SshPermitUserEnvironmentPolicy.get()`](esxi.ssh_permit_user_environment_policy.md#esxi.ssh_permit_user_environment_policy.SshPermitUserEnvironmentPolicy.get) + * [`SshPermitUserEnvironmentPolicy.set()`](esxi.ssh_permit_user_environment_policy.md#esxi.ssh_permit_user_environment_policy.SshPermitUserEnvironmentPolicy.set) + * [`SshPermitUserEnvironmentPolicy.check_compliance()`](esxi.ssh_permit_user_environment_policy.md#esxi.ssh_permit_user_environment_policy.SshPermitUserEnvironmentPolicy.check_compliance) * [`SshPortForwardingPolicy`](esxi.ssh_port_forwarding_policy.md) * [`SshPortForwardingPolicy.get()`](esxi.ssh_port_forwarding_policy.md#esxi.ssh_port_forwarding_policy.SshPortForwardingPolicy.get) * [`SshPortForwardingPolicy.set()`](esxi.ssh_port_forwarding_policy.md#esxi.ssh_port_forwarding_policy.SshPortForwardingPolicy.set) + * [`SshPortForwardingPolicy.check_compliance()`](esxi.ssh_port_forwarding_policy.md#esxi.ssh_port_forwarding_policy.SshPortForwardingPolicy.check_compliance) * [`SshServicePolicy`](esxi.ssh_service_policy.md) * [`SshServicePolicy.get()`](esxi.ssh_service_policy.md#esxi.ssh_service_policy.SshServicePolicy.get) * [`SshServicePolicy.set()`](esxi.ssh_service_policy.md#esxi.ssh_service_policy.SshServicePolicy.set) +* [`SshStrictModePolicy`](esxi.ssh_strict_mode_policy.md) + * [`SshStrictModePolicy.get()`](esxi.ssh_strict_mode_policy.md#esxi.ssh_strict_mode_policy.SshStrictModePolicy.get) + * [`SshStrictModePolicy.set()`](esxi.ssh_strict_mode_policy.md#esxi.ssh_strict_mode_policy.SshStrictModePolicy.set) + * [`SshStrictModePolicy.check_compliance()`](esxi.ssh_strict_mode_policy.md#esxi.ssh_strict_mode_policy.SshStrictModePolicy.check_compliance) +* [`SshX11ForwardingPolicy`](esxi.ssh_x11_forwarding_policy.md) + * [`SshX11ForwardingPolicy.get()`](esxi.ssh_x11_forwarding_policy.md#esxi.ssh_x11_forwarding_policy.SshX11ForwardingPolicy.get) + * [`SshX11ForwardingPolicy.set()`](esxi.ssh_x11_forwarding_policy.md#esxi.ssh_x11_forwarding_policy.SshX11ForwardingPolicy.set) + * [`SshX11ForwardingPolicy.check_compliance()`](esxi.ssh_x11_forwarding_policy.md#esxi.ssh_x11_forwarding_policy.SshX11ForwardingPolicy.check_compliance) * [`SuppressShellWarningPolicy`](esxi.suppress_shell_warning_policy.md) * [`SuppressShellWarningPolicy.get()`](esxi.suppress_shell_warning_policy.md#esxi.suppress_shell_warning_policy.SuppressShellWarningPolicy.get) * [`SuppressShellWarningPolicy.set()`](esxi.suppress_shell_warning_policy.md#esxi.suppress_shell_warning_policy.SuppressShellWarningPolicy.set) diff --git a/docs/controllers/markdown/esxi/esxi.ntp_config.md b/docs/controllers/markdown/esxi/esxi.ntp_config.md new file mode 100644 index 0000000..d127541 --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.ntp_config.md @@ -0,0 +1,55 @@ +### *class* NtpConfig + +Bases: `BaseController` + +ESXi controller to get/set ntp configurations for hosts. + +Config Id - 147 +
+Config Title - ESXi host must configure NTP time synchronization. +
+ +Controller Metadata +```json +{ + "name": "ntp_config", + "configuration_id": "147", + "path_in_schema": "compliance_config.esxi.ntp_config", + "title": "ESXi host must configure NTP time synchronization.", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": null, + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ntp configuration for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESXi context instance. +* **Returns:** + Tuple of dict ({“protocol”: “ntp”, “server”: [“10.0.0.250”]}) and a list of errors. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set ntp configurations for esxi host based on desired values. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*dict*) – dict of { “protocol”: “ntp”, “server”: [“10.0.0.250”] }. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple diff --git a/docs/controllers/markdown/esxi/esxi.password_quality_config.md b/docs/controllers/markdown/esxi/esxi.password_quality_config.md new file mode 100644 index 0000000..0445c59 --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.password_quality_config.md @@ -0,0 +1,67 @@ +### *class* PasswordQualityConfig + +Bases: `BaseController` + +ESXi password quality control configuration. + +Config Id - 22 +
+Config Title - The ESXi host must enforce password complexity. +
+ +Controller Metadata +```json +{ + "name": "password_quality_config", + "configuration_id": "22", + "path_in_schema": "compliance_config.esxi.password_quality_config", + "title": "The ESXi host must enforce password complexity.", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": null, + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get password quality control configuration for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESX context instance. +* **Returns:** + Tuple of dict for password quality control configs and a list of error messages. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set password quality control configurations for esxi host. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*dict*) – dict of desired configs to update ESXi password quality control configurations. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – ESX context instance. + * **desired_values** (*Any*) – Desired values for rulesets. +* **Returns:** + Dict of status and list of current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.rhttpproxy_fips_140_2_crypt_config.md b/docs/controllers/markdown/esxi/esxi.rhttpproxy_fips_140_2_crypt_config.md index a01f53f..45c23b2 100644 --- a/docs/controllers/markdown/esxi/esxi.rhttpproxy_fips_140_2_crypt_config.md +++ b/docs/controllers/markdown/esxi/esxi.rhttpproxy_fips_140_2_crypt_config.md @@ -2,7 +2,8 @@ Bases: `BaseController` -ESXi controller to get/config ssh fips 140-2 validated cryptographic modules. +ESXi controller to get/config ssh fips 140-2 validated cryptographic modules. This control is applicable only +below vsphere 8.x version. Config Id - 1117
@@ -48,8 +49,20 @@ Set rhttpproxy daemon FIPS 140-2 validated cryptographic modules config for esxi * **Parameters:** * **context** (*HostContext*) – Esxi context instance. - * **desired_values** (*dict*) – boolean value True/False to update config. + * **desired_values** (*bool*) – boolean value True/False to update config. * **Returns:** Tuple of “status” and list of error messages. * **Return type:** Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*bool*) – boolean value True/False to update config. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.snmp_config.md b/docs/controllers/markdown/esxi/esxi.snmp_config.md new file mode 100644 index 0000000..56e77b7 --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.snmp_config.md @@ -0,0 +1,55 @@ +### *class* SnmpConfig + +Bases: `BaseController` + +ESXi controller to get/set snmp configurations for ESXi host. + +Config Id - 1114 +
+Config Title - SNMP must be configured properly on the ESXi host. +
+ +Controller Metadata +```json +{ + "name": "snmp_config", + "configuration_id": "1114", + "path_in_schema": "compliance_config.esxi.snmp_config", + "title": "SNMP must be configured properly on the ESXi host.", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": null, + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get snmp configs for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESXi context instance. +* **Returns:** + Tuple of boolean value True/False and a list of errors. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set snmp config for esxi host based on desired value. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*dict*) – boolean value True/False to update config. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple diff --git a/docs/controllers/markdown/esxi/esxi.ssh_compression_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_compression_policy.md new file mode 100644 index 0000000..6a7b22d --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.ssh_compression_policy.md @@ -0,0 +1,68 @@ +### *class* SshCompressionPolicy + +Bases: `BaseController` + +ESXi ssh host compression settings. +The control is automated only for vsphere 8.x and above. No remediation support as the property is no configurable. + +Config Id - 12 +
+Config Title - Disallow compression for the ESXi host SSH daemon. +
+ +Controller Metadata +```json +{ + "name": "ssh_compression", + "configuration_id": "12", + "path_in_schema": "compliance_config.esxi.ssh_compression", + "title": "Disallow compression for the ESXi host SSH daemon", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": "REMEDIATION_SKIPPED", + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ssh host compression settings for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESX context instance. +* **Returns:** + Tuple of str for ‘compression’ value and a list of error messages. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set ssh host compression settings for esxi host. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*str*) – Desired value for ‘compression’ config. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the ssh host compression settings. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.ssh_gateway_ports_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_gateway_ports_policy.md new file mode 100644 index 0000000..d671fcd --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.ssh_gateway_ports_policy.md @@ -0,0 +1,67 @@ +### *class* SshGatewayPortsPolicy + +Bases: `BaseController` + +ESXi ssh gateway ports configuration. The control is automated only for vsphere 8.x and above. + +Config Id - 13 +
+Config Title - ESXi host SSH daemon does not contain gateway ports. +
+ +Controller Metadata +```json +{ + "name": "ssh_gateway_ports", + "configuration_id": "13", + "path_in_schema": "compliance_config.esxi.ssh_gateway_ports", + "title": "ESXi host SSH daemon does not contain gateway ports.", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": null, + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ssh host gateway ports policy for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESX context instance. +* **Returns:** + Tuple of str for ‘gatewayports’ value and a list of error messages. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set ssh host gateway ports policy for esxi host. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*str*) – Desired value for ‘gatewayports’ config. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the host gateway ports config. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.ssh_host_based_auth_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_host_based_auth_policy.md new file mode 100644 index 0000000..23fa972 --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.ssh_host_based_auth_policy.md @@ -0,0 +1,67 @@ +### *class* SshHostBasedAuthPolicy + +Bases: `BaseController` + +ESXi ssh host based authentication configuration. The control is automated only for vsphere 8.x and above. + +Config Id - 4 +
+Config Title - ESXi host SSH daemon does not allow host-based authentication. +
+ +Controller Metadata +```json +{ + "name": "ssh_host_based_authentication", + "configuration_id": "4", + "path_in_schema": "compliance_config.esxi.ssh_host_based_authentication", + "title": "ESXi host SSH daemon does not allow host-based authentication.", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": null, + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ssh host based auth policy for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESX context instance. +* **Returns:** + Tuple of str for ‘hostbasedauthentication’ value and a list of error messages. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set ssh host based auth policy for esxi host. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*str*) – Desired value for ‘hostbasedauthentication’ config. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the ssh host based authentication config. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.ssh_ignore_rhosts_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_ignore_rhosts_policy.md index 72d1041..733f127 100644 --- a/docs/controllers/markdown/esxi/esxi.ssh_ignore_rhosts_policy.md +++ b/docs/controllers/markdown/esxi/esxi.ssh_ignore_rhosts_policy.md @@ -2,7 +2,7 @@ Bases: `BaseController` -ESXi ignore ssh rhosts configuration. +ESXi ignore ssh rhosts configuration. The control is automated only for vsphere 8.x and above. Config Id - 3
@@ -53,3 +53,15 @@ Set ssh ignore rhosts policy for esxi host. Tuple of “status” and list of error messages. * **Return type:** Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the host ignore rhosts config. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.ssh_permit_empty_passwords_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_permit_empty_passwords_policy.md new file mode 100644 index 0000000..799513c --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.ssh_permit_empty_passwords_policy.md @@ -0,0 +1,68 @@ +### *class* SshPermitEmptyPasswordsPolicy + +Bases: `BaseController` + +ESXi ssh host permit empty passwords settings. +The control is automated only for vsphere 8.x and above. No remediation support as the property is no configurable. + +Config Id - 6 +
+Config Title - ESXi host SSH daemon rejects authentication using an empty password. +
+ +Controller Metadata +```json +{ + "name": "ssh_permit_empty_passwords", + "configuration_id": "6", + "path_in_schema": "compliance_config.esxi.ssh_permit_empty_passwords", + "title": "ESXi host SSH daemon rejects authentication using an empty password", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": "REMEDIATION_SKIPPED", + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ssh host permit empty passwords settings for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESX context instance. +* **Returns:** + Tuple of str for ‘permitemptypasswords’ value and a list of error messages. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set ssh host permit empty passwords settings for esxi host. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*str*) – Desired value for ‘permitemptypasswords’ config. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the ssh host permit empty passwords settings. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.ssh_permit_tunnel_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_permit_tunnel_policy.md new file mode 100644 index 0000000..fe45b2b --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.ssh_permit_tunnel_policy.md @@ -0,0 +1,67 @@ +### *class* SshPermitTunnelPolicy + +Bases: `BaseController` + +ESXi ssh permit tunnel configuration. The control is automated only for vsphere 8.x and above. + +Config Id - 16 +
+Config Title - ESXi host SSH daemon refuses tunnels. +
+ +Controller Metadata +```json +{ + "name": "ssh_permit_tunnel", + "configuration_id": "16", + "path_in_schema": "compliance_config.esxi.ssh_permit_tunnel", + "title": "ESXi host SSH daemon refuses tunnels.", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": null, + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ssh host permit tunnel policy for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESX context instance. +* **Returns:** + Tuple of str for ‘permittunnel’ value and a list of error messages. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set ssh host permit tunnel policy for esxi host. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*str*) – Desired value for ‘permittunnel’ config. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the host permit tunnel config. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.ssh_permit_user_environment_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_permit_user_environment_policy.md new file mode 100644 index 0000000..8044cc7 --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.ssh_permit_user_environment_policy.md @@ -0,0 +1,67 @@ +### *class* SshPermitUserEnvironmentPolicy + +Bases: `BaseController` + +ESXi ssh host permit user environment configuration. The control is automated only for vsphere 8.x and above. + +Config Id - 7 +
+Config Title - ESXi host SSH daemon does not permit user environment settings. +
+ +Controller Metadata +```json +{ + "name": "ssh_permit_user_environment", + "configuration_id": "7", + "path_in_schema": "compliance_config.esxi.ssh_permit_user_environment", + "title": "ESXi host SSH daemon does not permit user environment settings.", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": null, + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ssh host permit user environment policy for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESX context instance. +* **Returns:** + Tuple of str for ‘permituserenvironment’ value and a list of error messages. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set ssh host permit user environment policy for esxi host. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*str*) – Desired value for ‘permituserenvironment’ config. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the ssh host permit user environment config. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.ssh_port_forwarding_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_port_forwarding_policy.md index b85ca96..7474a6c 100644 --- a/docs/controllers/markdown/esxi/esxi.ssh_port_forwarding_policy.md +++ b/docs/controllers/markdown/esxi/esxi.ssh_port_forwarding_policy.md @@ -2,7 +2,7 @@ Bases: `BaseController` -ESXi ssh port forwarding configuration. +ESXi ssh port forwarding configuration. The control is automated only for vsphere 8.x and above. Config Id - 1111
@@ -53,3 +53,15 @@ Set ssh port forwarding policy for esxi host. Tuple of “status” and list of error messages. * **Return type:** Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the ‘AllowTcpForwarding’ config. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.ssh_strict_mode_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_strict_mode_policy.md new file mode 100644 index 0000000..d48c840 --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.ssh_strict_mode_policy.md @@ -0,0 +1,68 @@ +### *class* SshStrictModePolicy + +Bases: `BaseController` + +ESXi ssh host strict mode settings. +The control is automated only for vsphere 8.x and above. No remediation support as the property is no configurable. + +Config Id - 11 +
+Config Title - ESXi host SSH daemon performs strict mode checking of home directory configuration files. +
+ +Controller Metadata +```json +{ + "name": "ssh_strict_mode", + "configuration_id": "11", + "path_in_schema": "compliance_config.esxi.ssh_strict_mode", + "title": "ESXi host SSH daemon performs strict mode checking of home directory configuration files", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": "REMEDIATION_SKIPPED", + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ssh host strict mode settings for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESX context instance. +* **Returns:** + Tuple of str for ‘strictmodes’ value and a list of error messages. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set ssh host strict mode settings for esxi host. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*str*) – Desired value for ‘strictmodes’ config. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the ssh host strict mode settings. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/esxi/esxi.ssh_x11_forwarding_policy.md b/docs/controllers/markdown/esxi/esxi.ssh_x11_forwarding_policy.md new file mode 100644 index 0000000..d00e682 --- /dev/null +++ b/docs/controllers/markdown/esxi/esxi.ssh_x11_forwarding_policy.md @@ -0,0 +1,68 @@ +### *class* SshX11ForwardingPolicy + +Bases: `BaseController` + +ESXi ssh host x11 forwarding settings. +The control is automated only for vsphere 8.x and above. No remediation support as the property is no configurable. + +Config Id - 14 +
+Config Title - ESXi host SSH daemon refuses X11 forwarding. +
+ +Controller Metadata +```json +{ + "name": "ssh_x11_forwarding", + "configuration_id": "14", + "path_in_schema": "compliance_config.esxi.ssh_x11_forwarding", + "title": "ESXi host SSH daemon refuses X11 forwarding", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "esxi" + ], + "components": [], + "status": "ENABLED", + "impact": "REMEDIATION_SKIPPED", + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ssh host x11 forwarding settings for esxi host. + +* **Parameters:** + **context** (*HostContext*) – ESX context instance. +* **Returns:** + Tuple of str for ‘x11forwarding’ value and a list of error messages. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set ssh host x11 forwarding settings for esxi host. + +* **Parameters:** + * **context** (*HostContext*) – Esxi context instance. + * **desired_values** (*str*) – Desired value for ‘x11forwarding’ config. +* **Returns:** + Tuple of “status” and list of error messages. +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of current configuration against provided desired values. + +* **Parameters:** + * **context** (*HostContext*) – Product context instance. + * **desired_values** (*str*) – Desired value for the ssh host x11 forwarding settings. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + dict diff --git a/docs/controllers/markdown/index.md b/docs/controllers/markdown/index.md index 611fdb8..8600c41 100644 --- a/docs/controllers/markdown/index.md +++ b/docs/controllers/markdown/index.md @@ -22,12 +22,15 @@ * [`LockdownDcuiAccessUsers`](esxi/esxi.lockdown_dcui_access_users.md) * [`LockdownModeConfig`](esxi/esxi.lockdown_mode_config.md) * [`LockdownModeExceptionUsers`](esxi/esxi.lockdown_mode_exception_users.md) + * [`LogLocationConfig`](esxi/esxi.log_location_config.md) * [`ManagedObjectBrowser`](esxi/esxi.managed_object_browser.md) * [`MaxFailedLoginAttempts`](esxi/esxi.max_failed_login_attempts.md) * [`MemShareForceSaltingConfig`](esxi/esxi.mem_share_force_salting_config.md) + * [`NtpConfig`](esxi/esxi.ntp_config.md) * [`NtpServiceConfig`](esxi/esxi.ntp_service_config.md) * [`NtpServiceStartupPolicy`](esxi/esxi.ntp_service_startup_policy.md) * [`PasswordMaxLifetimePolicy`](esxi/esxi.password_max_lifetime_policy.md) + * [`PasswordQualityConfig`](esxi/esxi.password_quality_config.md) * [`PasswordReuseRestrictionPolicy`](esxi/esxi.password_reuse_restriction_policy.md) * [`PgVssAllowPromiscuousMode`](esxi/esxi.pg_vss_allow_promiscuous_mode.md) * [`PgVssForgedTransmitsAccept`](esxi/esxi.pg_vss_forged_transmits_accept.md) @@ -36,14 +39,23 @@ * [`RHttpProxyFips140_2CryptConfig`](esxi/esxi.rhttpproxy_fips_140_2_crypt_config.md) * [`ShellServicePolicy`](esxi/esxi.shell_service_policy.md) * [`SlpServicePolicy`](esxi/esxi.slp_service_policy.md) + * [`SnmpConfig`](esxi/esxi.snmp_config.md) * [`SnmpServicePolicy`](esxi/esxi.snmp_service_policy.md) + * [`SshCompressionPolicy`](esxi/esxi.ssh_compression_policy.md) * [`SshDaemonLoginBanner`](esxi/esxi.ssh_daemon_login_banner.md) * [`SSH_FIPS_140_2_CONFIG_SET()`](esxi/esxi.ssh_fips_140_2_crypt_config.md) * [`SshFips140_2CryptConfig`](esxi/esxi.ssh_fips_140_2_crypt_config.md#esxi.ssh_fips_140_2_crypt_config.SshFips140_2CryptConfig) + * [`SshGatewayPortsPolicy`](esxi/esxi.ssh_gateway_ports_policy.md) + * [`SshHostBasedAuthPolicy`](esxi/esxi.ssh_host_based_auth_policy.md) * [`SshIgnoreRHostsPolicy`](esxi/esxi.ssh_ignore_rhosts_policy.md) * [`SshLoginBanner`](esxi/esxi.ssh_login_banner.md) + * [`SshPermitEmptyPasswordsPolicy`](esxi/esxi.ssh_permit_empty_passwords_policy.md) + * [`SshPermitTunnelPolicy`](esxi/esxi.ssh_permit_tunnel_policy.md) + * [`SshPermitUserEnvironmentPolicy`](esxi/esxi.ssh_permit_user_environment_policy.md) * [`SshPortForwardingPolicy`](esxi/esxi.ssh_port_forwarding_policy.md) * [`SshServicePolicy`](esxi/esxi.ssh_service_policy.md) + * [`SshStrictModePolicy`](esxi/esxi.ssh_strict_mode_policy.md) + * [`SshX11ForwardingPolicy`](esxi/esxi.ssh_x11_forwarding_policy.md) * [`SuppressShellWarningPolicy`](esxi/esxi.suppress_shell_warning_policy.md) * [`SyslogEnforceSslCertificates`](esxi/esxi.syslog_enforce_ssl_certificates.md) * [`SyslogStrictX509Compliance`](esxi/esxi.syslog_strict_x509_compliance.md) @@ -90,6 +102,7 @@ * [`DVSNetworkIOControlPolicy`](vcenter/vcenter.dvs_network_io_control_policy.md) * [`DvsPortGroupNetflowConfig`](vcenter/vcenter.dvs_pg_netflow_config.md) * [`H5ClientSessionTimeoutConfig`](vcenter/vcenter.h5_client_session_timeout_config.md) + * [`IPBasedStoragePortGroupConfig`](vcenter/vcenter.ip_based_storage_port_group_config.md) * [`LdapIdentitySourceConfig`](vcenter/vcenter.ldap_identity_source_config.md) * [`LogonBannerConfig`](vcenter/vcenter.logon_banner_config.md) * [`ManagedObjectBrowser`](vcenter/vcenter.managed_object_browser.md) diff --git a/docs/controllers/markdown/vcenter/vcenter.ip_based_storage_port_group_config.md b/docs/controllers/markdown/vcenter/vcenter.ip_based_storage_port_group_config.md new file mode 100644 index 0000000..c51acf0 --- /dev/null +++ b/docs/controllers/markdown/vcenter/vcenter.ip_based_storage_port_group_config.md @@ -0,0 +1,131 @@ +### *class* IPBasedStoragePortGroupConfig + +Bases: `BaseController` + +Class for ip based storage port groups vlan isolation config with get and set methods. + +Remediation is not supported as it involves different configurations on vsan, iscsi and NFS. Any drifts should +be analyzed based on compliance report and manually remediated. + +Config Id - 1225 +
+Config Title - Isolate all IP-based storage traffic on distributed switches from other traffic types. +
+ +Controller Metadata +```json +{ + "name": "ip_based_storage_port_group_config", + "configuration_id": "1225", + "path_in_schema": "compliance_config.vcenter.ip_based_storage_port_group_config", + "title": "Isolate all IP-based storage traffic on distributed switches from other traffic types", + "tags": [], + "version": "1.0.0", + "since": "", + "products": [ + "vcenter" + ], + "components": [], + "status": "ENABLED", + "impact": "REMEDIATION_SKIPPED", + "scope": "", + "type": "COMPLIANCE", + "functional_test_targets": [] +} +``` + +#### get(context) + +Get ip based storage distributed port groups vlan configurations for the vCenter. + +* **Parameters:** + **context** (*VcenterContext*) – Product context instance. +* **Returns:** + A tuple containing a dictionary to store ip based storage port groups data and a list of error messages if any. +* **Return type:** + Tuple + +#### set(context, desired_values) + +Set method is not implemented as this control requires user intervention to remediate. + +* **Parameters:** + * **context** (*VcenterContext*) – Product context instance. + * **desired_values** (*dict*) – Desired value for ip based storage port groups. +* **Returns:** + Dict of status (RemediateStatus.SKIPPED) and errors if any +* **Return type:** + Tuple + +#### check_compliance(context, desired_values) + +Check compliance of all ip based storage distributed port groups vlan isolation configuration. + +Sample desired values +
+```json +{ + "__GLOBAL__": { + "is_dedicated_vlan": true + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch1", + "port_group_name": "PG2", + "is_dedicated_vlan": true, + } + ] +} +``` + +Sample check compliance response +
+```json +{ + "status": "NON_COMPLIANT", + "current": [ + { + "is_dedicated_vlan": false, + "switch_name": "Switch1", + "port_group_name": "PG2" + }, + { + "is_dedicated_vlan": false, + "switch_name": "Switch1", + "port_group_name": "PG1" + } + ], + "desired": [ + { + "is_dedicated_vlan": true, + "switch_name": "Switch1", + "port_group_name": "PG2" + }, + { + "is_dedicated_vlan": true, + "switch_name": "Switch1", + "port_group_name": "PG1" + } + ] +} +``` + +* **Parameters:** + * **context** (*VcenterContext*) – Product context instance. + * **desired_values** (*Dict*) – Desired values for ip based stotage port groups. +* **Returns:** + Dict of status and current/desired value(for non_compliant) or errors (for failure). +* **Return type:** + Dict + +#### remediate(context, desired_values) + +Remediate is not implemented as this control requires manual intervention. + +* **Parameters:** + * **context** (*VcenterContext*) – Product context instance. + * **desired_values** (*Dict*) – Desired value for the ip based storage port groups. +* **Returns:** + Dict of status (RemediateStatus.SKIPPED) and errors if any +* **Return type:** + *Dict* diff --git a/docs/controllers/markdown/vcenter/vcenter.ldap_identity_source_config.md b/docs/controllers/markdown/vcenter/vcenter.ldap_identity_source_config.md index a391352..201aa3c 100644 --- a/docs/controllers/markdown/vcenter/vcenter.ldap_identity_source_config.md +++ b/docs/controllers/markdown/vcenter/vcenter.ldap_identity_source_config.md @@ -52,7 +52,7 @@ Refer to Jira : VCFSC-147 * **Parameters:** * **context** (*VcenterContext*) – Product context instance. - * **desired_values** (*String* *or* *list* *of* *strings*) – Desired value for the certificate authority + * **desired_values** (*String* *or* *list* *of* *strings*) – Desired value for ldap accounts * **Returns:** Dict of status (RemediateStatus.SKIPPED) and errors if any * **Return type:** diff --git a/docs/controllers/markdown/vcenter/vcenter.md b/docs/controllers/markdown/vcenter/vcenter.md index c63c55e..60bd673 100644 --- a/docs/controllers/markdown/vcenter/vcenter.md +++ b/docs/controllers/markdown/vcenter/vcenter.md @@ -76,6 +76,11 @@ * [`H5ClientSessionTimeoutConfig`](vcenter.h5_client_session_timeout_config.md) * [`H5ClientSessionTimeoutConfig.get()`](vcenter.h5_client_session_timeout_config.md#vcenter.h5_client_session_timeout_config.H5ClientSessionTimeoutConfig.get) * [`H5ClientSessionTimeoutConfig.set()`](vcenter.h5_client_session_timeout_config.md#vcenter.h5_client_session_timeout_config.H5ClientSessionTimeoutConfig.set) +* [`IPBasedStoragePortGroupConfig`](vcenter.ip_based_storage_port_group_config.md) + * [`IPBasedStoragePortGroupConfig.get()`](vcenter.ip_based_storage_port_group_config.md#vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig.get) + * [`IPBasedStoragePortGroupConfig.set()`](vcenter.ip_based_storage_port_group_config.md#vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig.set) + * [`IPBasedStoragePortGroupConfig.check_compliance()`](vcenter.ip_based_storage_port_group_config.md#vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig.check_compliance) + * [`IPBasedStoragePortGroupConfig.remediate()`](vcenter.ip_based_storage_port_group_config.md#vcenter.ip_based_storage_port_group_config.IPBasedStoragePortGroupConfig.remediate) * [`LdapIdentitySourceConfig`](vcenter.ldap_identity_source_config.md) * [`LdapIdentitySourceConfig.get()`](vcenter.ldap_identity_source_config.md#vcenter.ldap_identity_source_config.LdapIdentitySourceConfig.get) * [`LdapIdentitySourceConfig.set()`](vcenter.ldap_identity_source_config.md#vcenter.ldap_identity_source_config.LdapIdentitySourceConfig.set) diff --git a/docs/controllers/markdown/vcenter/vcenter.vm_migrate_encryption_policy.md b/docs/controllers/markdown/vcenter/vcenter.vm_migrate_encryption_policy.md index a225c3e..6b8f52d 100644 --- a/docs/controllers/markdown/vcenter/vcenter.vm_migrate_encryption_policy.md +++ b/docs/controllers/markdown/vcenter/vcenter.vm_migrate_encryption_policy.md @@ -82,6 +82,8 @@ Sample Get call output #### set(context, desired_values) Set VM migrate Encryption policies for all Virtual machines. +If a VM is “template”, mark it as “VM” before remediation, and mark it +back to “template” after remediation. Recommended value for migrate encryption: “opportunistic” | “required”
diff --git a/docs/controllers/markdown/vcenter/vcenter.vsan_iscsi_targets_mchap_config.md b/docs/controllers/markdown/vcenter/vcenter.vsan_iscsi_targets_mchap_config.md index 57aeb01..4d9e6a1 100644 --- a/docs/controllers/markdown/vcenter/vcenter.vsan_iscsi_targets_mchap_config.md +++ b/docs/controllers/markdown/vcenter/vcenter.vsan_iscsi_targets_mchap_config.md @@ -49,7 +49,7 @@ Refer to Jira : VCFSC-202 and VCFSC-274 * **Parameters:** * **context** (*VcenterContext*) – Product context instance. - * **desired_values** (*String* *or* *list* *of* *strings*) – Desired value for the certificate authority + * **desired_values** (*String* *or* *list* *of* *strings*) – Desired value for iscsi auth configuration * **Returns:** Dict of status (RemediateStatus.SKIPPED) and errors if any * **Return type:** @@ -74,7 +74,7 @@ Sample desired_values spec * **Parameters:** * **context** – Product context instance. - * **desired_values** – Desired value for the certificate authority. + * **desired_values** – Desired value for iscsi auth configuration. * **Returns:** Dict of status and current/desired value or errors (for failure). * **Return type:** diff --git a/docs/controllers/vcenter/vcenter.ip_based_storage_port_group_config.rst b/docs/controllers/vcenter/vcenter.ip_based_storage_port_group_config.rst new file mode 100644 index 0000000..1f8bd2f --- /dev/null +++ b/docs/controllers/vcenter/vcenter.ip_based_storage_port_group_config.rst @@ -0,0 +1,4 @@ +.. automodule:: vcenter.ip_based_storage_port_group_config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/controllers/vcenter/vcenter.rst b/docs/controllers/vcenter/vcenter.rst index bab7fc7..7782a1e 100644 --- a/docs/controllers/vcenter/vcenter.rst +++ b/docs/controllers/vcenter/vcenter.rst @@ -29,6 +29,7 @@ Submodules vcenter.dvs_network_io_control_policy vcenter.dvs_pg_netflow_config vcenter.h5_client_session_timeout_config + vcenter.ip_based_storage_port_group_config vcenter.ldap_identity_source_config vcenter.logon_banner_config vcenter.managed_object_browser diff --git a/docs/instructions-to-create-new-controllers.md b/docs/instructions-to-create-new-controllers.md index d7ed279..87aa8fa 100644 --- a/docs/instructions-to-create-new-controllers.md +++ b/docs/instructions-to-create-new-controllers.md @@ -29,7 +29,7 @@ This process involves figuring out the publicly available APIs or scripts that c ##### [NOTE: This is only applicable for `COMPLIANCE` type controls] -Specification for each control should be defined in the [compliance schema](../config_modules_vmware/schemas/compliance_reference_schema.json). This schema serves as a standardized representation of the control's value, and the controller implementation for that control would be implemented based on the schema. The schema is based on [JSONSchema specification](https://json-schema.org/specification). +Specification for each control should be defined in the [compliance schema](../config_modules_vmware/schemas/compliance_reference_schema.json). This schema serves as a standardized representation of the control's value, and the controller implementation for that control would be implemented based on the schema. The schema is based on [JSONSchema specification](https://json-schema.org/specification) and documentation about the compliance reference schema is captured [here](../docs/compliance-schema-documentation.md) #### Things to follow while writing schema spec for a new control diff --git a/docs/openapi.json b/docs/openapi.json index 8da58ab..0a4813c 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -50,6 +50,116 @@ } } }, + "/config-modules/api/vcenter/schema/v1/get": { + "post": { + "tags": [ + "vcenter" + ], + "summary": "Get Schema", + "description": "Endpoint to get schema for a given product.", + "operationId": "get_schema_config_modules_api_vcenter_schema_v1_get_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchema" + }, + "examples": { + "default": { + "summary": "default", + "description": "Sample request body", + "value": { + "target": { + "hostname": "vcenter-1.vsphere.local", + "auth": [ + { + "username": "sso_username", + "password": "sso_password", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD..." + } + ] + } + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful get schema response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchemaResponsePayload" + }, + "example": { + "schema_version": "1.0", + "id": "null", + "name": "Get Schema", + "timestamp": "2024-08-05T22:46:05.210604", + "description": "Schema", + "status": "SUCCESS", + "result": { + "json_schema": { + "sample": "test" + } + }, + "errors": "null", + "target": { + "hostname": "10.168.160.16", + "type": "vcenter" + } + } + } + } + }, + "500": { + "description": "Internal error response.", + "content": { + "application/json": { + "example": { + "schema_version": "1.0", + "name": "Get Schema", + "timestamp": "2024-08-06T20:34:06.343944", + "description": "Exception fetching schema.", + "status": "FAILED", + "errors": [ + { + "timestamp": "2024-08-06T20:34:06.343944", + "source": { + "server": "192.168.1.1", + "type": "ConfigModules", + "endpoint": "/config-modules/api/vcenter/schema/v1/get" + }, + "error": { + "message": "unable to reach vc" + } + } + ], + "target": { + "hostname": "vcenter-1.vsphere.local", + "type": "vcenter" + } + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/config-modules/api/vcenter/configuration/v1/get": { "post": { "tags": [ @@ -82,9 +192,9 @@ } } }, - "template": { - "summary": "With template", - "description": "This is to retrieve only vcenter configuration based on the template.", + "template_vc_8x": { + "summary": "With template for VC 8.X", + "description": "This is to retrieve only vcenter configuration based on the template using VC Tech preview APIs.", "value": { "target": { "hostname": "vcenter-1.vsphere.local", @@ -111,6 +221,30 @@ } } }, + "template_vc_9+": { + "summary": "With template for VC 9+", + "description": "This is to retrieve only vcenter configuration based on the template using VC Public APIs.", + "value": { + "target": { + "hostname": "vcenter-1.vsphere.local", + "auth": [ + { + "username": "sso_username", + "password": "sso_password", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD..." + } + ] + }, + "template": { + "config": { + "vcenter": { + "inventory": {} + } + } + } + } + }, "invalid": { "summary": "Missing required parameters", "description": "Same request body with missing hostname.", @@ -156,6 +290,99 @@ } } }, + "/config-modules/api/vcenter/configuration/v1/validate": { + "post": { + "tags": [ + "vcenter" + ], + "summary": "Validate Configuration", + "description": "Endpoint to validate vcenter configuration.", + "operationId": "validate_configuration_config_modules_api_vcenter_configuration_v1_validate_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateConfigurationRequest" + }, + "examples": { + "default": { + "summary": "With spec for VC 9+", + "description": "Sample request body", + "value": { + "target": { + "hostname": "vcenter-1.vsphere.local", + "auth": [ + { + "username": "sso_username", + "password": "sso_password", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD..." + } + ] + }, + "input_spec": { + "config": { + "vcenter": { + "inventory": { + "folders": [ + { + "path": "/vcqaDC/testNetworkFolder", + "permissions": [], + "type": "HOST" + } + ], + "distributed_virtual_portgroups": [] + } + } + } + } + } + }, + "invalid": { + "summary": "Missing required parameters", + "description": "Same request body with missing hostname.", + "value": { + "target": { + "auth": [ + { + "username": "sso_username", + "password": "sso_password", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD..." + } + ] + } + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The configuration spec.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateResponsePayload" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/config-modules/api/vcenter/configuration/v1/scan-drifts": { "post": { "tags": [ @@ -171,8 +398,8 @@ "$ref": "#/components/schemas/ScanDriftsRequest" }, "examples": { - "default": { - "summary": "default", + "spec_vc_8x": { + "summary": "With spec for VC 8.X", "description": "Sample request body", "value": { "target": { @@ -217,6 +444,39 @@ } } } + }, + "spec_vc_9+": { + "summary": "With spec for VC 9+", + "description": "Sample request body", + "value": { + "target": { + "hostname": "vcenter-1.vsphere.local", + "auth": [ + { + "username": "sso_username", + "password": "sso_password", + "type": "SSO", + "ssl_thumbprint": "AA:BB:CC:DD..." + } + ] + }, + "input_spec": { + "config": { + "vcenter": { + "inventory": { + "folders": [ + { + "path": "/vcqaDC/testNetworkFolder", + "permissions": [], + "type": "HOST" + } + ], + "distributed_virtual_portgroups": [] + } + } + } + } + } } } } @@ -232,7 +492,7 @@ "$ref": "#/components/schemas/DriftResponsePayload" }, "example": { - "schema_version": "1.0-DRAFT", + "schema_version": "1.0", "id": "2bcaa939-e6c2-4347-808f-ad90debc20ae", "name": "config_modules_vmware.controllers.vcenter.vc_profile", "timestamp": "2024-03-28T23:03:19.472Z", @@ -279,7 +539,7 @@ "content": { "application/json": { "example": { - "schema_version": "1.0-DRAFT", + "schema_version": "1.0", "id": "2bcaa939-e6c2-4347-808f-ad90debc20ae", "name": "config_modules_vmware.controllers.vcenter.vc_profile", "timestamp": "2024-03-28T23:03:19.472Z", @@ -344,12 +604,12 @@ "version": { "type": "string", "title": "Version", - "default": "0.0.8" + "default": "0.14.7.0" }, "author": { "type": "string", "title": "Author", - "default": "VMware, Inc." + "default": "Broadcom" } }, "type": "object", @@ -482,7 +742,7 @@ "type": "string", "title": "Schema Version", "description": "The drift response spec.", - "default": "1.0-DRAFT" + "default": "1.0" }, "id": { "type": "string", @@ -508,7 +768,7 @@ "status": { "allOf": [ { - "$ref": "#/components/schemas/Status" + "$ref": "#/components/schemas/config_modules_vmware__framework__models__output_models__configuration_drift_response__Status" } ], "description": "The status of the function." @@ -516,7 +776,7 @@ "result": { "allOf": [ { - "$ref": "#/components/schemas/Result" + "$ref": "#/components/schemas/config_modules_vmware__services__apis__models__drift_payload__Result" } ], "description": "The drifts." @@ -618,7 +878,7 @@ "type": "string", "title": "Schema Version", "description": "The get configuration spec.", - "default": "1.0-DRAFT" + "default": "1.0" }, "name": { "type": "string", @@ -708,6 +968,24 @@ "title": "GetConfigurationRequest", "description": "Class to represent the request format of a get configuration API call." }, + "GetSchema": { + "properties": { + "target": { + "allOf": [ + { + "$ref": "#/components/schemas/RequestTarget" + } + ], + "description": "The product target information." + } + }, + "type": "object", + "required": [ + "target" + ], + "title": "GetSchema", + "description": "Class to represent the request format of a get schema API call." + }, "HTTPValidationError": { "properties": { "detail": { @@ -806,37 +1084,6 @@ "title": "RequestTarget", "description": "Class to represent the request target." }, - "Result": { - "properties": { - "additions": { - "items": { - "$ref": "#/components/schemas/ConfigAddition" - }, - "type": "array", - "title": "Additions", - "description": "The configurations that needs to be added to the product." - }, - "modifications": { - "items": { - "$ref": "#/components/schemas/ConfigModification" - }, - "type": "array", - "title": "Modifications", - "description": "The configurations that needs to be modified on the product." - }, - "deletions": { - "items": { - "$ref": "#/components/schemas/ConfigDeletion" - }, - "type": "array", - "title": "Deletions", - "description": "The configurations that needs to be deleted from the product" - } - }, - "type": "object", - "title": "Result", - "description": "Class to represent the results of the scan drift API call." - }, "ScanDriftsRequest": { "properties": { "target": { @@ -861,15 +1108,76 @@ "title": "ScanDriftsRequest", "description": "Class to represent the request format of a scan drifts API call." }, - "Status": { - "type": "string", - "enum": [ - "COMPLIANT", - "NON_COMPLIANT", - "FAILED" + "SchemaResponsePayload": { + "properties": { + "schema_version": { + "type": "string", + "title": "Schema Version", + "description": "The schema response spec.", + "default": "1.0" + }, + "id": { + "type": "string", + "title": "Id", + "description": "The uuid if applicable." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the function.", + "default": "Get Schema" + }, + "timestamp": { + "type": "string", + "title": "Timestamp", + "description": "The timestamp of drift calculation in ISO format (YYYY-MM-DDTHH:MM:SS.mmm)" + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description of the function." + }, + "status": { + "allOf": [ + { + "$ref": "#/components/schemas/config_modules_vmware__services__apis__models__schema_payload__Status" + } + ], + "description": "The status of the function." + }, + "result": { + "allOf": [ + { + "$ref": "#/components/schemas/config_modules_vmware__services__apis__models__schema_payload__Result" + } + ], + "description": "The drifts." + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Error" + }, + "type": "array", + "title": "Errors", + "description": "Errors during schema retrieval." + }, + "target": { + "allOf": [ + { + "$ref": "#/components/schemas/Target" + } + ], + "description": "The targeted product." + } + }, + "type": "object", + "required": [ + "timestamp", + "status", + "target" ], - "title": "Status", - "description": "Drift status enum" + "title": "SchemaResponsePayload", + "description": "Class to represent the response format of a schema API call." }, "Target": { "properties": { @@ -894,6 +1202,132 @@ "title": "Target", "description": "Class to represent a target." }, + "ValidateConfigurationRequest": { + "properties": { + "target": { + "allOf": [ + { + "$ref": "#/components/schemas/RequestTarget" + } + ], + "description": "The product target information." + }, + "input_spec": { + "type": "object", + "title": "Input Spec", + "description": "Desired state input spec, based on the product schema." + } + }, + "type": "object", + "required": [ + "target", + "input_spec" + ], + "title": "ValidateConfigurationRequest", + "description": "Class to represent the request format of a validate configuration API call." + }, + "ValidateResponsePayload": { + "properties": { + "schema_version": { + "type": "string", + "title": "Schema Version", + "description": "The validate response spec.", + "default": "1.0" + }, + "id": { + "type": "string", + "title": "Id", + "description": "The uuid if applicable." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the function.", + "default": "Validate Configuration" + }, + "timestamp": { + "type": "string", + "title": "Timestamp", + "description": "The timestamp of validate operation in ISO format (YYYY-MM-DDTHH:MM:SS.mmm)" + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description of the function." + }, + "status": { + "allOf": [ + { + "$ref": "#/components/schemas/config_modules_vmware__services__apis__models__validate_payload__Status" + } + ], + "description": "The status of the function." + }, + "result": { + "allOf": [ + { + "$ref": "#/components/schemas/ValidateResult" + } + ], + "description": "The validate response." + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Error" + }, + "type": "array", + "title": "Errors", + "description": "Errors during validation." + }, + "target": { + "allOf": [ + { + "$ref": "#/components/schemas/Target" + } + ], + "description": "The targeted product." + } + }, + "type": "object", + "required": [ + "timestamp", + "status", + "target" + ], + "title": "ValidateResponsePayload", + "description": "Class to represent the response format of the validate response API call." + }, + "ValidateResult": { + "properties": { + "warnings": { + "items": { + "type": "object" + }, + "type": "array", + "title": "Warnings", + "description": "List of warnings" + }, + "errors": { + "items": { + "type": "object" + }, + "type": "array", + "title": "Errors", + "description": "List of errors" + }, + "info": { + "items": { + "type": "object" + }, + "type": "array", + "title": "Info", + "description": "List of info" + } + }, + "type": "object", + "title": "ValidateResult", + "description": "Class to represent the result of the validate API" + }, "ValidationError": { "properties": { "loc": { @@ -926,6 +1360,81 @@ "type" ], "title": "ValidationError" + }, + "config_modules_vmware__framework__models__output_models__configuration_drift_response__Status": { + "type": "string", + "enum": [ + "COMPLIANT", + "NON_COMPLIANT", + "FAILED" + ], + "title": "Status", + "description": "Drift status enum" + }, + "config_modules_vmware__services__apis__models__drift_payload__Result": { + "properties": { + "additions": { + "items": { + "$ref": "#/components/schemas/ConfigAddition" + }, + "type": "array", + "title": "Additions", + "description": "The configurations that needs to be added to the product." + }, + "modifications": { + "items": { + "$ref": "#/components/schemas/ConfigModification" + }, + "type": "array", + "title": "Modifications", + "description": "The configurations that needs to be modified on the product." + }, + "deletions": { + "items": { + "$ref": "#/components/schemas/ConfigDeletion" + }, + "type": "array", + "title": "Deletions", + "description": "The configurations that needs to be deleted from the product" + } + }, + "type": "object", + "title": "Result", + "description": "Class to represent the results of the scan drift API call." + }, + "config_modules_vmware__services__apis__models__schema_payload__Result": { + "properties": { + "json_schema": { + "type": "object", + "title": "Json Schema", + "description": "Product schema in json_schema format" + } + }, + "type": "object", + "required": [ + "json_schema" + ], + "title": "Result", + "description": "Class to represent the results of the schema API call." + }, + "config_modules_vmware__services__apis__models__schema_payload__Status": { + "type": "string", + "enum": [ + "SUCCESS", + "FAILED" + ], + "title": "Status", + "description": "Status enum" + }, + "config_modules_vmware__services__apis__models__validate_payload__Status": { + "type": "string", + "enum": [ + "VALID", + "INVALID", + "FAILED" + ], + "title": "Status", + "description": "Status enum" } } } diff --git a/docs/testing-controllers.md b/docs/testing-controllers.md index b43fa76..6981ea2 100644 --- a/docs/testing-controllers.md +++ b/docs/testing-controllers.md @@ -108,7 +108,25 @@ Check compliance response Remediation response ```json { - "status": "SUCCESS" + "status": "SKIPPED", + "changes": { + "compliance_config": { + "vcenter": { + "ntp": { + "status": "SKIPPED", + "message": [ + "Control already compliant" + ] + }, + "dns": { + "status": "SKIPPED", + "message": [ + "Control already compliant" + ] + } + } + } + } } ``` @@ -162,6 +180,12 @@ Remediate response "216.239.35.0" ] } + }, + "dns": { + "status": "SKIPPED", + "message": [ + "Control already compliant" + ] } } } @@ -274,52 +298,181 @@ Remediate response } } ``` -###### Case 5: Remediation is not implemented for the control + + +###### Case 5: Check compliance and/or remediation is skipped +###### ---- Case 5.1: Control is not applicable on the particular product version Check compliance response ```json { - "status": "NON_COMPLIANT", + "status": "SKIPPED", + "changes": { + "compliance_config": { + "vcenter": { + "tls_version": { + "status": "SKIPPED", + "message": [ + "Control is not applicable on this product version" + ] + } + } + } + } +} +``` +Remediation response +```json +{ + "status": "SKIPPED", + "changes": { + "compliance_config": { + "vcenter": { + "tls_version": { + "status": "SKIPPED", + "message": [ + "Remediation Skipped as Check compliance is SKIPPED" + ] + } + } + } + } +} +``` + +###### ---- Case 5.2: Control is not automated for the particular product version +Check compliance response +```json +{ + "message": "Skipped for hosts - ['esxi-2.vrack.vsphere.local']", + "status": "COMPLIANT", "changes": { - "compliance_config": { - "vcenter": { - "cert_config": { - "status": "NON_COMPLIANT", - "desired": { - "certificate_issuer": [ - "test" - ] - }, - "current": "OU=VMware Engineering, O=vcenter-1.vrack.vsphere.local, ST=California, C=US, DC=local, DC=vsphere, CN=CA" + "esxi-2.vrack.vsphere.local": { + "status": "SKIPPED", + "host_changes": { + "compliance_config": { + "esxi": { + "ssh_port_forwarding": { + "status": "SKIPPED", + "message": [ + "Control is not automated for this product version" + ] + } + } } } } } } ``` +Remediation response +```json +{ + "message": "Skipped for hosts - ['esxi-2.vrack.vsphere.local']", + "status": "SKIPPED", + "changes": { + "esxi-2.vrack.vsphere.local": { + "status": "SKIPPED", + "host_changes": { + "compliance_config": { + "esxi": { + "ssh_port_forwarding": { + "status": "SKIPPED", + "message": [ + "Remediation Skipped as Check compliance is SKIPPED" + ] + } + } + } + } + } + } +} +``` + +###### ---- Case 5.3: Control is already compliant so remediation is skipped +Check compliance response +```json +{ + "status": "COMPLIANT", + "changes": { + "compliance_config": { + "vcenter": { + "ntp": { + "status": "COMPLIANT" + } + } + } + } +} +``` Remediate response ```json { - "status": "SUCCESS", + "status": "SKIPPED", + "changes": { + "compliance_config": { + "vcenter": { + "ntp": { + "status": "SKIPPED", + "message": [ + "Control already compliant" + ] + } + } + } + } +} +``` + +###### ---- Case 5.4: Control is non-compliant but remediation is not supported +Check compliance response +```json +{ + "status": "NON_COMPLIANT", "changes": { "compliance_config": { "vcenter": { "cert_config": { - "status": "SKIPPED", - "message": [ - "Set is not implemented as this control requires manual intervention" - ], + "status": "NON_COMPLIANT", + "current": "OU=VMware Engineering, O=vcenter-1.vrack.vsphere.local, ST=California, C=US, DC=local, DC=vsphere, CN=CA", "desired": { "certificate_issuer": [ - "test" + "OU=VMware Engineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CC", + "OU=VMware Engineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CD" ] - }, - "current": "OU=VMware Engineering, O=vcenter-1.vrack.vsphere.local, ST=California, C=US, DC=local, DC=vsphere, CN=CA" + } } } } } } ``` +Remediation response +```json +{ + "status": "SKIPPED", + "changes": { + "compliance_config": { + "vcenter": { + "cert_config": { + "status": "SKIPPED", + "current": "OU=VMware Engineering, O=vcenter-1.vrack.vsphere.local, ST=California, C=US, DC=local, DC=vsphere, CN=CA", + "desired": { + "certificate_issuer": [ + "OU=VMware Engineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CC", + "OU=VMware Engineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CD" + ] + }, + "message": [ + "Set is not implemented as this control requires manual intervention" + ] + } + } + } + } +} +``` + ###### Case 6: Sample response for ESXi product where vcenter is used to perform check compliance/remediation on list of target hosts (Default target is all hosts) Check compliance response ```json diff --git a/requirements/api-requirements.txt b/requirements/api-requirements.txt index 2b32a36..2d92d6b 100644 --- a/requirements/api-requirements.txt +++ b/requirements/api-requirements.txt @@ -1,13 +1,13 @@ -r prod-requirements.txt -fastapi~=0.110.0; python_version>="3.8" +fastapi~=0.114.0; python_version>="3.8" fastapi~=0.103.2; python_version<="3.7" -uvicorn[standard]~=0.28.0; python_version>="3.8" +uvicorn[standard]~=0.30.6; python_version>="3.8" uvicorn[standard]~=0.22.0; python_version<="3.7" -gunicorn~=21.2.0 +gunicorn~=23.0.0 -pydantic~=2.6.4; python_version>="3.8" +pydantic~=2.9.0; python_version>="3.8" pydantic~=2.5.3; python_version<="3.7" pyyaml~=6.0.1 diff --git a/setup.py b/setup.py index 1bf8dfe..31a2f96 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def _parse_requirements(requirements_file): setup( name=config_modules_vmware.name, # duplicate information due to concourse pipeline requirement - version="0.14.6.0", + version=config_modules_vmware.version, description=config_modules_vmware.description, author=config_modules_vmware.author, install_requires=_parse_requirements("requirements/prod-requirements.txt"),