def __new__(cls, config: ServerConfig, num_processors: int): """Create the correct message consumer class for the message protocol. :param ServerConfig config: content of the CSE config file. :param int num_processors: number of processors for thread pool executor :return: instance of appropriate message protocol consumer """ if server_utils.should_use_mqtt_protocol(config): return MQTTConsumer( url=config.get_value_at('vcd.host'), listen_topic=config.get_value_at( f'mqtt.{MQTTExtKey.EXT_LISTEN_TOPIC}'), # noqa: E501 respond_topic=config.get_value_at( f'mqtt.{MQTTExtKey.EXT_RESPOND_TOPIC}'), # noqa: E501 verify_ssl=config.get_value_at('mqtt.verify_ssl'), token=config.get_value_at(f'mqtt.{MQTTExtTokenKey.TOKEN}'), client_username=f'{server_constants.MQTT_EXTENSION_VENDOR}/' f'{server_constants.CSE_SERVICE_NAME}/' f'{server_constants.MQTT_EXTENSION_VERSION}', num_processors=num_processors) else: return AMQPConsumer( host=config.get_value_at('amqp.host'), port=config.get_value_at('amqp.port'), vhost=config.get_value_at('amqp.vhost'), username=config.get_value_at('amqp.username'), password=config.get_value_at('amqp.password'), exchange=config.get_value_at('amqp.exchange'), routing_key=config.get_value_at('amqp.routing_key'), num_processors=num_processors)
def get_telemetry_instance_id(config_dict, logger_debug=NULL_LOGGER, msg_update_callback=NullPrinter()): """Get CSE AMQP or MQTT extension id which is used as instance id. Any exception is logged as error. No exception is leaked out of this method and does not affect the server startup. :param dict config_dict: CSE configuration :param logging.logger logger_debug: logger instance to log any error in retrieving CSE extension id. :param utils.ConsoleMessagePrinter msg_update_callback: Callback object. :return instance id to use for sending data to Vmware telemetry server :rtype str (unless no instance id found) """ vcd = config_dict['vcd'] try: client = Client(vcd['host'], api_version=vcd['api_version'], verify_ssl_certs=vcd['verify']) client.set_credentials( BasicLoginCredentials(vcd['username'], SYSTEM_ORG_NAME, vcd['password'])) if should_use_mqtt_protocol(config_dict): # Get MQTT extension uuid mqtt_ext_manager = MQTTExtensionManager(client) ext_info = mqtt_ext_manager.get_extension_info( ext_name=CSE_SERVICE_NAME, ext_version=MQTT_EXTENSION_VERSION, ext_vendor=MQTT_EXTENSION_VENDOR) if not ext_info: logger_debug.debug("Failed to retrieve telemetry instance id") return None logger_debug.debug("Retrieved telemetry instance id") return mqtt_ext_manager.get_extension_uuid( ext_info[MQTTExtKey.EXT_URN_ID]) else: # Get AMQP extension id ext = APIExtension(client) cse_info = ext.get_extension_info(CSE_SERVICE_NAME, namespace=CSE_SERVICE_NAMESPACE) logger_debug.debug("Retrieved telemetry instance id") return cse_info.get('id') except Exception as err: msg = f"Cannot retrieve telemetry instance id:{err}" msg_update_callback.general(msg) logger_debug.error(msg) finally: if client is not None: client.logout()
def run(self, msg_update_callback=utils.NullPrinter()): sysadmin_client = None try: sysadmin_client = vcd_utils.get_sys_admin_client(api_version=None) verify_version_compatibility( sysadmin_client, should_cse_run_in_legacy_mode=self.config['service'] ['legacy_mode'], # noqa: E501 is_mqtt_extension=server_utils.should_use_mqtt_protocol( self.config)) # noqa: E501 except Exception as err: logger.SERVER_LOGGER.info(err) raise finally: if sysadmin_client: sysadmin_client.logout() if server_utils.should_use_mqtt_protocol(self.config): # Store/setup MQTT extension, api filter, and token info try: sysadmin_client = \ vcd_utils.get_sys_admin_client(api_version=None) mqtt_ext_manager = MQTTExtensionManager(sysadmin_client) ext_info = mqtt_ext_manager.get_extension_info( ext_name=server_constants.CSE_SERVICE_NAME, ext_version=server_constants.MQTT_EXTENSION_VERSION, ext_vendor=server_constants.MQTT_EXTENSION_VENDOR) ext_urn_id = ext_info[server_constants.MQTTExtKey.EXT_URN_ID] ext_uuid = mqtt_ext_manager.get_extension_uuid(ext_urn_id) api_filters_status = mqtt_ext_manager.check_api_filters_setup( ext_uuid, configure_cse.API_FILTER_PATTERNS) if not api_filters_status: msg = 'MQTT Api filter is not set up' logger.SERVER_LOGGER.error(msg) raise cse_exception.MQTTExtensionError(msg) token_info = mqtt_ext_manager.setup_extension_token( token_name=server_constants.MQTT_TOKEN_NAME, ext_name=server_constants.CSE_SERVICE_NAME, ext_version=server_constants.MQTT_EXTENSION_VERSION, ext_vendor=server_constants.MQTT_EXTENSION_VENDOR, ext_urn_id=ext_urn_id) self.config['mqtt'].update(ext_info) self.config['mqtt'].update(token_info) self.config['mqtt'][server_constants.MQTTExtKey.EXT_UUID] = \ ext_uuid except Exception as err: msg = f'MQTT extension setup error: {err}' logger.SERVER_LOGGER.error(msg) raise err finally: if sysadmin_client: sysadmin_client.logout() populate_vsphere_list(self.config['vcs']) # Load def entity-type and interface self._load_def_schema(msg_update_callback=msg_update_callback) # Read k8s catalog definition from catalog item metadata and append # the same to to server run-time config self._load_template_definition_from_catalog( msg_update_callback=msg_update_callback) self._load_placement_policy_details( msg_update_callback=msg_update_callback) if self.config['service']['legacy_mode']: # Read templates rules from config and update template definition # in server run-time config self._process_template_rules( msg_update_callback=msg_update_callback) # Make sure that all vms in templates are compliant with the # compute policy specified in template definition (can be affected # by rules). self._process_template_compute_policy_compliance( msg_update_callback=msg_update_callback) else: msg = "Template rules are not supported by CSE for vCD api " \ "version 35.0 or above. Skipping template rule processing." msg_update_callback.info(msg) logger.SERVER_LOGGER.debug(msg) if self.should_check_config: configure_cse.check_cse_installation( self.config, msg_update_callback=msg_update_callback) if self.config.get('pks_config'): pks_config = self.config.get('pks_config') self.pks_cache = PksCache( pks_servers=pks_config.get('pks_api_servers', []), pks_accounts=pks_config.get('pks_accounts', []), pvdcs=pks_config.get('pvdcs', []), orgs=pks_config.get('orgs', []), nsxt_servers=pks_config.get('nsxt_servers', [])) num_processors = self.config['service']['processors'] name = server_constants.MESSAGE_CONSUMER_THREAD try: self.consumer = MessageConsumer(self.config, num_processors) consumer_thread = Thread(name=name, target=consumer_thread_run, args=(self.consumer, )) consumer_thread.daemon = True consumer_thread.start() self.consumer_thread = consumer_thread msg = f"Started thread '{name}' ({consumer_thread.ident})" msg_update_callback.general(msg) logger.SERVER_LOGGER.info(msg) except KeyboardInterrupt: if self.consumer: self.consumer.stop() interrupt_msg = f"\nKeyboard interrupt when starting thread " \ f"'{name}'" logger.SERVER_LOGGER.debug(interrupt_msg) raise Exception(interrupt_msg) except Exception: if self.consumer: self.consumer.stop() logger.SERVER_LOGGER.error(traceback.format_exc()) # Updating state to Running before starting watchdog because watchdog # exits when server is not Running self._state = ServerState.RUNNING # Start consumer watchdog name = server_constants.WATCHDOG_THREAD consumer_watchdog = Thread(name=name, target=watchdog_thread_run, args=(self, num_processors)) consumer_watchdog.daemon = True consumer_watchdog.start() self._consumer_watchdog = consumer_watchdog msg = f"Started thread '{name}' ({consumer_watchdog.ident})" msg_update_callback.general(msg) logger.SERVER_LOGGER.info(msg) message = f"Container Service Extension for vCloud Director" \ f"\nServer running using config file: {self.config_file}" \ f"\nLog files: {logger.SERVER_INFO_LOG_FILEPATH}, " \ f"{logger.SERVER_DEBUG_LOG_FILEPATH}" \ f"\nwaiting for requests (ctrl+c to close)" signal.signal(signal.SIGINT, signal_handler) msg_update_callback.general_no_color(message) logger.SERVER_LOGGER.info(message) # Record telemetry on user action and details of operation. cse_params = { PayloadKey.WAS_DECRYPTION_SKIPPED: bool(self.skip_config_decryption), # noqa: E501 PayloadKey.WAS_PKS_CONFIG_FILE_PROVIDED: bool(self.pks_config_file), # noqa: E501 PayloadKey.WAS_INSTALLATION_CHECK_SKIPPED: bool(self.should_check_config) # noqa: E501 } record_user_action_details(cse_operation=CseOperation.SERVICE_RUN, cse_params=cse_params) record_user_action(cse_operation=CseOperation.SERVICE_RUN) while True: try: time.sleep(1) if self._state == ServerState.STOPPING and \ self.active_requests_count() == 0: break except KeyboardInterrupt: break except Exception: msg_update_callback.general_no_color(traceback.format_exc()) logger.SERVER_LOGGER.error(traceback.format_exc()) sys.exit(1) logger.SERVER_LOGGER.info("Stop detected") logger.SERVER_LOGGER.info("Closing connections...") self._state = ServerState.STOPPING try: self.consumer.stop() except Exception: logger.SERVER_LOGGER.error(traceback.format_exc()) self._state = ServerState.STOPPED logger.SERVER_LOGGER.info("Done")
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
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