def __init__(self, spec_file_name, config_handler, command_handler, cc_session=None, handle_logging_config=True, socket_file=None): """Initialize a ModuleCCSession. This does *NOT* send the specification and request the configuration yet. Use start() for that once the ModuleCCSession has been initialized. specfile_name is the path to the specification file. config_handler and command_handler are callback functions, see set_config_handler and set_command_handler for more information on their signatures. cc_session can be used to pass in an existing CCSession, if it is None, one will be set up. This is mainly intended for testing purposes. handle_logging_config: if True, the module session will automatically handle logging configuration for the module; it will read the system-wide Logging configuration and call the logger manager to apply it. It will also inform the logger manager when the logging configuration gets updated. The module does not need to do anything except initializing its loggers, and provide log messages. Defaults to true. socket_file: If cc_session was none, this optional argument specifies which socket file to use to connect to msgq. It will be overridden by the environment variable MSGQ_SOCKET_FILE. If none, and no environment variable is set, it will use the system default. """ module_spec = isc.config.module_spec_from_file(spec_file_name) ConfigData.__init__(self, module_spec) self._module_name = module_spec.get_module_name() self.set_config_handler(config_handler) self.set_command_handler(command_handler) if not cc_session: self._session = Session(socket_file) else: self._session = cc_session self._session.group_subscribe(self._module_name, CC_INSTANCE_WILDCARD) self._remote_module_configs = {} self._remote_module_callbacks = {} self._notification_callbacks = {} self._last_notif_id = 0 if handle_logging_config: self.add_remote_config( path_search('logging.spec', bind10_config.PLUGIN_PATHS), default_logconfig_handler)
def __init__(self, spec_file_name, config_handler, command_handler, cc_session=None, handle_logging_config=True, socket_file = None): """Initialize a ModuleCCSession. This does *NOT* send the specification and request the configuration yet. Use start() for that once the ModuleCCSession has been initialized. specfile_name is the path to the specification file. config_handler and command_handler are callback functions, see set_config_handler and set_command_handler for more information on their signatures. cc_session can be used to pass in an existing CCSession, if it is None, one will be set up. This is mainly intended for testing purposes. handle_logging_config: if True, the module session will automatically handle logging configuration for the module; it will read the system-wide Logging configuration and call the logger manager to apply it. It will also inform the logger manager when the logging configuration gets updated. The module does not need to do anything except initializing its loggers, and provide log messages. Defaults to true. socket_file: If cc_session was none, this optional argument specifies which socket file to use to connect to msgq. It will be overridden by the environment variable MSGQ_SOCKET_FILE. If none, and no environment variable is set, it will use the system default. """ module_spec = isc.config.module_spec_from_file(spec_file_name) ConfigData.__init__(self, module_spec) self._module_name = module_spec.get_module_name() self.set_config_handler(config_handler) self.set_command_handler(command_handler) if not cc_session: self._session = Session(socket_file) else: self._session = cc_session self._session.group_subscribe(self._module_name, CC_INSTANCE_WILDCARD) self._remote_module_configs = {} self._remote_module_callbacks = {} self._notification_callbacks = {} self._last_notif_id = 0 if handle_logging_config: self.add_remote_config(path_search('logging.spec', bind10_config.PLUGIN_PATHS), default_logconfig_handler)
class ModuleCCSession(ConfigData): """This class maintains a connection to the command channel, as well as configuration options for modules. The module provides a specification file that contains the module name, configuration options, and commands. It also gives the ModuleCCSession two callback functions, one to call when there is a direct command to the module, and one to update the configuration run-time. These callbacks are called when 'check_command' is called on the ModuleCCSession""" def __init__(self, spec_file_name, config_handler, command_handler, cc_session=None, handle_logging_config=True, socket_file=None): """Initialize a ModuleCCSession. This does *NOT* send the specification and request the configuration yet. Use start() for that once the ModuleCCSession has been initialized. specfile_name is the path to the specification file. config_handler and command_handler are callback functions, see set_config_handler and set_command_handler for more information on their signatures. cc_session can be used to pass in an existing CCSession, if it is None, one will be set up. This is mainly intended for testing purposes. handle_logging_config: if True, the module session will automatically handle logging configuration for the module; it will read the system-wide Logging configuration and call the logger manager to apply it. It will also inform the logger manager when the logging configuration gets updated. The module does not need to do anything except initializing its loggers, and provide log messages. Defaults to true. socket_file: If cc_session was none, this optional argument specifies which socket file to use to connect to msgq. It will be overridden by the environment variable MSGQ_SOCKET_FILE. If none, and no environment variable is set, it will use the system default. """ module_spec = isc.config.module_spec_from_file(spec_file_name) ConfigData.__init__(self, module_spec) self._module_name = module_spec.get_module_name() self.set_config_handler(config_handler) self.set_command_handler(command_handler) if not cc_session: self._session = Session(socket_file) else: self._session = cc_session self._session.group_subscribe(self._module_name, CC_INSTANCE_WILDCARD) self._remote_module_configs = {} self._remote_module_callbacks = {} self._notification_callbacks = {} self._last_notif_id = 0 if handle_logging_config: self.add_remote_config( path_search('logging.spec', bind10_config.PLUGIN_PATHS), default_logconfig_handler) def __del__(self): # If the CC Session obejct has been closed, it returns # immediately. if self._session._closed: return self._session.group_unsubscribe(self._module_name, CC_INSTANCE_WILDCARD) for module_name in self._remote_module_configs: self._session.group_unsubscribe(module_name) def start(self): """Send the specification for this module to the configuration manager, and request the current non-default configuration. The config_handler will be called with that configuration""" self.__send_spec() self.__request_config() def send_stopping(self): """Sends a 'stopping' message to the configuration manager. This message is just an FYI, and no response is expected. Any errors when sending this message (for instance if the msgq session has previously been closed) are logged, but ignored.""" # create_command could raise an exception as well, but except for # out of memory related errors, these should all be programming # failures and are not caught msg = create_command(COMMAND_MODULE_STOPPING, self.get_module_spec().get_full_spec()) try: self._session.group_sendmsg(msg, "ConfigManager") except Exception as se: # If the session was previously closed, obvously trying to send # a message fails. (TODO: check if session is open so we can # error on real problems?) logger.error(CONFIG_SESSION_STOPPING_FAILED, se) def get_socket(self): """Returns the socket from the command channel session. This should *only* be used for select() loops to see if there is anything on the channel. If that loop is not completely time-critical, it is strongly recommended to only use check_command(), and not look at the socket at all.""" return self._session._socket def close(self): """Close the session to the command channel""" self._session.close() def check_command(self, nonblock=True): """Check whether there is a command or configuration update on the channel. This function does a read on the cc session, and returns nothing. It calls check_command_without_recvmsg() to parse the received message. If nonblock is True, it just checks if there's a command and does nothing if there isn't. If nonblock is False, it waits until it arrives. It temporarily sets timeout to infinity, because commands may not come in arbitrary long time.""" timeout_orig = self._session.get_timeout() self._session.set_timeout(0) try: msg, env = self._session.group_recvmsg(nonblock) finally: self._session.set_timeout(timeout_orig) self.check_command_without_recvmsg(msg, env) def check_command_without_recvmsg(self, msg, env): """Parse the given message to see if there is a command or a configuration update. Calls the corresponding handler functions if present. Responds on the channel if the handler returns a message.""" if msg is None: return if CC_PAYLOAD_NOTIFICATION in msg: group_s = env[CC_HEADER_GROUP].split('/', 1) # What to do with these bogus inputs? We just ignore them for now. if len(group_s) != 2: return [prefix, group] = group_s if prefix + '/' != CC_GROUP_NOTIFICATION_PREFIX: return # Now, get the callbacks and call one by one callbacks = self._notification_callbacks.get(group, {}) event = msg[CC_PAYLOAD_NOTIFICATION][0] params = None if len(msg[CC_PAYLOAD_NOTIFICATION]) > 1: params = msg[CC_PAYLOAD_NOTIFICATION][1] for key in sorted(callbacks.keys()): callbacks[key](event, params) elif not CC_PAYLOAD_RESULT in msg: # should we default to an answer? success-by-default? unhandled # error? answer = None try: module_name = env[CC_HEADER_GROUP] cmd, arg = isc.config.ccsession.parse_command(msg) if cmd == COMMAND_CONFIG_UPDATE: new_config = arg # If the target channel was not this module # it might be in the remote_module_configs if module_name != self._module_name: if module_name in self._remote_module_configs: # no checking for validity, that's up to the # module itself. newc = self._remote_module_configs[ module_name].get_local_config() isc.cc.data.merge(newc, new_config) self._remote_module_configs[ module_name].set_local_config(newc) if self._remote_module_callbacks[ module_name] != None: self._remote_module_callbacks[module_name]( new_config, self._remote_module_configs[module_name]) # For other modules, we're not supposed to answer return # ok, so apparently this update is for us. errors = [] if not self._config_handler: answer = create_answer( 2, self._module_name + " has no config handler") elif not self.get_module_spec().validate_config( False, new_config, errors): answer = create_answer(1, ", ".join(errors)) else: isc.cc.data.remove_identical(new_config, self.get_local_config()) answer = self._config_handler(new_config) rcode, val = parse_answer(answer) if rcode == CC_REPLY_SUCCESS: newc = self.get_local_config() isc.cc.data.merge(newc, new_config) self.set_local_config(newc) else: # ignore commands for 'remote' modules if module_name == self._module_name: if self._command_handler: answer = self._command_handler(cmd, arg) else: answer = create_answer( 2, self._module_name + " has no command handler") except Exception as exc: answer = create_answer(1, str(exc)) if answer: self._session.group_reply(env, answer) def set_config_handler(self, config_handler): """Set the config handler for this module. The handler is a function that takes the full configuration and handles it. It should return an answer created with create_answer()""" self._config_handler = config_handler # should we run this right now since we've changed the handler? def set_command_handler(self, command_handler): """Set the command handler for this module. The handler is a function that takes a command as defined in the .spec file and return an answer created with create_answer()""" self._command_handler = command_handler def _add_remote_config_internal(self, module_spec, config_update_callback=None): """The guts of add_remote_config and add_remote_config_by_name""" module_cfg = ConfigData(module_spec) module_name = module_spec.get_module_name() self._session.group_subscribe(module_name) # Get the current config for that module now seq = self._session.group_sendmsg( create_command(COMMAND_GET_CONFIG, {"module_name": module_name}), "ConfigManager") try: answer, _ = self._session.group_recvmsg(False, seq) except isc.cc.SessionTimeout: raise ModuleCCSessionError("No answer from ConfigManager when " "asking about Remote module " + module_name) call_callback = False if answer: rcode, value = parse_answer(answer) if rcode == 0: if value != None: if module_spec.validate_config(False, value): module_cfg.set_local_config(value) call_callback = True else: raise ModuleCCSessionError("Bad config data for " + module_name + ": " + str(value)) else: raise ModuleCCSessionError("Failure requesting remote " + "configuration data for " + module_name) # all done, add it self._remote_module_configs[module_name] = module_cfg self._remote_module_callbacks[module_name] = config_update_callback if call_callback and config_update_callback is not None: config_update_callback(value, module_cfg) def add_remote_config_by_name(self, module_name, config_update_callback=None): """ This does the same as add_remote_config, but you provide the module name instead of the name of the spec file. """ seq = self._session.group_sendmsg( create_command(COMMAND_GET_MODULE_SPEC, {"module_name": module_name}), "ConfigManager") try: answer, env = self._session.group_recvmsg(False, seq) except isc.cc.SessionTimeout: raise ModuleCCSessionError("No answer from ConfigManager when " + "asking about for spec of Remote " + "module " + module_name) if answer: rcode, value = parse_answer(answer) if rcode == 0: module_spec = isc.config.module_spec.ModuleSpec(value) if module_spec.get_module_name() != module_name: raise ModuleCCSessionError("Module name mismatch: " + module_name + " and " + module_spec.get_module_name()) self._add_remote_config_internal(module_spec, config_update_callback) else: raise ModuleCCSessionError("Error code " + str(rcode) + "when asking for module spec of " + module_name) else: raise ModuleCCSessionError("No answer when asking for module " + "spec of " + module_name) # Just to be consistent with the add_remote_config return module_name def add_remote_config(self, spec_file_name, config_update_callback=None): """Gives access to the configuration of a different module. These remote module options can at this moment only be accessed through get_remote_config_value(). This function also subscribes to the channel of the remote module name to receive the relevant updates. It is not possible to specify your own handler for this right now, but you can specify a callback that is called after the change happened. start() must have been called on this CCSession prior to the call to this method. Returns the name of the module.""" module_spec = isc.config.module_spec_from_file(spec_file_name) self._add_remote_config_internal(module_spec, config_update_callback) return module_spec.get_module_name() def remove_remote_config(self, module_name): """Removes the remote configuration access for this module""" if module_name in self._remote_module_configs: self._session.group_unsubscribe(module_name) del self._remote_module_configs[module_name] del self._remote_module_callbacks[module_name] def get_remote_config_value(self, module_name, identifier): """Returns the current setting for the given identifier at the given module. If the module has not been added with add_remote_config, a ModuleCCSessionError is raised""" if module_name in self._remote_module_configs: return self._remote_module_configs[module_name].get_value( identifier) else: raise ModuleCCSessionError("Remote module " + module_name + " not found") def __send_spec(self): """Sends the data specification to the configuration manager""" msg = create_command(COMMAND_MODULE_SPEC, self.get_module_spec().get_full_spec()) seq = self._session.group_sendmsg(msg, "ConfigManager") try: answer, env = self._session.group_recvmsg(False, seq) except isc.cc.SessionTimeout: # TODO: log an error? pass def __request_config(self): """Asks the configuration manager for the current configuration, and call the config handler if set. Raises a ModuleCCSessionError if there is no answer from the configuration manager""" seq = self._session.group_sendmsg( create_command(COMMAND_GET_CONFIG, {"module_name": self._module_name}), "ConfigManager") try: answer, env = self._session.group_recvmsg(False, seq) if answer: rcode, value = parse_answer(answer) if rcode == 0: errors = [] if value != None: if self.get_module_spec().validate_config( False, value, errors): self.set_local_config(value) if self._config_handler: self._config_handler(value) else: raise ModuleCCSessionError( "Wrong data in configuration: " + " ".join(errors)) else: logger.error(CONFIG_GET_FAILED, value) else: raise ModuleCCSessionError( "No answer from configuration manager") except isc.cc.SessionTimeout: raise ModuleCCSessionError( "CC Session timeout waiting for configuration manager") def rpc_call(self, command, group, instance=CC_INSTANCE_WILDCARD, to=CC_TO_WILDCARD, params=None): """ Create a command with the given name and parameters. Send it to a recipient, wait for the answer and parse it. This is a wrapper around the group_sendmsg and group_recvmsg on the CC session. It exists mostly for convenience. Params: - command: Name of the command to call on the remote side. - group, instance, to: Address specification of the recipient. - params: Parameters to pass to the command (as keyword arguments). Return: The return value of the remote call (just the value, no status code or anything). May be None. Raise: - RPCRecipientMissing if the given recipient doesn't exist. - RPCError if the other side sent an error response. The error string is in the exception. - ModuleCCSessionError in case of protocol errors, like malformed answer. """ cmd = create_command(command, params) seq = self._session.group_sendmsg(cmd, group, instance=instance, to=to, want_answer=True) # For non-blocking, we'll have rpc_call_async (once the nonblock # actually works) reply, rheaders = self._session.group_recvmsg(nonblock=False, seq=seq) code, value = parse_answer(reply) if code == CC_REPLY_NO_RECPT: raise RPCRecipientMissing(value) elif code != CC_REPLY_SUCCESS: raise RPCError(code, value) return value def notify(self, notification_group, event_name, params=None): """ Send a notification to subscribed users. Send a notification message to all users subscribed to the given notification group. This method does not block. See docs/design/ipc-high.txt for details about notifications and the format of messages sent. Throws: - CCSessionError: for low-level communication errors. Params: - notification_group (string): This parameter (indirectly) signifies what users should receive the notification. Only users that subscribed to notifications on the same group receive it. - event_name (string): The name of the event to notify about (for example `new_group_member`). - params: Other parameters that describe the event. This might be, for example, the ID of the new member and the name of the group. This can be any data that can be sent over the isc.cc.Session, but it is common for it to be dict. Returns: Nothing """ notification = [event_name] if params is not None: notification.append(params) self._session.group_sendmsg({CC_PAYLOAD_NOTIFICATION: notification}, CC_GROUP_NOTIFICATION_PREFIX + notification_group, instance=CC_INSTANCE_WILDCARD, to=CC_TO_WILDCARD, want_answer=False) def subscribe_notification(self, notification_group, callback): """ Subscribe to receive notifications in given notification group. When a notification comes to the group, the callback is called with two parameters, the name of the event (the value of `event_name` parameter passed to `notify`) and the parameters of the event (the value of `params` passed to `notify`). This is a fast operation (there may be communication with the message queue daemon, but it does not wait for any remote process). The callback may get called multiple times (once for each notification). It is possible to subscribe multiple callbacks for the same notification, by multiple calls of this method, and they will be called in the order of registration when the notification comes. Throws: - CCSessionError: for low-level communication errors. Params: - notification_group (string): Notification group to subscribe to. Notification with the same value of the same parameter of `notify` will be received. - callback (callable): The callback to be called whenever the notification comes. The callback should not raise exceptions, such exceptions are likely to propagate through the loop and terminate program. Returns: Opaque id of the subscription. It can be used to cancel the subscription by unsubscribe_notification. """ self._last_notif_id += 1 my_id = self._last_notif_id if notification_group in self._notification_callbacks: self._notification_callbacks[notification_group][my_id] = callback else: self._session.group_subscribe(CC_GROUP_NOTIFICATION_PREFIX + notification_group) self._notification_callbacks[notification_group] = \ { my_id: callback } return (notification_group, my_id) def unsubscribe_notification(self, nid): """ Remove previous subscription for notifications. Pass the id returned from subscribe_notification. Throws: - CCSessionError: for low-level communication errors. - KeyError: The id does not correspond to valid subscription. """ (group, cid) = nid del self._notification_callbacks[group][cid] if not self._notification_callbacks[group]: # Removed the last one self._session.group_unsubscribe(CC_GROUP_NOTIFICATION_PREFIX + group) del self._notification_callbacks[group]
class ModuleCCSession(ConfigData): """This class maintains a connection to the command channel, as well as configuration options for modules. The module provides a specification file that contains the module name, configuration options, and commands. It also gives the ModuleCCSession two callback functions, one to call when there is a direct command to the module, and one to update the configuration run-time. These callbacks are called when 'check_command' is called on the ModuleCCSession""" def __init__(self, spec_file_name, config_handler, command_handler, cc_session=None, handle_logging_config=True, socket_file = None): """Initialize a ModuleCCSession. This does *NOT* send the specification and request the configuration yet. Use start() for that once the ModuleCCSession has been initialized. specfile_name is the path to the specification file. config_handler and command_handler are callback functions, see set_config_handler and set_command_handler for more information on their signatures. cc_session can be used to pass in an existing CCSession, if it is None, one will be set up. This is mainly intended for testing purposes. handle_logging_config: if True, the module session will automatically handle logging configuration for the module; it will read the system-wide Logging configuration and call the logger manager to apply it. It will also inform the logger manager when the logging configuration gets updated. The module does not need to do anything except initializing its loggers, and provide log messages. Defaults to true. socket_file: If cc_session was none, this optional argument specifies which socket file to use to connect to msgq. It will be overridden by the environment variable MSGQ_SOCKET_FILE. If none, and no environment variable is set, it will use the system default. """ module_spec = isc.config.module_spec_from_file(spec_file_name) ConfigData.__init__(self, module_spec) self._module_name = module_spec.get_module_name() self.set_config_handler(config_handler) self.set_command_handler(command_handler) if not cc_session: self._session = Session(socket_file) else: self._session = cc_session self._session.group_subscribe(self._module_name, CC_INSTANCE_WILDCARD) self._remote_module_configs = {} self._remote_module_callbacks = {} self._notification_callbacks = {} self._last_notif_id = 0 if handle_logging_config: self.add_remote_config(path_search('logging.spec', bind10_config.PLUGIN_PATHS), default_logconfig_handler) def __del__(self): # If the CC Session obejct has been closed, it returns # immediately. if self._session._closed: return self._session.group_unsubscribe(self._module_name, CC_INSTANCE_WILDCARD) for module_name in self._remote_module_configs: self._session.group_unsubscribe(module_name) def start(self): """Send the specification for this module to the configuration manager, and request the current non-default configuration. The config_handler will be called with that configuration""" self.__send_spec() self.__request_config() def send_stopping(self): """Sends a 'stopping' message to the configuration manager. This message is just an FYI, and no response is expected. Any errors when sending this message (for instance if the msgq session has previously been closed) are logged, but ignored.""" # create_command could raise an exception as well, but except for # out of memory related errors, these should all be programming # failures and are not caught msg = create_command(COMMAND_MODULE_STOPPING, self.get_module_spec().get_full_spec()) try: self._session.group_sendmsg(msg, "ConfigManager") except Exception as se: # If the session was previously closed, obvously trying to send # a message fails. (TODO: check if session is open so we can # error on real problems?) logger.error(CONFIG_SESSION_STOPPING_FAILED, se) def get_socket(self): """Returns the socket from the command channel session. This should *only* be used for select() loops to see if there is anything on the channel. If that loop is not completely time-critical, it is strongly recommended to only use check_command(), and not look at the socket at all.""" return self._session._socket def close(self): """Close the session to the command channel""" self._session.close() def check_command(self, nonblock=True): """Check whether there is a command or configuration update on the channel. This function does a read on the cc session, and returns nothing. It calls check_command_without_recvmsg() to parse the received message. If nonblock is True, it just checks if there's a command and does nothing if there isn't. If nonblock is False, it waits until it arrives. It temporarily sets timeout to infinity, because commands may not come in arbitrary long time.""" timeout_orig = self._session.get_timeout() self._session.set_timeout(0) try: msg, env = self._session.group_recvmsg(nonblock) finally: self._session.set_timeout(timeout_orig) self.check_command_without_recvmsg(msg, env) def check_command_without_recvmsg(self, msg, env): """Parse the given message to see if there is a command or a configuration update. Calls the corresponding handler functions if present. Responds on the channel if the handler returns a message.""" if msg is None: return if CC_PAYLOAD_NOTIFICATION in msg: group_s = env[CC_HEADER_GROUP].split('/', 1) # What to do with these bogus inputs? We just ignore them for now. if len(group_s) != 2: return [prefix, group] = group_s if prefix + '/' != CC_GROUP_NOTIFICATION_PREFIX: return # Now, get the callbacks and call one by one callbacks = self._notification_callbacks.get(group, {}) event = msg[CC_PAYLOAD_NOTIFICATION][0] params = None if len(msg[CC_PAYLOAD_NOTIFICATION]) > 1: params = msg[CC_PAYLOAD_NOTIFICATION][1] for key in sorted(callbacks.keys()): callbacks[key](event, params) elif not CC_PAYLOAD_RESULT in msg: # should we default to an answer? success-by-default? unhandled # error? answer = None try: module_name = env[CC_HEADER_GROUP] cmd, arg = isc.config.ccsession.parse_command(msg) if cmd == COMMAND_CONFIG_UPDATE: new_config = arg # If the target channel was not this module # it might be in the remote_module_configs if module_name != self._module_name: if module_name in self._remote_module_configs: # no checking for validity, that's up to the # module itself. newc = self._remote_module_configs[module_name].get_local_config() isc.cc.data.merge(newc, new_config) self._remote_module_configs[module_name].set_local_config(newc) if self._remote_module_callbacks[module_name] != None: self._remote_module_callbacks[module_name](new_config, self._remote_module_configs[module_name]) # For other modules, we're not supposed to answer return # ok, so apparently this update is for us. errors = [] if not self._config_handler: answer = create_answer(2, self._module_name + " has no config handler") elif not self.get_module_spec().validate_config(False, new_config, errors): answer = create_answer(1, ", ".join(errors)) else: isc.cc.data.remove_identical(new_config, self.get_local_config()) answer = self._config_handler(new_config) rcode, val = parse_answer(answer) if rcode == CC_REPLY_SUCCESS: newc = self.get_local_config() isc.cc.data.merge(newc, new_config) self.set_local_config(newc) else: # ignore commands for 'remote' modules if module_name == self._module_name: if self._command_handler: answer = self._command_handler(cmd, arg) else: answer = create_answer(2, self._module_name + " has no command handler") except Exception as exc: answer = create_answer(1, str(exc)) if answer: self._session.group_reply(env, answer) def set_config_handler(self, config_handler): """Set the config handler for this module. The handler is a function that takes the full configuration and handles it. It should return an answer created with create_answer()""" self._config_handler = config_handler # should we run this right now since we've changed the handler? def set_command_handler(self, command_handler): """Set the command handler for this module. The handler is a function that takes a command as defined in the .spec file and return an answer created with create_answer()""" self._command_handler = command_handler def _add_remote_config_internal(self, module_spec, config_update_callback=None): """The guts of add_remote_config and add_remote_config_by_name""" module_cfg = ConfigData(module_spec) module_name = module_spec.get_module_name() self._session.group_subscribe(module_name) # Get the current config for that module now seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": module_name }), "ConfigManager") try: answer, _ = self._session.group_recvmsg(False, seq) except isc.cc.SessionTimeout: raise ModuleCCSessionError("No answer from ConfigManager when " "asking about Remote module " + module_name) call_callback = False if answer: rcode, value = parse_answer(answer) if rcode == 0: if value != None: if module_spec.validate_config(False, value): module_cfg.set_local_config(value) call_callback = True else: raise ModuleCCSessionError("Bad config data for " + module_name + ": " + str(value)) else: raise ModuleCCSessionError("Failure requesting remote " + "configuration data for " + module_name) # all done, add it self._remote_module_configs[module_name] = module_cfg self._remote_module_callbacks[module_name] = config_update_callback if call_callback and config_update_callback is not None: config_update_callback(value, module_cfg) def add_remote_config_by_name(self, module_name, config_update_callback=None): """ This does the same as add_remote_config, but you provide the module name instead of the name of the spec file. """ seq = self._session.group_sendmsg(create_command(COMMAND_GET_MODULE_SPEC, { "module_name": module_name }), "ConfigManager") try: answer, env = self._session.group_recvmsg(False, seq) except isc.cc.SessionTimeout: raise ModuleCCSessionError("No answer from ConfigManager when " + "asking about for spec of Remote " + "module " + module_name) if answer: rcode, value = parse_answer(answer) if rcode == 0: module_spec = isc.config.module_spec.ModuleSpec(value) if module_spec.get_module_name() != module_name: raise ModuleCCSessionError("Module name mismatch: " + module_name + " and " + module_spec.get_module_name()) self._add_remote_config_internal(module_spec, config_update_callback) else: raise ModuleCCSessionError("Error code " + str(rcode) + "when asking for module spec of " + module_name) else: raise ModuleCCSessionError("No answer when asking for module " + "spec of " + module_name) # Just to be consistent with the add_remote_config return module_name def add_remote_config(self, spec_file_name, config_update_callback=None): """Gives access to the configuration of a different module. These remote module options can at this moment only be accessed through get_remote_config_value(). This function also subscribes to the channel of the remote module name to receive the relevant updates. It is not possible to specify your own handler for this right now, but you can specify a callback that is called after the change happened. start() must have been called on this CCSession prior to the call to this method. Returns the name of the module.""" module_spec = isc.config.module_spec_from_file(spec_file_name) self._add_remote_config_internal(module_spec, config_update_callback) return module_spec.get_module_name() def remove_remote_config(self, module_name): """Removes the remote configuration access for this module""" if module_name in self._remote_module_configs: self._session.group_unsubscribe(module_name) del self._remote_module_configs[module_name] del self._remote_module_callbacks[module_name] def get_remote_config_value(self, module_name, identifier): """Returns the current setting for the given identifier at the given module. If the module has not been added with add_remote_config, a ModuleCCSessionError is raised""" if module_name in self._remote_module_configs: return self._remote_module_configs[module_name].get_value(identifier) else: raise ModuleCCSessionError("Remote module " + module_name + " not found") def __send_spec(self): """Sends the data specification to the configuration manager""" msg = create_command(COMMAND_MODULE_SPEC, self.get_module_spec().get_full_spec()) seq = self._session.group_sendmsg(msg, "ConfigManager") try: answer, env = self._session.group_recvmsg(False, seq) except isc.cc.SessionTimeout: # TODO: log an error? pass def __request_config(self): """Asks the configuration manager for the current configuration, and call the config handler if set. Raises a ModuleCCSessionError if there is no answer from the configuration manager""" seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": self._module_name }), "ConfigManager") try: answer, env = self._session.group_recvmsg(False, seq) if answer: rcode, value = parse_answer(answer) if rcode == 0: errors = [] if value != None: if self.get_module_spec().validate_config(False, value, errors): self.set_local_config(value) if self._config_handler: self._config_handler(value) else: raise ModuleCCSessionError( "Wrong data in configuration: " + " ".join(errors)) else: logger.error(CONFIG_GET_FAILED, value) else: raise ModuleCCSessionError("No answer from configuration manager") except isc.cc.SessionTimeout: raise ModuleCCSessionError("CC Session timeout waiting for configuration manager") def rpc_call(self, command, group, instance=CC_INSTANCE_WILDCARD, to=CC_TO_WILDCARD, params=None): """ Create a command with the given name and parameters. Send it to a recipient, wait for the answer and parse it. This is a wrapper around the group_sendmsg and group_recvmsg on the CC session. It exists mostly for convenience. Params: - command: Name of the command to call on the remote side. - group, instance, to: Address specification of the recipient. - params: Parameters to pass to the command (as keyword arguments). Return: The return value of the remote call (just the value, no status code or anything). May be None. Raise: - RPCRecipientMissing if the given recipient doesn't exist. - RPCError if the other side sent an error response. The error string is in the exception. - ModuleCCSessionError in case of protocol errors, like malformed answer. """ cmd = create_command(command, params) seq = self._session.group_sendmsg(cmd, group, instance=instance, to=to, want_answer=True) # For non-blocking, we'll have rpc_call_async (once the nonblock # actually works) reply, rheaders = self._session.group_recvmsg(nonblock=False, seq=seq) code, value = parse_answer(reply) if code == CC_REPLY_NO_RECPT: raise RPCRecipientMissing(value) elif code != CC_REPLY_SUCCESS: raise RPCError(code, value) return value def notify(self, notification_group, event_name, params=None): """ Send a notification to subscribed users. Send a notification message to all users subscribed to the given notification group. This method does not block. See docs/design/ipc-high.txt for details about notifications and the format of messages sent. Throws: - CCSessionError: for low-level communication errors. Params: - notification_group (string): This parameter (indirectly) signifies what users should receive the notification. Only users that subscribed to notifications on the same group receive it. - event_name (string): The name of the event to notify about (for example `new_group_member`). - params: Other parameters that describe the event. This might be, for example, the ID of the new member and the name of the group. This can be any data that can be sent over the isc.cc.Session, but it is common for it to be dict. Returns: Nothing """ notification = [event_name] if params is not None: notification.append(params) self._session.group_sendmsg({CC_PAYLOAD_NOTIFICATION: notification}, CC_GROUP_NOTIFICATION_PREFIX + notification_group, instance=CC_INSTANCE_WILDCARD, to=CC_TO_WILDCARD, want_answer=False) def subscribe_notification(self, notification_group, callback): """ Subscribe to receive notifications in given notification group. When a notification comes to the group, the callback is called with two parameters, the name of the event (the value of `event_name` parameter passed to `notify`) and the parameters of the event (the value of `params` passed to `notify`). This is a fast operation (there may be communication with the message queue daemon, but it does not wait for any remote process). The callback may get called multiple times (once for each notification). It is possible to subscribe multiple callbacks for the same notification, by multiple calls of this method, and they will be called in the order of registration when the notification comes. Throws: - CCSessionError: for low-level communication errors. Params: - notification_group (string): Notification group to subscribe to. Notification with the same value of the same parameter of `notify` will be received. - callback (callable): The callback to be called whenever the notification comes. The callback should not raise exceptions, such exceptions are likely to propagate through the loop and terminate program. Returns: Opaque id of the subscription. It can be used to cancel the subscription by unsubscribe_notification. """ self._last_notif_id += 1 my_id = self._last_notif_id if notification_group in self._notification_callbacks: self._notification_callbacks[notification_group][my_id] = callback else: self._session.group_subscribe(CC_GROUP_NOTIFICATION_PREFIX + notification_group) self._notification_callbacks[notification_group] = \ { my_id: callback } return (notification_group, my_id) def unsubscribe_notification(self, nid): """ Remove previous subscription for notifications. Pass the id returned from subscribe_notification. Throws: - CCSessionError: for low-level communication errors. - KeyError: The id does not correspond to valid subscription. """ (group, cid) = nid del self._notification_callbacks[group][cid] if not self._notification_callbacks[group]: # Removed the last one self._session.group_unsubscribe(CC_GROUP_NOTIFICATION_PREFIX + group) del self._notification_callbacks[group]
class ModuleCCSession(ConfigData): """This class maintains a connection to the command channel, as well as configuration options for modules. The module provides a specification file that contains the module name, configuration options, and commands. It also gives the ModuleCCSession two callback functions, one to call when there is a direct command to the module, and one to update the configuration run-time. These callbacks are called when 'check_command' is called on the ModuleCCSession""" def __init__(self, spec_file_name, config_handler, command_handler, cc_session=None, handle_logging_config=True, socket_file = None): """Initialize a ModuleCCSession. This does *NOT* send the specification and request the configuration yet. Use start() for that once the ModuleCCSession has been initialized. specfile_name is the path to the specification file. config_handler and command_handler are callback functions, see set_config_handler and set_command_handler for more information on their signatures. cc_session can be used to pass in an existing CCSession, if it is None, one will be set up. This is mainly intended for testing purposes. handle_logging_config: if True, the module session will automatically handle logging configuration for the module; it will read the system-wide Logging configuration and call the logger manager to apply it. It will also inform the logger manager when the logging configuration gets updated. The module does not need to do anything except intializing its loggers, and provide log messages. Defaults to true. socket_file: If cc_session was none, this optional argument specifies which socket file to use to connect to msgq. It will be overridden by the environment variable MSGQ_SOCKET_FILE. If none, and no environment variable is set, it will use the system default. """ module_spec = isc.config.module_spec_from_file(spec_file_name) ConfigData.__init__(self, module_spec) self._module_name = module_spec.get_module_name() self.set_config_handler(config_handler) self.set_command_handler(command_handler) if not cc_session: self._session = Session(socket_file) else: self._session = cc_session self._session.group_subscribe(self._module_name, "*") self._remote_module_configs = {} self._remote_module_callbacks = {} if handle_logging_config: self.add_remote_config(path_search('logging.spec', bind10_config.PLUGIN_PATHS), default_logconfig_handler) def __del__(self): # If the CC Session obejct has been closed, it returns # immediately. if self._session._closed: return self._session.group_unsubscribe(self._module_name, "*") for module_name in self._remote_module_configs: self._session.group_unsubscribe(module_name) def start(self): """Send the specification for this module to the configuration manager, and request the current non-default configuration. The config_handler will be called with that configuration""" self.__send_spec() self.__request_config() def send_stopping(self): """Sends a 'stopping' message to the configuration manager. This message is just an FYI, and no response is expected. Any errors when sending this message (for instance if the msgq session has previously been closed) are logged, but ignored.""" # create_command could raise an exception as well, but except for # out of memory related errors, these should all be programming # failures and are not caught msg = create_command(COMMAND_MODULE_STOPPING, self.get_module_spec().get_full_spec()) try: self._session.group_sendmsg(msg, "ConfigManager") except Exception as se: # If the session was previously closed, obvously trying to send # a message fails. (TODO: check if session is open so we can # error on real problems?) logger.error(CONFIG_SESSION_STOPPING_FAILED, se) def get_socket(self): """Returns the socket from the command channel session. This should *only* be used for select() loops to see if there is anything on the channel. If that loop is not completely time-critical, it is strongly recommended to only use check_command(), and not look at the socket at all.""" return self._session._socket def close(self): """Close the session to the command channel""" self._session.close() def check_command(self, nonblock=True): """Check whether there is a command or configuration update on the channel. This function does a read on the cc session, and returns nothing. It calls check_command_without_recvmsg() to parse the received message. If nonblock is True, it just checks if there's a command and does nothing if there isn't. If nonblock is False, it waits until it arrives. It temporarily sets timeout to infinity, because commands may not come in arbitrary long time.""" timeout_orig = self._session.get_timeout() self._session.set_timeout(0) try: msg, env = self._session.group_recvmsg(nonblock) finally: self._session.set_timeout(timeout_orig) self.check_command_without_recvmsg(msg, env) def check_command_without_recvmsg(self, msg, env): """Parse the given message to see if there is a command or a configuration update. Calls the corresponding handler functions if present. Responds on the channel if the handler returns a message.""" # should we default to an answer? success-by-default? unhandled error? if msg is not None and not 'result' in msg: answer = None try: module_name = env['group'] cmd, arg = isc.config.ccsession.parse_command(msg) if cmd == COMMAND_CONFIG_UPDATE: new_config = arg # If the target channel was not this module # it might be in the remote_module_configs if module_name != self._module_name: if module_name in self._remote_module_configs: # no checking for validity, that's up to the # module itself. newc = self._remote_module_configs[module_name].get_local_config() isc.cc.data.merge(newc, new_config) self._remote_module_configs[module_name].set_local_config(newc) if self._remote_module_callbacks[module_name] != None: self._remote_module_callbacks[module_name](new_config, self._remote_module_configs[module_name]) # For other modules, we're not supposed to answer return # ok, so apparently this update is for us. errors = [] if not self._config_handler: answer = create_answer(2, self._module_name + " has no config handler") elif not self.get_module_spec().validate_config(False, new_config, errors): answer = create_answer(1, ", ".join(errors)) else: isc.cc.data.remove_identical(new_config, self.get_local_config()) answer = self._config_handler(new_config) rcode, val = parse_answer(answer) if rcode == 0: newc = self.get_local_config() isc.cc.data.merge(newc, new_config) self.set_local_config(newc) else: # ignore commands for 'remote' modules if module_name == self._module_name: if self._command_handler: answer = self._command_handler(cmd, arg) else: answer = create_answer(2, self._module_name + " has no command handler") except Exception as exc: answer = create_answer(1, str(exc)) if answer: self._session.group_reply(env, answer) def set_config_handler(self, config_handler): """Set the config handler for this module. The handler is a function that takes the full configuration and handles it. It should return an answer created with create_answer()""" self._config_handler = config_handler # should we run this right now since we've changed the handler? def set_command_handler(self, command_handler): """Set the command handler for this module. The handler is a function that takes a command as defined in the .spec file and return an answer created with create_answer()""" self._command_handler = command_handler def _add_remote_config_internal(self, module_spec, config_update_callback=None): """The guts of add_remote_config and add_remote_config_by_name""" module_cfg = ConfigData(module_spec) module_name = module_spec.get_module_name() self._session.group_subscribe(module_name) # Get the current config for that module now seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": module_name }), "ConfigManager") try: answer, _ = self._session.group_recvmsg(False, seq) except isc.cc.SessionTimeout: raise ModuleCCSessionError("No answer from ConfigManager when " "asking about Remote module " + module_name) call_callback = False if answer: rcode, value = parse_answer(answer) if rcode == 0: if value != None: if module_spec.validate_config(False, value): module_cfg.set_local_config(value) call_callback = True else: raise ModuleCCSessionError("Bad config data for " + module_name + ": " + str(value)) else: raise ModuleCCSessionError("Failure requesting remote " + "configuration data for " + module_name) # all done, add it self._remote_module_configs[module_name] = module_cfg self._remote_module_callbacks[module_name] = config_update_callback if call_callback and config_update_callback is not None: config_update_callback(value, module_cfg) def add_remote_config_by_name(self, module_name, config_update_callback=None): """ This does the same as add_remote_config, but you provide the module name instead of the name of the spec file. """ seq = self._session.group_sendmsg(create_command(COMMAND_GET_MODULE_SPEC, { "module_name": module_name }), "ConfigManager") try: answer, env = self._session.group_recvmsg(False, seq) except isc.cc.SessionTimeout: raise ModuleCCSessionError("No answer from ConfigManager when " + "asking about for spec of Remote " + "module " + module_name) if answer: rcode, value = parse_answer(answer) if rcode == 0: module_spec = isc.config.module_spec.ModuleSpec(value) if module_spec.get_module_name() != module_name: raise ModuleCCSessionError("Module name mismatch: " + module_name + " and " + module_spec.get_module_name()) self._add_remote_config_internal(module_spec, config_update_callback) else: raise ModuleCCSessionError("Error code " + str(rcode) + "when asking for module spec of " + module_name) else: raise ModuleCCSessionError("No answer when asking for module " + "spec of " + module_name) # Just to be consistent with the add_remote_config return module_name def add_remote_config(self, spec_file_name, config_update_callback=None): """Gives access to the configuration of a different module. These remote module options can at this moment only be accessed through get_remote_config_value(). This function also subscribes to the channel of the remote module name to receive the relevant updates. It is not possible to specify your own handler for this right now, but you can specify a callback that is called after the change happened. start() must have been called on this CCSession prior to the call to this method. Returns the name of the module.""" module_spec = isc.config.module_spec_from_file(spec_file_name) self._add_remote_config_internal(module_spec, config_update_callback) return module_spec.get_module_name() def remove_remote_config(self, module_name): """Removes the remote configuration access for this module""" if module_name in self._remote_module_configs: self._session.group_unsubscribe(module_name) del self._remote_module_configs[module_name] del self._remote_module_callbacks[module_name] def get_remote_config_value(self, module_name, identifier): """Returns the current setting for the given identifier at the given module. If the module has not been added with add_remote_config, a ModuleCCSessionError is raised""" if module_name in self._remote_module_configs: return self._remote_module_configs[module_name].get_value(identifier) else: raise ModuleCCSessionError("Remote module " + module_name + " not found") def __send_spec(self): """Sends the data specification to the configuration manager""" msg = create_command(COMMAND_MODULE_SPEC, self.get_module_spec().get_full_spec()) seq = self._session.group_sendmsg(msg, "ConfigManager") try: answer, env = self._session.group_recvmsg(False, seq) except isc.cc.SessionTimeout: # TODO: log an error? pass def __request_config(self): """Asks the configuration manager for the current configuration, and call the config handler if set. Raises a ModuleCCSessionError if there is no answer from the configuration manager""" seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": self._module_name }), "ConfigManager") try: answer, env = self._session.group_recvmsg(False, seq) if answer: rcode, value = parse_answer(answer) if rcode == 0: errors = [] if value != None: if self.get_module_spec().validate_config(False, value, errors): self.set_local_config(value) if self._config_handler: self._config_handler(value) else: raise ModuleCCSessionError( "Wrong data in configuration: " + " ".join(errors)) else: logger.error(CONFIG_GET_FAILED, value) else: raise ModuleCCSessionError("No answer from configuration manager") except isc.cc.SessionTimeout: raise ModuleCCSessionError("CC Session timeout waiting for configuration manager")