def get_validated_config(
        config_file_name,
        pks_config_file_name=None,
        skip_config_decryption=False,
        decryption_password=None,
        log_wire_file=None,
        logger_debug=NULL_LOGGER,
        msg_update_callback=NullPrinter()
):
    """Get the config file as a dictionary and check for validity.

    Ensures that all properties exist and all values are the expected type.
    Checks that AMQP connection is available, and vCD/VCs are valid.
    Does not guarantee that CSE has been installed according to this
    config file. Additionally populates certain key-value pairs in the
    config dict to avoid repeated computation of those e.g.
    supported api versions, feature flags, RDE version in use etc.

    :param str config_file_name: path to config file.
    :param str pks_config_file_name: path to PKS config file.
    :param bool skip_config_decryption: do not decrypt the config file.
    :param str decryption_password: password to decrypt the config file.
    :param str log_wire_file: log_wire_file to use if needed to wire log
        pyvcloud requests and responses
    :param logging.Logger logger_debug: logger to log with.
    :param utils.ConsoleMessagePrinter msg_update_callback: Callback object.

    :return: CSE config

    :rtype: dict

    :raises KeyError: if config file has missing or extra properties.
    :raises TypeError: if the value type for a config file property
        is incorrect.
    :raises container_service_extension.exceptions.AmqpConnectionError:
        (when not using MQTT) if AMQP connection failed (host, password, port,
        username, vhost is invalid).
    :raises requests.exceptions.ConnectionError: if 'vcd' 'host' is invalid.
    :raises pyvcloud.vcd.exceptions.VcdException: if 'vcd' 'username' or
        'password' is invalid.
    :raises pyVmomi.vim.fault.InvalidLogin: if 'vcs' 'username' or 'password'
        is invalid.
    """
    check_file_permissions(config_file_name,
                           msg_update_callback=msg_update_callback)
    if skip_config_decryption:
        with open(config_file_name) as config_file:
            config = yaml.safe_load(config_file) or {}
    else:
        msg_update_callback.info(
            f"Decrypting '{config_file_name}'")
        try:
            config = yaml.safe_load(
                get_decrypted_file_contents(
                    config_file_name,
                    decryption_password
                )
            ) or {}
        except cryptography.fernet.InvalidToken:
            raise Exception(CONFIG_DECRYPTION_ERROR_MSG)

    msg_update_callback.info(
        f"Validating config file '{config_file_name}'"
    )

    is_no_vc_communication_mode = \
        server_utils.is_no_vc_communication_mode(ServerConfig(config))

    use_mqtt = server_utils.should_use_mqtt_protocol(ServerConfig(config))
    sample_message_queue_config = SAMPLE_AMQP_CONFIG if not use_mqtt \
        else SAMPLE_MQTT_CONFIG

    # This allows us to compare top-level config keys and value types
    sample_config = {
        **sample_message_queue_config,
        **SAMPLE_VCD_CONFIG,
        **SAMPLE_SERVICE_CONFIG,
        **SAMPLE_BROKER_CONFIG
    }
    if not is_no_vc_communication_mode:
        sample_config.update(SAMPLE_VCS_CONFIG)
    else:
        if 'vcs' in config:
            del config['vcs']

    log_wire = str_to_bool(config.get('service', {}).get('log_wire'))
    nsxt_wire_logger = NULL_LOGGER
    if not log_wire:
        log_wire_file = None
        nsxt_wire_logger = SERVER_NSXT_WIRE_LOGGER

    check_keys_and_value_types(
        config,
        sample_config,
        location='config file',
        msg_update_callback=msg_update_callback
    )
    # MQTT validation not required because no MQTT host, exchange, etc.
    # is needed in the config file since the server code creates and
    # registers the MQTT extension directly using server constants
    if not use_mqtt:
        _validate_amqp_config(config['amqp'], msg_update_callback)

    # Validation of service properties is done first as those properties are
    # used in broker validation.
    check_keys_and_value_types(
        config['service'],
        SAMPLE_SERVICE_CONFIG['service'],
        location="config file 'service' section",
        excluded_keys=['log_wire'],
        msg_update_callback=msg_update_callback
    )

    try:
        if is_no_vc_communication_mode:
            _validate_vcd_config(
                config['vcd'],
                msg_update_callback,
                log_file=log_wire_file,
                log_wire=log_wire
            )
        else:
            _validate_vcd_and_vcs_config(
                config['vcd'],
                config['vcs'],
                msg_update_callback,
                log_file=log_wire_file,
                log_wire=log_wire
            )
    except vim.fault.InvalidLogin:
        raise Exception(VCENTER_LOGIN_ERROR_MSG)
    except requests.exceptions.SSLError as err:
        raise Exception(f"SSL verification failed: {str(err)}")
    except requests.exceptions.ConnectionError as err:
        raise Exception(f"Cannot connect to {err.request.url}.")

    check_keys_and_value_types(
        config['service']['telemetry'],
        SAMPLE_SERVICE_CONFIG['service']['telemetry'],
        location="config file 'service->telemetry' section",
        msg_update_callback=msg_update_callback
    )

    _validate_broker_config(
        config['broker'],
        legacy_mode=config['service']['legacy_mode'],
        msg_update_callback=msg_update_callback,
        logger_debug=logger_debug
    )

    config = add_additional_details_to_config(
        config=config,
        vcd_host=config['vcd']['host'],
        vcd_username=config['vcd']['username'],
        vcd_password=config['vcd']['password'],
        verify_ssl=config['vcd']['verify'],
        is_legacy_mode=config['service']['legacy_mode'],
        is_mqtt_exchange=server_utils.should_use_mqtt_protocol(
            ServerConfig(config)
        ),
        log_wire=log_wire,
        log_wire_file=log_wire_file
    )
    _raise_error_if_amqp_not_supported(
        use_mqtt,
        config['service']['default_api_version'],
        logger=logger_debug
    )

    msg_update_callback.general(
        f"Config file '{config_file_name}' is valid"
    )

    if pks_config_file_name:
        check_file_permissions(pks_config_file_name,
                               msg_update_callback=msg_update_callback)
        if skip_config_decryption:
            with open(pks_config_file_name) as f:
                pks_config = yaml.safe_load(f) or {}
        else:
            msg_update_callback.info(
                f"Decrypting '{pks_config_file_name}'")
            pks_config = yaml.safe_load(
                get_decrypted_file_contents(pks_config_file_name,
                                            decryption_password)) or {}
        msg_update_callback.info(
            f"Validating PKS config file '{pks_config_file_name}'")
        _validate_pks_config_structure(pks_config, msg_update_callback)
        try:
            _validate_pks_config_data_integrity(pks_config,
                                                msg_update_callback,
                                                logger_debug=logger_debug,
                                                logger_wire=nsxt_wire_logger)
        except requests.exceptions.SSLError as err:
            raise Exception(f"SSL verification failed: {str(err)}")

        msg_update_callback.general(
            f"PKS Config file '{pks_config_file_name}' is valid")
        config['pks_config'] = pks_config
    else:
        config['pks_config'] = None

    return config
Example #2
0
def get_validated_config(config_file_name,
                         pks_config_file_name=None,
                         skip_config_decryption=False,
                         decryption_password=None,
                         log_wire_file=None,
                         logger_debug=NULL_LOGGER,
                         msg_update_callback=NullPrinter()):
    """Get the config file as a dictionary and check for validity.

    Ensures that all properties exist and all values are the expected type.
    Checks that AMQP connection is available, and vCD/VCs are valid.
    Does not guarantee that CSE has been installed according to this
    config file.

    :param str config_file_name: path to config file.
    :param str pks_config_file_name: path to PKS config file.
    :param bool skip_config_decryption: do not decrypt the config file.
    :param str decryption_password: password to decrypt the config file.
    :param str log_wire_file: log_wire_file to use if needed to wire log
        pyvcloud requests and responses
    :param logging.Logger logger_debug: logger to log with.
    :param utils.ConsoleMessagePrinter msg_update_callback: Callback object.

    :return: CSE config

    :rtype: dict

    :raises KeyError: if config file has missing or extra properties.
    :raises TypeError: if the value type for a config file property
        is incorrect.
    :raises container_service_extension.exceptions.AmqpConnectionError:
        (when not using MQTT) if AMQP connection failed (host, password, port,
        username, vhost is invalid).
    :raises requests.exceptions.ConnectionError: if 'vcd' 'host' is invalid.
    :raises pyvcloud.vcd.exceptions.VcdException: if 'vcd' 'username' or
        'password' is invalid.
    :raises pyVmomi.vim.fault.InvalidLogin: if 'vcs' 'username' or 'password'
        is invalid.
    """
    check_file_permissions(config_file_name,
                           msg_update_callback=msg_update_callback)
    if skip_config_decryption:
        with open(config_file_name) as config_file:
            config = yaml.safe_load(config_file) or {}
    else:
        msg_update_callback.info(
            f"Decrypting '{config_file_name}'")
        try:
            config = yaml.safe_load(
                get_decrypted_file_contents(config_file_name,
                                            decryption_password)) or {}
        except cryptography.fernet.InvalidToken:
            raise Exception(CONFIG_DECRYPTION_ERROR_MSG)

    msg_update_callback.info(
        f"Validating config file '{config_file_name}'")
    # This allows us to compare top-level config keys and value types
    use_mqtt = should_use_mqtt_protocol(config)
    sample_message_queue_config = SAMPLE_AMQP_CONFIG if not use_mqtt \
        else SAMPLE_MQTT_CONFIG
    sample_config = {
        **sample_message_queue_config, **SAMPLE_VCD_CONFIG,
        **SAMPLE_VCS_CONFIG, **SAMPLE_SERVICE_CONFIG,
        **SAMPLE_BROKER_CONFIG
    }
    log_wire = str_to_bool(config.get('service', {}).get('log_wire'))
    nsxt_wire_logger = NULL_LOGGER
    if not log_wire:
        log_wire_file = None
        nsxt_wire_logger = SERVER_NSXT_WIRE_LOGGER
    check_keys_and_value_types(config, sample_config, location='config file',
                               msg_update_callback=msg_update_callback)
    # MQTT validation not required because no MQTT host, exchange, etc.
    # is needed in the config file since the server code creates and
    # registers the MQTT extension directly using server constants
    if not use_mqtt:
        _validate_amqp_config(config['amqp'], msg_update_callback)
    try:
        _validate_vcd_and_vcs_config(config['vcd'], config['vcs'],
                                     msg_update_callback,
                                     log_file=log_wire_file,
                                     log_wire=log_wire)
    except vim.fault.InvalidLogin:
        raise Exception(VCENTER_LOGIN_ERROR_MSG)
    except requests.exceptions.SSLError as err:
        raise Exception(f"SSL verification failed: {str(err)}")
    except requests.exceptions.ConnectionError as err:
        raise Exception(f"Cannot connect to {err.request.url}.")

    _validate_broker_config(config['broker'],
                            legacy_mode=config['service']['legacy_mode'],
                            msg_update_callback=msg_update_callback,
                            logger_debug=logger_debug)
    check_keys_and_value_types(config['service'],
                               SAMPLE_SERVICE_CONFIG['service'],
                               location="config file 'service' section",
                               excluded_keys=['log_wire'],
                               msg_update_callback=msg_update_callback)
    check_keys_and_value_types(config['service']['telemetry'],
                               SAMPLE_SERVICE_CONFIG['service']['telemetry'],
                               location="config file 'service->telemetry' "
                                        "section",
                               msg_update_callback=msg_update_callback)
    msg_update_callback.general(
        f"Config file '{config_file_name}' is valid")

    if pks_config_file_name:
        check_file_permissions(pks_config_file_name,
                               msg_update_callback=msg_update_callback)
        if skip_config_decryption:
            with open(pks_config_file_name) as f:
                pks_config = yaml.safe_load(f) or {}
        else:
            msg_update_callback.info(
                f"Decrypting '{pks_config_file_name}'")
            pks_config = yaml.safe_load(
                get_decrypted_file_contents(pks_config_file_name,
                                            decryption_password)) or {}
        msg_update_callback.info(
            f"Validating PKS config file '{pks_config_file_name}'")
        _validate_pks_config_structure(pks_config, msg_update_callback)
        try:
            _validate_pks_config_data_integrity(pks_config,
                                                msg_update_callback,
                                                logger_debug=logger_debug,
                                                logger_wire=nsxt_wire_logger)
        except requests.exceptions.SSLError as err:
            raise Exception(f"SSL verification failed: {str(err)}")

        msg_update_callback.general(
            f"PKS Config file '{pks_config_file_name}' is valid")
        config['pks_config'] = pks_config
    else:
        config['pks_config'] = None

    # Compute common supported api versions by the CSE server and vCD
    sysadmin_client = None
    try:
        sysadmin_client = Client(
            config['vcd']['host'],
            verify_ssl_certs=config['vcd']['verify'],
            log_file=log_wire_file,
            log_requests=log_wire,
            log_headers=log_wire,
            log_bodies=log_wire)
        sysadmin_client.set_credentials(BasicLoginCredentials(
            config['vcd']['username'],
            SYSTEM_ORG_NAME,
            config['vcd']['password']))

        vcd_supported_api_versions = \
            set(sysadmin_client.get_supported_versions_list())
        cse_supported_api_versions = set(SUPPORTED_VCD_API_VERSIONS)
        common_supported_api_versions = \
            list(cse_supported_api_versions.intersection(vcd_supported_api_versions))  # noqa: E501
        common_supported_api_versions.sort()
        config['service']['supported_api_versions'] = \
            common_supported_api_versions
    finally:
        if sysadmin_client:
            sysadmin_client.logout()

    # Convert legacy_mode flag in service_section to corresponding
    # feature flags
    is_legacy_mode = config['service']['legacy_mode']
    if 'feature_flags' not in config:
        config['feature_flags'] = {}
    config['feature_flags']['legacy_api'] = str_to_bool(is_legacy_mode)
    config['feature_flags']['non_legacy_api'] = \
        not str_to_bool(is_legacy_mode)

    # Temporary work around before api version is completely removed from
    # config
    if is_legacy_mode:
        supported_api_versions_float = \
            [float(x) for x in config['service']['supported_api_versions']
                if float(x) < 35.0]
    else:
        supported_api_versions_float = \
            [float(x) for x in config['service']['supported_api_versions']
                if float(x) >= 35.0]
    config['vcd']['api_version'] = str(max(supported_api_versions_float))

    # Store telemetry instance id, url and collector id in config
    # This steps needs to be done after api_version has been computed
    # and stored in the config
    store_telemetry_settings(config)

    return config