def get(self): session = SessionHandler() session.logout() clear_cookie(self, name="_ut_") if self.GET("r"): url = urllib.unquote(str(self.GET("r"))) else: url = self.request.referer self.redirect(url)
def get(self): """ Handles the /logout endpoint. Logs out users. """ session = SessionHandler() session.logout() clear_cookie(self, name="_ut_") success = "You have logged out successfully!" success_message(self, success) self.redirect("/login")
def _configure(self, config_name, action, contents): """ The main configuration for volttron central. This is where validation will occur. Note this method is called: 1. When the agent first starts (with the params from packaged agent file) 2. When 'store' is called through the volttron-ctl config command line with 'config' as the name. Required Configuration: The volttron central requires a user mapping. :param config_name: :param action: :param contents: """ config = self._default_config.copy() config.update(contents) users = config.get("users", None) if self._authenticated_sessions: self._authenticated_sessions.clear() if users is None: users = {} _log.warn("No users are available for logging in!") # Unregister all routes for vc and then re-add down below. self.vip.web.unregister_all_routes() self._authenticated_sessions = SessionHandler(Authenticate(users)) self.vip.web.register_endpoint(r'/vc/jsonrpc', self.jsonrpc) self.vip.web.register_websocket(r'/vc/ws', self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) self.vip.web.register_path(r'^/vc/.*', config.get('webroot')) # Start scanning for new platforms connections as well as for # disconnects that happen. self._scan_platform_connect_disconnect()
def __init__(self, request=None, response=None): self.initialize(request, response) self.tv = {} self.tv["user"] = None self.tv["local"] = APP_IS_LOCAL self.tv["show_breadcrumb"] = True self.tv["show_add_dataset"] = True self.tv["v"] = os.environ.get('CURRENT_VERSION_ID') self.user = None self.GET = self.request.get self.POST = self.request.POST.get try: self.user = SessionHandler().owner global_vars.user = self.user except Exception, e: self.user = None
class VolttronCentralAgent(Agent): """ Agent for managing many volttron instances from a central web ui. During the """ def __init__(self, webroot=DEFAULT_WEB_ROOT, users={}, topic_replace_list=[], **kwargs): """ Creates a `VolttronCentralAgent` object to manage instances. Each instances that is registered must contain a running `VolttronCentralPlatform`. Through this conduit the `VolttronCentralAgent` is able to communicate securly and efficiently. :param config_path: :param kwargs: :return: """ _log.info("{} constructing...".format(self.__class__.__name__)) super(VolttronCentralAgent, self).__init__(enable_web=True, **kwargs) # Create default configuration to be used in case of problems in the # packaged agent configuration file. self._default_config = dict( webroot=os.path.abspath(webroot), users=users, topic_replace_list=topic_replace_list ) self.vip.config.set_default("config", self._default_config) # Start using config store. self.vip.config.subscribe(self._configure, actions=["NEW", "UPDATE"], pattern="config") # # # During the configuration update/new/delete action this will be # # updated to the current configuration. # self.runtime_config = None # # # Start using config store. # self.vip.config.set_default("config", config) # self.vip.config.subscribe(self.configure_main, # actions=['NEW', 'UPDATE', 'DELETE'], # pattern="config") # # # Use config store to update the settings of a platform's configuration. # self.vip.config.subscribe(self.configure_platforms, # actions=['NEW', 'UPDATE', 'DELETE'], # pattern="platforms/*") # # # mapping from the real topic into the replacement. # self.replaced_topic_map = {} # # # mapping from md5 hash of address to the actual connection to the # # remote instance. # self.vcp_connections = {} # # # Current sessions available to the # self.web_sessions = None # # # Platform health based upon device driver publishes # self.device_health = defaultdict(dict) # # # Used to hold scheduled reconnection event for vcp agents. # self._vcp_reconnect_event = None # # # the registered socket endpoints so we can send out management # # events to all the registered session. self._websocket_endpoints = set() self._platforms = Platforms(self) self._platform_scan_event = None # Sessions that have been authentication with the system. self._authenticated_sessions = None def _configure(self, config_name, action, contents): """ The main configuration for volttron central. This is where validation will occur. Note this method is called: 1. When the agent first starts (with the params from packaged agent file) 2. When 'store' is called through the volttron-ctl config command line with 'config' as the name. Required Configuration: The volttron central requires a user mapping. :param config_name: :param action: :param contents: """ config = self._default_config.copy() config.update(contents) users = config.get("users", None) if self._authenticated_sessions: self._authenticated_sessions.clear() if users is None: users = {} _log.warn("No users are available for logging in!") # Unregister all routes for vc and then re-add down below. self.vip.web.unregister_all_routes() self._authenticated_sessions = SessionHandler(Authenticate(users)) self.vip.web.register_endpoint(r'/vc/jsonrpc', self.jsonrpc) self.vip.web.register_websocket(r'/vc/ws', self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) self.vip.web.register_path(r'^/vc/.*', config.get('webroot')) # Start scanning for new platforms connections as well as for # disconnects that happen. self._scan_platform_connect_disconnect() @staticmethod def _get_next_time_seconds(seconds=10): now = get_aware_utc_now() next_time = now + datetime.timedelta(seconds=seconds) return next_time def _handle_platform_connection(self, platform_vip_identity): _log.info("Handling new platform connection {}".format( platform_vip_identity)) platform = self._platforms.add_platform(platform_vip_identity) def _handle_platform_disconnect(self, platform_vip_identity): _log.warn("Handling disconnection of connection from identity: {}".format( platform_vip_identity )) # TODO send alert that there was a platform disconnect. self._platforms.disconnect_platform(platform_vip_identity) def _scan_platform_connect_disconnect(self): """ Scan the local bus for peers that start with 'vcp-'. Handle the connection and disconnection events here. """ if self._platform_scan_event is not None: # This won't hurt anything if we are canceling ourselves. self._platform_scan_event.cancel() # Identities of all platform agents that are connecting to us should # have an identity of platform.md5hash. connected_platforms = set([x for x in self.vip.peerlist().get(timeout=5) if x.startswith('vcp-')]) disconnected = self._platforms.get_platform_keys() - connected_platforms for vip_id in disconnected: self._handle_platform_disconnect(vip_id) not_known = connected_platforms - self._platforms.get_platform_keys() for vip_id in not_known: self._handle_platform_connection(vip_id) next_platform_scan = VolttronCentralAgent._get_next_time_seconds() # reschedule the next scan. self._platform_scan_event = self.core.schedule( next_platform_scan, self._scan_platform_connect_disconnect) def configure_platforms(self, config_name, action, contents): _log.debug('Platform configuration updated.') _log.debug('ACTION IS {}'.format(action)) _log.debug('CONTENT IS {}'.format(contents)) def open_authenticate_ws_endpoint(self, fromip, endpoint): """ Callback method from when websockets are opened. The endpoine must be '/' delimited with the second to last section being the session of a logged in user to volttron central itself. :param fromip: :param endpoint: A string representing the endpoint of the websocket. :return: """ _log.debug("OPENED ip: {} endpoint: {}".format(fromip, endpoint)) try: session = endpoint.split('/')[-2] except IndexError: _log.error("Malformed endpoint. Must be delimited by '/'") _log.error( 'Endpoint must have valid session in second to last position') return False if not self._authenticated_sessions.check_session(session, fromip): _log.error("Authentication error for session!") return False _log.debug('Websocket allowed.') self._websocket_endpoints.add(endpoint) return True def _ws_closed(self, endpoint): _log.debug("CLOSED endpoint: {}".format(endpoint)) try: self._websocket_endpoints.remove(endpoint) except KeyError: pass # This should never happen but protect against it anyways. def _ws_received(self, endpoint, message): _log.debug("RECEIVED endpoint: {} message: {}".format(endpoint, message)) @RPC.export def is_registered(self, address_hash=None, address=None): if address_hash is None and address is None: return False if address_hash is None: address_hash = PlatformHandler.address_hasher(address) return self._platforms.is_registered(address_hash) @RPC.export def get_publickey(self): """ RPC method allowing the caller to retrieve the publickey of this agent. This method is available for allowing :class:`VolttronCentralPlatform` agents to allow this agent to be able to connect to its instance. :return: The publickey of this volttron central agent. :rtype: str """ return self.core.publickey @RPC.export def unregister_platform(self, platform_uuid): _log.debug('unregister_platform') platform = self._registered_platforms.get(platform_uuid) if platform: connected = self._platform_connections.get(platform_uuid) if connected is not None: connected.call('unmanage') connected.kill() address = None for v in self._address_to_uuid.values(): if v == platform_uuid: address = v break if address: del self._address_to_uuid[address] del self._platform_connections[platform_uuid] del self._registered_platforms[platform_uuid] self._registered_platforms.sync() context = 'Unregistered platform {}'.format(platform_uuid) return {'status': 'SUCCESS', 'context': context} else: msg = 'Unable to unregistered platform {}'.format(platform_uuid) return {'error': {'code': UNABLE_TO_UNREGISTER_INSTANCE, 'message': msg}} def _to_jsonrpc_obj(self, jsonrpcstr): """ Convert data string into a JsonRpcData named tuple. :param object data: Either a string or a dictionary representing a json document. """ return jsonrpc.JsonRpcData.parse(jsonrpcstr) def jsonrpc(self, env, data): """ The main entry point for ^jsonrpc data This method will only accept rpcdata. The first time this method is called, per session, it must be using get_authorization. That will return a session token that must be included in every subsequent request. The session is tied to the ip address of the caller. :param object env: Environment dictionary for the request. :param object data: The JSON-RPC 2.0 method to call. :return object: An JSON-RPC 2.0 response. """ if env['REQUEST_METHOD'].upper() != 'POST': return jsonrpc.json_error('NA', INVALID_REQUEST, 'Invalid request method, only POST allowed' ) try: rpcdata = self._to_jsonrpc_obj(data) _log.info('rpc method: {}'.format(rpcdata.method)) if rpcdata.method == 'get_authorization': args = {'username': rpcdata.params['username'], 'password': rpcdata.params['password'], 'ip': env['REMOTE_ADDR']} sess = self._authenticated_sessions.authenticate(**args) if not sess: _log.info('Invalid username/password for {}'.format( rpcdata.params['username'])) return jsonrpc.json_error( rpcdata.id, UNAUTHORIZED, "Invalid username/password specified.") _log.info('Session created for {}'.format( rpcdata.params['username'])) self.vip.web.register_websocket( "/vc/ws/{}/management".format(sess), self.open_authenticate_ws_endpoint, self._ws_closed, self._received_data) _log.info('Session created for {}'.format( rpcdata.params['username'])) gevent.sleep(1) return jsonrpc.json_result(rpcdata.id, sess) token = rpcdata.authorization ip = env['REMOTE_ADDR'] _log.debug('REMOTE_ADDR: {}'.format(ip)) session_user = self._authenticated_sessions.check_session(token, ip) _log.debug('SESSION_USER IS: {}'.format(session_user)) if not session_user: _log.debug("Session Check Failed for Token: {}".format(token)) return jsonrpc.json_error(rpcdata.id, UNAUTHORIZED, "Invalid authentication token") _log.debug('RPC METHOD IS: {}'.format(rpcdata.method)) # Route any other method that isn't result_or_error = self._route_request(session_user, rpcdata.id, rpcdata.method, rpcdata.params) except AssertionError: return jsonrpc.json_error( 'NA', INVALID_REQUEST, 'Invalid rpc data {}'.format(data)) except Unreachable: return jsonrpc.json_error( rpcdata.id, UNAVAILABLE_PLATFORM, "Couldn't reach platform with method {} params: {}".format( rpcdata.method, rpcdata.params)) except Exception as e: return jsonrpc.json_error( 'NA', UNHANDLED_EXCEPTION, e ) return self._get_jsonrpc_response(rpcdata.id, result_or_error) def _get_jsonrpc_response(self, id, result_or_error): """ Wrap the response in either a json-rpc error or result. :param id: :param result_or_error: :return: """ if isinstance(result_or_error, dict): if 'jsonrpc' in result_or_error: return result_or_error if result_or_error is not None and isinstance(result_or_error, dict): if 'error' in result_or_error: error = result_or_error['error'] _log.debug("RPC RESPONSE ERROR: {}".format(error)) return jsonrpc.json_error(id, error['code'], error['message']) return jsonrpc.json_result(id, result_or_error) def _get_agents(self, instance_uuid, groups): """ Retrieve the list of agents on a specific platform. :param instance_uuid: :param groups: :return: """ _log.debug('_get_agents') connected_to_pa = self._platform_connections[instance_uuid] agents = connected_to_pa.agent.vip.rpc.call( 'platform.agent', 'list_agents').get(timeout=30) for a in agents: if 'admin' in groups: if "platformagent" in a['name'] or \ "volttroncentral" in a['name']: a['vc_can_start'] = False a['vc_can_stop'] = False a['vc_can_restart'] = True else: a['vc_can_start'] = True a['vc_can_stop'] = True a['vc_can_restart'] = True else: # Handle the permissions that are not admin. a['vc_can_start'] = False a['vc_can_stop'] = False a['vc_can_restart'] = False _log.debug('Agents returned: {}'.format(agents)) return agents def _setupexternal(self): _log.debug(self.vip.ping('', "PING ROUTER?").get(timeout=3)) def _configure_agent(self, endpoint, message): _log.debug('Configure agent: {} message: {}'.format(endpoint, message)) def _received_data(self, endpoint, message): print('Received from endpoint {} message: {}'.format(endpoint, message)) self.vip.web.send(endpoint, message) def set_setting(self, session_user, params): """ Sets or removes a setting from the config store. If the value is None then the item will be removed from the store. If there is an error in saving the value then a jsonrpc.json_error object is returned. :param session_user: Unused :param params: Dictionary that must contain 'key' and 'value' keys. :return: A 'SUCCESS' string or a jsonrpc.json_error object. """ if 'key' not in params or not params['key']: return jsonrpc.json_error(params['message_id'], INVALID_PARAMS, 'Invalid parameter key not set') if 'value' not in params: return jsonrpc.json_error(params['message_id'], INVALID_PARAMS, 'Invalid parameter key not set') config_key = "settings/{}".format(params['key']) value = params['value'] if value is None: try: self.vip.config.delete(config_key) except KeyError: pass else: # We handle empt string here because the config store doesn't allow # empty strings to be set as a config store. I wasn't able to # trap the ValueError that is raised on the server side. if value == "": return jsonrpc.json_error(params['message_id'], INVALID_PARAMS, 'Invalid value set (empty string?)') self.vip.config.set(config_key, value) return 'SUCCESS' def get_setting(self, session_user, params): """ Retrieve a value from the passed setting key. The params object must contain a "key" to return from the settings store. :param session_user: Unused :param params: Dictionary that must contain a 'key' key. :return: The value or a jsonrpc error object. """ config_key = "settings/{}".format(params['key']) try: value = self.vip.config.get(config_key) except KeyError: return jsonrpc.json_error(params['message_id'], INVALID_PARAMS, 'Invalid key specified') else: return value def get_setting_keys(self, session_user, params): """ Returns a list of all of the settings keys so the caller can know what settings to request. :param session_user: Unused :param params: Unused :return: A list of settings available to the caller. """ prefix = "settings/" keys = [x[len(prefix):] for x in self.vip.config.list() if x.startswith(prefix)] return keys or [] def _handle_bacnet_props(self, session_user, params): platform_uuid = params.pop('platform_uuid') id = params.pop('message_id') _log.debug('Handling bacnet_props platform: {}'.format(platform_uuid)) configure_topic = "{}/configure".format(session_user['token']) ws_socket_topic = "/vc/ws/{}".format(configure_topic) if configure_topic not in self._websocket_endpoints: self.vip.web.register_websocket(ws_socket_topic, self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) def start_sending_props(): response_topic = "configure/{}".format(session_user['token']) # Two ways we could have handled this is to pop the identity off # of the params and then passed both the identity and the response # topic. Or what I chose to do and to put the argument in a # copy of the params. cp = params.copy() cp['publish_topic'] = response_topic cp['device_id'] = int(cp['device_id']) platform = self._platforms.get_platform(platform_uuid) _log.debug('PARAMS: {}'.format(cp)) platform.call("publish_bacnet_props", **cp) gevent.spawn_later(2, start_sending_props) def _handle_bacnet_scan(self, session_user, params): platform_uuid = params.pop('platform_uuid') id = params.pop('message_id') _log.debug('Handling bacnet_scan platform: {}'.format(platform_uuid)) if not self._platforms.is_registered(platform_uuid): return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "Couldn't connect to platform {}".format( platform_uuid )) scan_length = params.pop('scan_length', 5) try: scan_length = float(scan_length) params['scan_length'] = scan_length platform = self._platforms.get_platform(platform_uuid) iam_topic = "{}/iam".format(session_user['token']) ws_socket_topic = "/vc/ws/{}".format(iam_topic) self.vip.web.register_websocket(ws_socket_topic, self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) def start_scan(): # We want the datatype (iam) to be second in the response so # we need to reposition the iam and the session id to the topic # that is passed to the rpc function on vcp iam_session_topic = "iam/{}".format(session_user['token']) platform.call("start_bacnet_scan", iam_session_topic, **params) def close_socket(): _log.debug('Closing bacnet scan for {}'.format( platform_uuid)) gevent.spawn_later(2, self.vip.web.unregister_websocket, iam_session_topic) gevent.spawn_later(scan_length, close_socket) # By starting the scan a second later we allow the websocket # client to subscribe to the newly available endpoint. gevent.spawn_later(2, start_scan) except ValueError: return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "Couldn't connect to platform {}".format( platform_uuid )) except KeyError: return jsonrpc.json_error(id, UNAUTHORIZED, "Invalid user session token") def _enable_setup_mode(self, session_user, params): id = params.pop('message_id') if 'admin' not in session_user['groups']: _log.debug('Returning json_error enable_setup_mode') return jsonrpc.json_error( id, UNAUTHORIZED, "Admin access is required to enable setup mode") auth_file = AuthFile() entries = auth_file.find_by_credentials(".*") if len(entries) > 0: return "SUCCESS" entry = AuthEntry(credentials="/.*/", comments="Un-Authenticated connections allowed here", user_id="unknown") auth_file.add(entry) return "SUCCESS" def _disable_setup_mode(self, session_user, params): id = params.pop('message_id') if 'admin' not in session_user['groups']: _log.debug('Returning json_error disable_setup_mode') return jsonrpc.json_error( id, UNAUTHORIZED, "Admin access is required to disable setup mode") auth_file = AuthFile() auth_file.remove_by_credentials("/.*/") return "SUCCESS" def _handle_management_endpoint(self, session_user, params): ws_topic = "/vc/ws/{}/management".format(session_user.get('token')) self.vip.web.register_websocket(ws_topic, self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) return ws_topic def send_management_message(self, type, data={}): """ Send a message to any socket that has connected to the management socket. The payload sent to the client is like the following:: { "type": "UPDATE_DEVICE_STATUS", "data": "this is data that was passed" } :param type: A string defining a unique type for sending to the websockets. :param data: An object that str can be called on. :type type: str :type data: serializable """ management_sockets = [s for s in self._websocket_endpoints if s.endswith("management")] # Nothing to send if we don't have any management sockets open. if len(management_sockets) <= 0: return if data is None: data = {} payload = dict( type=type, data=str(data) ) payload = jsonapi.dumps(payload) for s in management_sockets: self.vip.web.send(s, payload) def _route_request(self, session_user, id, method, params): """ Handle the methods volttron central can or pass off to platforms. :param session_user: The authenticated user's session info. :param id: JSON-RPC id field. :param method: :param params: :return: """ _log.debug( 'inside _route_request {}, {}, {}'.format(id, method, params)) def err(message, code=METHOD_NOT_FOUND): return {'error': {'code': code, 'message': message}} self.send_management_message(method) method_split = method.split('.') # The last part of the jsonrpc method is the actual method to be called. method_check = method_split[-1] # These functions will be sent to a platform.agent on either this # instance or another. All of these functions have the same interface # and can be collected into a dictionary rather than an if tree. platform_methods = dict( # bacnet related start_bacnet_scan=self._handle_bacnet_scan, publish_bacnet_props=self._handle_bacnet_props, # config store related store_agent_config="store_agent_config", get_agent_config="get_agent_config", list_agent_configs="get_agent_config_list", # management related list_agents="get_agent_list", get_devices="get_devices", status_agents="status_agents" ) # These methods are specifically to be handled by the platform not any # agents on the platform that is why we have the length requirement. # # The jsonrpc method looks like the following # # platform.uuid.<dynamic entry>.method_on_vcp if method_check in platform_methods: platform_uuid = None if isinstance(params, dict): platform_uuid = params.pop('platform_uuid', None) if platform_uuid is None: if method_split[0] == 'platforms' and method_split[1] == 'uuid': platform_uuid = method_split[2] if not platform_uuid: return err("Invalid platform_uuid specified as parameter" .format(platform_uuid), INVALID_PARAMS) if not self._platforms.is_registered(platform_uuid): return err("Unknown or unavailable platform {} specified as " "parameter".format(platform_uuid), UNAVAILABLE_PLATFORM) try: _log.debug('Calling {} on platform {}'.format( method_check, platform_uuid )) class_method = platform_methods[method_check] platform = self._platforms.get_platform(platform_uuid) # Determine whether the method to call is on the current class # or on the platform object. if isinstance(class_method, basestring): method_ref = getattr(platform, class_method) else: method_ref = class_method # Put the platform_uuid in the params so it can be used # inside the method params['platform_uuid'] = platform_uuid except AttributeError or KeyError: return jsonrpc.json_error(id, INTERNAL_ERROR, "Attempted calling function " "{} was unavailable".format( class_method )) except ValueError: return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "Couldn't connect to platform " "{}".format(platform_uuid)) else: # pass the id through the message_id parameter. if not params: params = dict(message_id=id) else: params['message_id'] = id # Methods will all have the signature # method(session, params) # return method_ref(session_user, params) vc_methods = dict( register_management_endpoint=self._handle_management_endpoint, list_platforms=self._platforms.get_platform_list, list_performance=self._platforms.get_performance_list, # Settings set_setting=self.set_setting, get_setting=self.get_setting, get_setting_keys=self.get_setting_keys, # Setup mode enable_setup_mode=self._enable_setup_mode, disable_setup_mode=self._disable_setup_mode ) if method in vc_methods: if not params: params = dict(message_id=id) else: params['message_id'] = id response = vc_methods[method](session_user, params) _log.debug("Response is {}".format(response)) return response # vc_methods[method](session_user, params) if method == 'register_instance': if isinstance(params, list): return self._register_instance(*params) else: return self._register_instance(**params) elif method == 'unregister_platform': return self.unregister_platform(params['instance_uuid']) elif 'historian' in method: has_platform_historian = PLATFORM_HISTORIAN in \ self.vip.peerlist().get(timeout=30) if not has_platform_historian: return err( 'The VOLTTRON Central platform historian is unavailable.', UNAVAILABLE_AGENT) _log.debug('Trapping platform.historian to vc.') _log.debug('has_platform_historian: {}'.format( has_platform_historian)) if 'historian.query' in method: return self.vip.rpc.call( PLATFORM_HISTORIAN, 'query', **params).get(timeout=30) elif 'historian.get_topic_list' in method: return self.vip.rpc.call( PLATFORM_HISTORIAN, 'get_topic_list').get(timeout=30) # This isn't known as a proper method on vc or a platform. if len(method_split) < 3: return err('Unknown method {}'.format(method)) if method_split[0] != 'platforms' or method_split[1] != 'uuid': return err('Invalid format for instance must start with ' 'platforms.uuid') instance_uuid = method_split[2] _log.debug('Instance uuid is: {}'.format(instance_uuid)) if not self._platforms.is_registered(instance_uuid): return err('Unknown platform {}'.format(instance_uuid)) platform_method = '.'.join(method_split[3:]) _log.debug("Platform method is: {}".format(platform_method)) platform = self._platforms.get_platform(instance_uuid) if not platform: return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "cannot connect to platform." ) if platform_method.startswith('install'): if 'admin' not in session_user['groups']: return jsonrpc.json_error( id, UNAUTHORIZED, "Admin access is required to install agents") return platform.route_to_agent_method(id, platform_method, params) def _validate_config_params(self, config): """ Validate the configuration parameters of the default/updated parameters. This method will return a list of "problems" with the configuration. If there are no problems then an empty list is returned. :param config: Configuration parameters for the volttron central agent. :type config: dict :return: The problems if any, [] if no problems :rtype: list """ problems = [] webroot = config.get('webroot') if not webroot: problems.append('Invalid webroot in configuration.') elif not os.path.exists(webroot): problems.append( 'Webroot {} does not exist on machine'.format(webroot)) users = config.get('users') if not users: problems.append('A users node must be specified!') else: has_admin = False try: for user, item in users.items(): if 'password' not in item.keys(): problems.append('user {} must have a password!'.format( user)) elif not item['password']: problems.append('password for {} is blank!'.format( user )) if 'groups' not in item.keys(): problems.append('missing groups key for user {}'.format( user )) elif not isinstance(item['groups'], list): problems.append('groups must be a list of strings.') elif not item['groups']: problems.append( 'user {} must belong to at least one group.'.format( user)) # See if there is an adminstator present. if not has_admin and isinstance(item['groups'], list): has_admin = 'admin' in item['groups'] except AttributeError: problems.append('invalid user node.') if not has_admin: problems.append("One user must be in the admin group.") return problems
def configure_main(self, config_name, action, contents): """ The main configuration for volttron central. This is where validation will occur. Note this method is called: 1. When the agent first starts (with the params from packaged agent file) 2. When 'store' is called through the volttron-ctl config command line with 'config' as the name. Required Configuration: The volttron central requires a user mapping. :param config_name: :param action: :param contents: """ _log.debug('Main config updated') _log.debug('ACTION IS {}'.format(action)) _log.debug('CONTENT IS {}'.format(contents)) if action == 'DELETE': # Remove the registry and keep the service running. self.runtime_config = None # Now stop the exposition of service. else: self.runtime_config = self.default_config.copy() self.runtime_config.update(contents) problems = self._validate_config_params(self.runtime_config) if len(problems) > 0: _log.error( "The following configuration problems were detected!") for p in problems: _log.error(p) sys.exit(INVALID_CONFIGURATION_CODE) else: _log.info('volttron central webroot is: {}'.format( self.runtime_config.get('webroot'))) users = self.runtime_config.get('users') self.web_sessions = SessionHandler(Authenticate(users)) _log.debug('Querying router for addresses and serverkey.') q = Query(self.core) external_addresses = q.query('addresses').get(timeout=5) self.runtime_config['local_external_address'] = external_addresses[ 0] self.vip.web.register_websocket(r'/vc/ws', self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) self.vip.web.register_endpoint(r'/jsonrpc', self.jsonrpc) self.vip.web.register_path(r'^/.*', self.runtime_config.get('webroot')) # Start scanning for new platforms connections as well as for # disconnects that happen. self._scan_for_platforms()
def post(self): """ Handles the /login endpoint. Logs in users. """ if self.POST("email") and self.POST("password"): url = "/login" redirect = None email = self.POST("email").strip().lower() query = User.query() query = query.filter(User.current_email == email) user = query.get() if self.POST("redirect"): redirect = urllib.quote(self.POST("redirect")) url += "?redirect=" + str(redirect) if not user: error = "Invalid email or password." error_message(self, error) self.redirect(url) return if user.hashed_password: if not user.verify_password(self.POST("password")): error = "Invalid email or password." error_message(self, error) self.redirect(url) return else: password = hp(email=email, password=self.POST("password")) if user.password != password: error = "Invalid email or password." error_message(self, error) self.redirect(url) return else: user.hashed_password = user.hash_password( self.POST("password")) user.put() if user.status == "PENDING": error = "Your account has not been verified. " error += "Please verify your account by opening the " error += "verification email we sent you. " error_message(self, error) self.redirect(url) return if user.status == "DISABLED": error = "Your account has been disabled. " error += "Please contact the Geostore Admin." error_message(self, error) self.redirect(url) return if user.role in ["AGENCYADMIN", "USER"]: if user.status == "VERIFIED": error = "Your account is still pending approval. " error += "Once your account is approved, you will be able " error += "to login. You will receive an email once your " error += "account is approved." error_message(self, error) self.redirect(url) return if user.status == "DISAPPROVED": error = "Your account has been disapproved. " error += "Please contact the Geostore Admin." error_message(self, error) self.redirect(url) return user.csrf_token = generate_token() session = SessionHandler(user) session.login() code = session.generate_login_code() if self.POST("redirect"): self.redirect(urllib.unquote(str(self.POST("redirect")))) else: self.redirect("/dashboard") return error = "Please enter your email and password." error_message(self, error) self.redirect("/login")
def __init__(self, config_path, **kwargs): """ Creates a `VolttronCentralAgent` object to manage instances. Each instances that is registered must contain a running `VolttronCentralPlatform`. Through this conduit the `VolttronCentralAgent` is able to communicate securly and efficiently. :param config_path: :param kwargs: :return: """ _log.info("{} constructing...".format(self.__name__)) # This is a special object so only use it's identity. identity = kwargs.pop("identity", None) identity = VOLTTRON_CENTRAL super(VolttronCentralAgent, self).__init__(identity=identity, **kwargs) # Load the configuration into a dictionary self._config = utils.load_config(config_path) # Expose the webroot property to be customized through the config # file. self._webroot = self._config.get('webroot', DEFAULT_WEB_ROOT) if self._webroot.endswith('/'): self._webroot = self._webroot[:-1] _log.debug('The webroot is {}'.format(self._webroot)) # Required users self._user_map = self._config.get('users', None) _log.debug("User map is: {}".format(self._user_map)) if self._user_map is None: raise ValueError('users not specified within the config file.') # Search and replace for topics # The difference between the list and the map is that the list # specifies the search and replaces that should be done on all of the # incoming topics. Once all of the search and replaces are done then # the mapping from the original to the final is stored in the map. self._topic_replace_list = self._config.get('topic_replace_list', []) self._topic_replace_map = defaultdict(str) _log.debug('Topic replace list: {}'.format(self._topic_replace_list)) # A resource directory that contains everything that can be looked up. self._resources = ResourceDirectory() self._registry = self._resources.platform_registry # This has a dictionary mapping the platform_uuid to an agent # connected to the vip-address of the registered platform. If the # registered platform is None then that means we were unable to # connect to the platform the last time it was tried. self._pa_agents = {} # if there is a volttron central agent on this instance then this # will be resolved. self._peer_platform = None # An object that allows the checking of currently authenticated # sessions. self._sessions = SessionHandler(Authenticate(self._user_map)) self.webaddress = None self._web_info = None # A flag that tels us that we are in the process of updating already. # This will allow us to not have multiple periodic calls at the same # time which could cause unpredicatable results. self._flag_updating_deviceregistry = False self._setting_store = load_create_store( os.path.join(os.environ['VOLTTRON_HOME'], 'data', 'volttron.central.settings')) self._request_store = load_create_store( os.path.join(os.environ['VOLTTRON_HOME'], 'data', 'volttron.central.requeststore'))
class VolttronCentralAgent(Agent): """ Agent for managing many volttron instances from a central web ui. During the """ __name__ = 'VolttronCentralAgent' def __init__(self, config_path, **kwargs): """ Creates a `VolttronCentralAgent` object to manage instances. Each instances that is registered must contain a running `VolttronCentralPlatform`. Through this conduit the `VolttronCentralAgent` is able to communicate securly and efficiently. :param config_path: :param kwargs: :return: """ _log.info("{} constructing...".format(self.__name__)) # This is a special object so only use it's identity. identity = kwargs.pop("identity", None) identity = VOLTTRON_CENTRAL super(VolttronCentralAgent, self).__init__(identity=identity, **kwargs) # Load the configuration into a dictionary self._config = utils.load_config(config_path) # Expose the webroot property to be customized through the config # file. self._webroot = self._config.get('webroot', DEFAULT_WEB_ROOT) if self._webroot.endswith('/'): self._webroot = self._webroot[:-1] _log.debug('The webroot is {}'.format(self._webroot)) # Required users self._user_map = self._config.get('users', None) _log.debug("User map is: {}".format(self._user_map)) if self._user_map is None: raise ValueError('users not specified within the config file.') # Search and replace for topics # The difference between the list and the map is that the list # specifies the search and replaces that should be done on all of the # incoming topics. Once all of the search and replaces are done then # the mapping from the original to the final is stored in the map. self._topic_replace_list = self._config.get('topic_replace_list', []) self._topic_replace_map = defaultdict(str) _log.debug('Topic replace list: {}'.format(self._topic_replace_list)) # A resource directory that contains everything that can be looked up. self._resources = ResourceDirectory() self._registry = self._resources.platform_registry # This has a dictionary mapping the platform_uuid to an agent # connected to the vip-address of the registered platform. If the # registered platform is None then that means we were unable to # connect to the platform the last time it was tried. self._pa_agents = {} # if there is a volttron central agent on this instance then this # will be resolved. self._peer_platform = None # An object that allows the checking of currently authenticated # sessions. self._sessions = SessionHandler(Authenticate(self._user_map)) self.webaddress = None self._web_info = None # A flag that tels us that we are in the process of updating already. # This will allow us to not have multiple periodic calls at the same # time which could cause unpredicatable results. self._flag_updating_deviceregistry = False self._setting_store = load_create_store( os.path.join(os.environ['VOLTTRON_HOME'], 'data', 'volttron.central.settings')) self._request_store = load_create_store( os.path.join(os.environ['VOLTTRON_HOME'], 'data', 'volttron.central.requeststore')) # @Core.periodic(60) def _reconnect_to_platforms(self): """ Attempt to reconnect to all the registered platforms. """ _log.info('Reconnecting to platforms') for entry in self._registry.get_platforms(): try: conn_to_instance = None if entry.is_local: _log.debug('connecting to vip address: {}'.format( self._local_address )) conn_to_instance = ConnectedLocalPlatform(self) elif entry.platform_uuid in self._pa_agents.keys(): conn_to_instance = self._pa_agents.get(entry.platform_uuid) try: if conn_to_instance.agent.vip.peerlist.get(timeout=15): pass except gevent.Timeout: del self._pa_agents[entry.platform_uuid] conn_to_instance = None if not conn_to_instance: _log.debug('connecting to vip address: {}'.format( entry.vip_address )) conn_to_instance = ConnectedPlatform( address=entry.vip_address, serverkey=entry.serverkey, publickey=self.core.publickey, secretkey=self.core.secretkey ) # Subscribe to the underlying agent's pubsub bus. _log.debug("subscribing to platforms pubsub bus.") conn_to_instance.agent.vip.pubsub.subscribe( "pubsub", "platforms", self._on_platforms_messsage) self._pa_agents[entry.platform_uuid] = conn_to_instance _log.debug('Configuring platform to be the correct uuid.') conn_to_instance.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, "reconfigure", platform_uuid=entry.platform_uuid).get(timeout=15) except (gevent.Timeout, Unreachable) as e: _log.error("Unreachable platform address: {}" .format(entry.vip_address)) self._pa_agents[entry.platform_uuid] = None @PubSub.subscribe("pubsub", "heartbeat/volttroncentralplatform") def _on_platform_heartbeat(self, peer, sender, bus, topic, headers, message): _log.debug('Got Heartbeat from: {}'.format(topic)) def _handle_pubsub_register(self, message): # register the platform if a local platform otherwise put it # in a to_register store required = ('serverkey', 'publickey', 'address') valid = True for p in required: if not p in message or not message[p]: _log.error('Invalid {} param not specified or invalid') valid = False # Exit loop if not valid. if not valid: _log.warn('Invalid message format for platform registration.') return _log.info('Attempting to register through pubsub address: {}' .format(message['address'])) passed_uuid = None if 'had_platform_uuid' in message: passed_uuid = message['had_platform_uuid'] entry = self._registry.get_platform(passed_uuid) if not entry: _log.error('{} was not found as a previously registered ' 'platform. Address: {}' .format(passed_uuid, message['address'])) return # Verify the platform address serverkey publickey are the # same. _log.debug('Refreshing registration for {}' .format(message['address'])) if self._local_address == message['address']: if passed_uuid is not None: _log.debug('Local platform entry attempting to re-register.') local_entry = self._registry.get_platform(passed_uuid) if passed_uuid in self._pa_agents.keys(): if self._pa_agents[passed_uuid]: self._pa_agents[passed_uuid].disconnect() del self._pa_agents[passed_uuid] else: _log.debug('Local platform entry attempting to register.') local_entry = PlatformRegistry.build_entry( None, None, None, is_local=True, display_name='local') self._registry.register(local_entry) connected = ConnectedLocalPlatform(self) _log.debug('Calling manage on local vcp.') pubkey = connected.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'manage', self._local_address, self.core.publickey, self._web_info.serverkey ) _log.debug('Reconfiguring platform_uuid for local vcp.') # Change the uuid of the agent. connected.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'reconfigure', platform_uuid=local_entry.platform_uuid) self._pa_agents[local_entry.platform_uuid] = connected else: _log.debug('External platform entry attempting to register.') # TODO use the following to store data for registering # platform. # self._request_store[message['address']] = message # self._request_store.sync() connected = ConnectedPlatform( address=message['address'], serverkey=message['serverkey'], publickey=self.core.publickey, secretkey=self.core.secretkey ) _log.debug('Connecting to external vcp.') connected.connect() if not connected.is_connected(): _log.error("Couldn't connect {} address" .format(message['address'])) return _log.debug('Attempting to manage {} from address {}' .format(message['address'], self.core.address)) vcp_pubkey = connected.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'manage', address=self.core.address, vcserverkey=self._web_info.serverkey, vcpublickey=self.core.publickey ).get(timeout=15) # Check that this pubkey is the same as the one passed through # pubsub mechanism. if not vcp_pubkey == message['publickey']: _log.error("Publickey through pubsub doesn't match " "through manage platform call. ") _log.error("Address {} was attempting to register." .format(message['address'])) return if passed_uuid: entry = self._registry.get_platform(passed_uuid) else: entry = PlatformRegistry.build_entry( vip_address=message['address'], serverkey=message['serverkey'], vcp_publickey=message['publickey'], is_local=False, discovery_address=message.get('discovery_address'), display_name=message.get('display_name') ) self._registry.register(entry) self._pa_agents[entry.platform_uuid] = connected @PubSub.subscribe("pubsub", "platforms") def _on_platforms_messsage(self, peer, sender, bus, topic, headers, message): """ This method subscribes to the platforms topic. Platforms that are being managed should publish to this topic with the agent_list and other interesting things that the volttron central shsould want to know. """ topicsplit = topic.split('/') if len(topicsplit) < 2: _log.error('Invalid topic length published to volttron central') return if topicsplit[1] == 'register': self._handle_pubsub_register(message) return if topicsplit[1] == 'unregister': pass platform_uuid = topicsplit[1] if len(platform_uuid) != 36: _log.error('Invalid platform id detected {}' .format(platform_uuid)) return if not self._registry.get_platform(platform_uuid): _log.warn('Platform {} is not registered but sent message {}' .format(platform_uuid, message)) return if len(topicsplit) < 3: _log.warn("Invalid topic length no operation specified.") _log.debug('Doing operation: {}'.format(topicsplit[2])) self._registry.update_agent_list(platform_uuid, message) @PubSub.subscribe("pubsub", "datalogger/platforms") def _on_platform_log_message(self, peer, sender, bus, topic, headers, message): """ Receive message from a registered platform This method is called with stats from the registered platform agents. """ _log.debug('Got topic: {}'.format(topic)) _log.debug('Got message: {}'.format(message)) topicsplit = topic.split('/') platform_uuid = topicsplit[2] # For devices we use everything between devices/../all as a unique # key for determining the last time it was seen. key = '/'.join(topicsplit[:]) _log.debug("key is: {}".format(key)) uuid = topicsplit[2] point_list = [] for point, item in message.iteritems(): point_list.append(point) stats = { 'topic': key, 'points': point_list, 'last_published_utc': format_timestamp(get_aware_utc_now()) } self._registry.update_performance(platform_uuid=platform_uuid, performance=stats) @RPC.export def get_platforms(self): """ Retrieves the platforms that have been registered with VC. @return: """ return self._registry.get_platforms() @RPC.export def get_platform(self, platform_uuid): return self._registry.get_platform(platform_uuid) # @Core.periodic(15) def _auto_register_peer(self): """ Auto register a volttron central platform. This should only happen if there isn't already a peer registered and then only if there hasn't been a local platform registered already. """ pass # if not self._peer_platform: # for p in self._registry.get_platforms(): # if p.is_local: # _log.debug("Reconfiguring local to use: {}".format( # p.platform_uuid)) # self.vip.rpc.call( # VOLTTRON_CENTRAL_PLATFORM, 'reconfigure', # platform_uuid=p.platform_uuid # ) # return # # peers = self.vip.peerlist().get(timeout=30) # if 'platform.agent' in peers: # _log.debug('Auto connecting platform.agent on vc') # # the _peer_platform is set to self because we don't need # # another agent to connect to the bus instead we just use # # this agent. # self._peer_platform = self # local_entry = PlatformRegistry.build_entry( # None, None, None, is_local=True, display_name='local') # # self._registry.register(local_entry) # self._pa_agents[local_entry.platform_uuid] = self # _log.debug("Reconfiguring local to use: {}".format( # local_entry.platform_uuid)) # self.vip.rpc.call( # VOLTTRON_CENTRAL_PLATFORM, 'reconfigure', # platform_uuid=local_entry.platform_uuid # ) def _disconnect_peer_platform(self, sender, **kwargs): _log.debug("disconnecting peer_platform") self._peer_platform = None @RPC.export def list_platform_details(self): _log.debug('list_platform_details {}', self._registry.get_platforms()) return self._registry.get_platforms() @RPC.export def unregister_platform(self, platform_uuid): _log.debug('unregister_platform') platform = self._registry.get_platform(platform_uuid) if platform: self._registry.unregister(platform.vip_address) self._store_registry() if platform_uuid in self._pa_agents.keys(): connected = self._pa_agents[platform_uuid] # Don't stop the local platform because that is this # agent. if not platform.is_local: connected.disconnect() del self._pa_agents[platform_uuid] del connected if platform.is_local: self._peer_platform = None context = 'Unregistered platform {}'.format(platform_uuid) return {'status': 'SUCCESS', 'context': context} else: msg = 'Unable to unregistered platform {}'.format(platform_uuid) return {'error': {'code': UNABLE_TO_UNREGISTER_INSTANCE, 'message': msg}} @RPC.export def register_instance(self, discovery_address): """ Adds discovery_address to proposed list of agents. This method is called from a configured agent to hone into the `VolttronCentralAgent`. An administrator must then choose to accept the call from this agent before the agent will be granted status. :param string: The url of the discovery_address for the platform. """ _log.debug('register_instance called via RPC') self._register_instance(discovery_address, display_name=discovery_address) def _register_instance(self, discovery_address, display_name=None, provisional=False): """ Register an instance with VOLTTRON Central based on jsonrpc. NOTE: This method is meant to be called from the jsonrpc method. The registration of the instance will fail in the following cases: - no discoverable instance at the passed uri - no platform.agent installed at the discoverable instance - is a different volttron central managing the discoverable instance. If the display name is not set then the display name becomes the same as the discovery_address. This will be used in the volttron central ui. :param discovery_address: A ip:port for an instance of volttron discovery. :param display_name: :return: dictionary: The dictionary will hold either an error object or a result object. """ _log.info( 'Attempting to register name: {} with address: {}'.format( display_name, discovery_address)) try: discovery_response = DiscoveryInfo.request_discovery_info( discovery_address) except DiscoveryError as e: return { 'error': { 'code': DISCOVERY_ERROR, 'message': e.message }} pa_instance_serverkey = discovery_response.serverkey pa_vip_address = discovery_response.vip_address assert pa_instance_serverkey _log.debug('connecting to pa_instance') try: connected_to_pa = ConnectedPlatform( address=pa_vip_address, serverkey=pa_instance_serverkey, secretkey=self.core.secretkey, publickey=self.core.publickey ) connected_to_pa.connect() if not connected_to_pa.is_connected(): return { 'error': { 'code': UNABLE_TO_REGISTER_INSTANCE, 'message': 'Could not connect to {}' .format(pa_vip_address) }} except gevent.Timeout: return { 'error': { 'code': UNABLE_TO_REGISTER_INSTANCE, 'message': 'Could not connect to {}' .format(pa_vip_address) }} except Exception as ex: return {'error': {'code': UNHANDLED_EXCEPTION, 'message': ex.message }} _log.debug('Connected to address') peers = connected_to_pa.agent.vip.peerlist().get(timeout=30) if VOLTTRON_CENTRAL_PLATFORM not in peers: connected_to_pa.core.stop() return {'error': {'code': UNABLE_TO_REGISTER_INSTANCE, 'message': '{} not present.'.format( VOLTTRON_CENTRAL_PLATFORM) }} # The call to manage should return a public key for that agent result = connected_to_pa.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'manage', self._web_info.vip_address, self._web_info.serverkey, self.core.publickey).get(timeout=30) # Magic number 43 is the length of a encoded public key. if len(result) != 43: return {'error': {'code': UNABLE_TO_REGISTER_INSTANCE, 'message': 'Invalid publickey returned from {}' .format(VOLTTRON_CENTRAL_PLATFORM) }} # Add the pa's public key so it can connect back to us. auth_file = AuthFile() auth_entry = AuthEntry(credentials="CURVE:{}".format(result), capabilities=['managing'] ) auth_file.add(auth_entry) # TODO: figure out if we are local or not entry = PlatformRegistry.build_entry( pa_vip_address, pa_instance_serverkey, discovery_address, display_name, False) self._registry.register(entry) self._pa_agents[entry.platform_uuid] = connected_to_pa _log.debug("Adding {}".format(entry.platform_uuid)) instance_name = display_name if display_name else discovery_address context = 'Registered instance {}'.format(instance_name) connected_to_pa.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'reconfigure', platform_uuid=entry.platform_uuid).get(timeout=30) return {'status': 'SUCCESS', 'context': context} def _store_registry(self): self._store('registry', self._registry.package()) @Core.receiver('onsetup') def _setup(self, sender, **kwargs): if not os.environ.get('VOLTTRON_HOME', None): raise ValueError('VOLTTRON_HOME environment must be set!') db_path = os.path.join(os.environ.get('VOLTTRON_HOME'), 'data/volttron.central') db_dir = os.path.dirname(db_path) try: os.makedirs(db_dir) except OSError as exc: if exc.errno != errno.EEXIST or not os.path.isdir(db_dir): raise self.persistence_path = db_path # Returns None if there has been no registration of any platforms. registered = self._load('registry') if registered: self._registry.unpackage(registered) def _to_jsonrpc_obj(self, jsonrpcstr): """ Convert data string into a JsonRpcData named tuple. :param object data: Either a string or a dictionary representing a json document. """ return jsonrpc.JsonRpcData.parse(jsonrpcstr) @RPC.export def jsonrpc(self, env, data): """ The main entry point for ^jsonrpc data This method will only accept rpcdata. The first time this method is called, per session, it must be using get_authorization. That will return a session token that must be included in every subsequent request. The session is tied to the ip address of the caller. :param object env: Environment dictionary for the request. :param object data: The JSON-RPC 2.0 method to call. :return object: An JSON-RPC 2.0 response. """ if env['REQUEST_METHOD'].upper() != 'POST': return jsonrpc.json_error('NA', INVALID_REQUEST, 'Invalid request method') try: rpcdata = self._to_jsonrpc_obj(data) _log.info('rpc method: {}'.format(rpcdata.method)) if rpcdata.method == 'get_authorization': args = {'username': rpcdata.params['username'], 'password': rpcdata.params['password'], 'ip': env['REMOTE_ADDR']} sess = self._sessions.authenticate(**args) if not sess: _log.info('Invalid username/password for {}'.format( rpcdata.params['username'])) return jsonrpc.json_error( rpcdata.id, UNAUTHORIZED, "Invalid username/password specified.") _log.info('Session created for {}'.format( rpcdata.params['username'])) return jsonrpc.json_result(rpcdata.id, sess) token = rpcdata.authorization ip = env['REMOTE_ADDR'] _log.debug('REMOTE_ADDR: {}'.format(ip)) session_user = self._sessions.check_session(token, ip) _log.debug('SESSION_USER IS: {}'.format(session_user)) if not session_user: _log.debug("Session Check Failed for Token: {}".format(token)) return jsonrpc.json_error(rpcdata.id, UNAUTHORIZED, "Invalid authentication token") _log.debug('RPC METHOD IS: {}'.format(rpcdata.method)) # Route any other method that isn't result_or_error = self._route_request(session_user, rpcdata.id, rpcdata.method, rpcdata.params) except AssertionError: return jsonrpc.json_error( 'NA', INVALID_REQUEST, 'Invalid rpc data {}'.format(data)) except Exception as e: return jsonrpc.json_error( 'NA', UNHANDLED_EXCEPTION, e ) _log.debug("RETURNING: {}".format(self._get_jsonrpc_response( rpcdata.id, result_or_error))) return self._get_jsonrpc_response(rpcdata.id, result_or_error) def _get_jsonrpc_response(self, id, result_or_error): """ Wrap the response in either a json-rpc error or result. :param id: :param result_or_error: :return: """ if 'error' in result_or_error: error = result_or_error['error'] _log.debug("RPC RESPONSE ERROR: {}".format(error)) return jsonrpc.json_error(id, error['code'], error['message']) return jsonrpc.json_result(id, result_or_error) def _get_agents(self, platform_uuid, groups): """ Retrieve the list of agents on a specific platform. :param platform_uuid: :param groups: :return: """ _log.debug('_get_agents') connected_to_pa = self._pa_agents[platform_uuid] agents = connected_to_pa.agent.vip.rpc.call( 'platform.agent', 'list_agents').get(timeout=30) for a in agents: if 'admin' in groups: if "platformagent" in a['name'] or \ "volttroncentral" in a['name']: a['vc_can_start'] = False a['vc_can_stop'] = False a['vc_can_restart'] = True else: a['vc_can_start'] = True a['vc_can_stop'] = True a['vc_can_restart'] = True else: # Handle the permissions that are not admin. a['vc_can_start'] = False a['vc_can_stop'] = False a['vc_can_restart'] = False _log.debug('Agents returned: {}'.format(agents)) return agents @Core.receiver('onstart') def _starting(self, sender, **kwargs): """ Starting of the platform :param sender: :param kwargs: :return: """ self.vip.heartbeat.start() q = query.Query(self.core) self._external_addresses = q.query('addresses').get(timeout=30) # TODO: Use all addresses for fallback, #114 _log.debug("external addresses are: {}".format( self._external_addresses )) self._local_address = q.query('local_address').get(timeout=30) _log.debug('Local address is? {}'.format(self._local_address)) _log.debug('Registering jsonrpc and /.* routes') self.vip.rpc.call(MASTER_WEB, 'register_agent_route', r'^/jsonrpc.*', self.core.identity, 'jsonrpc').get(timeout=30) self.vip.rpc.call(MASTER_WEB, 'register_path_route', VOLTTRON_CENTRAL, r'^/.*', self._webroot).get(timeout=30) self.webaddress = self.vip.rpc.call( MASTER_WEB, 'get_bind_web_address').get(timeout=30) assert self.core.publickey assert self.core.secretkey assert self.webaddress self._web_info = DiscoveryInfo.request_discovery_info(self.webaddress) # Reconnect to the platforms that are in the registry. self._reconnect_to_platforms() def __load_persist_data(self): persist_kv = None if os.path.exists(self.persistence_path): try: with open(self.persistence_path, 'rb') as file: persist_kv = jsonapi.loads(file.read()) file.close() except Exception as err: _log.error("Couldn't read persistence data {}" .format(err.message)) return persist_kv def _store(self, key, data): persist = self.__load_persist_data() if not persist: persist = {} persist[key] = data with open(self.persistence_path, 'wb') as file: file.write(jsonapi.dumps(persist)) def _load(self, key): persist = self.__load_persist_data() value = None if persist: value = persist.get(key, None) return value def _sync_connected_platforms(self): """ Sync the registry entries with the connections to vcp agents """ _log.debug("len pa_agents {}".format(len(self._pa_agents))) pakeys = set(self._pa_agents.keys()) _log.debug("Syncing with {}".format(pakeys)) for p in self._registry.get_platforms(): if p.platform_uuid in pakeys: pakeys.remove(p.platform_uuid) for k in pakeys: _log.debug('Removing {} from pa_agents'.format(k)) if k in self._pa_agents.keys(): if self._pa_agents[k]: self._pa_agents[k].disconnect() del self._pa_agents[k] @Core.receiver('onstop') def _stopping(self, sender, **kwargs): """ Clean up the agent code before the agent is killed """ for v in self._pa_agents.values(): v.disconnect() self._pa_agents.clear() self.vip.rpc.call(MASTER_WEB, 'unregister_all_agent_routes', self.core.identity).get(timeout=30) @Core.periodic(10) def _update_device_registry(self): """ Updating the device registery from registered platforms. :return: """ try: if not self._flag_updating_deviceregistry: _log.debug("Updating device registry") self._flag_updating_deviceregistry = True self._sync_connected_platforms() unreachable = [] # Loop over the connections to the registered agent platforms. for k, v in self._pa_agents.items(): _log.debug('updating for {}'.format(k)) # Only attempt update if we have a connection to the # agent instance. if v is not None: try: devices = v.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'get_devices').get(timeout=30) anon_devices = defaultdict(dict) # for each device returned from the query to # get_devices we need to anonymize the k1 in the # anon_devices dictionary. for k1, v1 in devices.items(): _log.debug( "before anon: {}, {}".format(k1, v1)) # now we need to do a search/replace on the # self._topic_list so that the devices are # known as the correct itme nin the tree. anon_topic = self._topic_replace_map[k1] # if replaced has not already been replaced if not anon_topic: anon_topic = k1 for sr in self._topic_replace_list: anon_topic = anon_topic.replace( sr['from'], sr['to']) self._topic_replace_map[k1] = anon_topic anon_devices[anon_topic] = v1 _log.debug('Anon devices are: {}'.format( anon_devices)) self._registry.update_devices(k, anon_devices) except (gevent.Timeout, Unreachable) as e: _log.error( 'Error getting devices from platform {}' .format(k)) unreachable.append(k) for k in unreachable: if self._pa_agents[k]: self._pa_agents[k].disconnect() del self._pa_agents[k] finally: self._flag_updating_deviceregistry = False def _handle_list_performance(self): _log.debug('Listing performance topics from vc') return [{'platform.uuid': x.platform_uuid, 'performance': self._registry.get_performance( x.platform_uuid) } for x in self._registry.get_platforms() if self._registry.get_performance(x.platform_uuid)] def _handle_list_devices(self): _log.debug('Listing devices from vc') return [{'platform.uuid': x.platform_uuid, 'devices': self._registry.get_devices(x.platform_uuid)} for x in self._registry.get_platforms() if self._registry.get_devices(x.platform_uuid)] def _handle_list_platforms(self): def get_status(platform_uuid): cn = self._pa_agents.get(platform_uuid) if cn is None: _log.debug('cn is NONE so status is BAD for uuid {}' .format(platform_uuid)) return Status.build(BAD_STATUS, "Platform Unreachable.").as_dict() try: _log.debug('TRYING TO REACH {}'.format(platform_uuid)) health = cn.agent.vip.rpc.call(VOLTTRON_CENTRAL_PLATFORM, 'get_health').get(timeout=30) except Unreachable: health = Status.build(UNKNOWN_STATUS, "Platform Agent Unreachable").as_dict() return health _log.debug( 'Listing platforms: {}'.format(self._registry.get_platforms())) return [{'uuid': x.platform_uuid, 'name': x.display_name, 'health': get_status(x.platform_uuid)} for x in self._registry.get_platforms()] def _route_request(self, session_user, id, method, params): '''Route request to either a registered platform or handle here.''' _log.debug( 'inside _route_request {}, {}, {}'.format(id, method, params)) def err(message, code=METHOD_NOT_FOUND): return {'error': {'code': code, 'message': message}} if method == 'register_instance': if isinstance(params, list): return self._register_instance(*params) else: return self._register_instance(**params) elif method == 'list_deivces': return self._handle_list_devices() elif method == 'list_performance': return self._handle_list_performance() elif method == 'list_platforms': return self._handle_list_platforms() elif method == 'unregister_platform': return self.unregister_platform(params['platform_uuid']) elif method == 'get_setting': if 'key' not in params or not params['key']: return err('Invalid parameter key not set', INVALID_PARAMS) value = self._setting_store.get(params['key'], None) if value is None: return err('Invalid key specified', INVALID_PARAMS) return value elif method == 'get_setting_keys': return self._setting_store.keys() elif method == 'set_setting': if 'key' not in params or not params['key']: return err('Invalid parameter key not set', INVALID_PARAMS) _log.debug('VALUE: {}'.format(params)) if 'value' not in params: return err('Invalid parameter value not set', INVALID_PARAMS) # if passing None value then remove the value from the keystore # don't raise an error if the key isn't present in the store. if params['value'] is None: if params['key'] in self._setting_store: del self._setting_store[params['key']] else: self._setting_store[params['key']] = params['value'] self._setting_store.sync() return 'SUCCESS' elif 'historian' in method: has_platform_historian = PLATFORM_HISTORIAN in \ self.vip.peerlist().get(timeout=30) if not has_platform_historian: return err('Platform historian not found on volttorn central', UNAVAILABLE_AGENT) _log.debug('Trapping platform.historian to vc.') _log.debug('has_platform_historian: {}'.format( has_platform_historian)) if 'historian.query' in method: return self.vip.rpc.call( PLATFORM_HISTORIAN, 'query', **params).get(timeout=30) elif 'historian.get_topic_list' in method: return self.vip.rpc.call( PLATFORM_HISTORIAN, 'get_topic_list').get(timeout=30) fields = method.split('.') if len(fields) < 3: return err('Unknown method {}'.format(method)) platform_uuid = fields[2] platform = self._registry.get_platform(platform_uuid) if not platform: return err('Unknown platform {}'.format(platform_uuid)) platform_method = '.'.join(fields[3:]) _log.debug(platform_uuid) # Get a connection object associated with the platform uuid. cn = self._pa_agents.get(platform_uuid) if not cn: return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "Cannot connect to platform." ) _log.debug('Routing to {}'.format(VOLTTRON_CENTRAL_PLATFORM)) if platform_method == 'install': if 'admin' not in session_user['groups']: return jsonrpc.json_error( id, UNAUTHORIZED, "Admin access is required to install agents") if platform_method == 'list_agents': _log.debug('Callling list_agents') agents = self._registry.get_agent_list(platform_uuid) if agents is None: _log.warn('No agents found for platform_uuid {}'.format( platform_uuid )) agents = [] for a in agents: if 'admin' not in session_user['groups']: a['permissions'] = { 'can_stop': False, 'can_start': False, 'can_restart': False, 'can_remove': False } else: _log.debug('Permissionse for {} are {}' .format(a['name'], a['permissions'])) return agents else: try: return cn.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'route_request', id, platform_method, params).get(timeout=30) except (Unreachable, gevent.Timeout) as e: del self._pa_agents[platform_uuid] return err("Can't route to platform", UNAVAILABLE_PLATFORM)
def configure_main(self, config_name, action, contents): """ The main configuration for volttron central. This is where validation will occur. Note this method is called: 1. When the agent first starts (with the params from packaged agent file) 2. When 'store' is called through the volttron-ctl config command line with 'config' as the name. Required Configuration: The volttron central requires a user mapping. :param config_name: :param action: :param contents: """ _log.debug('Main config updated') _log.debug('ACTION IS {}'.format(action)) _log.debug('CONTENT IS {}'.format(contents)) if action == 'DELETE': # Remove the registry and keep the service running. self.runtime_config = None # Now stop the exposition of service. else: self.runtime_config = self.default_config.copy() self.runtime_config.update(contents) problems = self._validate_config_params(self.runtime_config) if len(problems) > 0: _log.error( "The following configuration problems were detected!") for p in problems: _log.error(p) sys.exit(INVALID_CONFIGURATION_CODE) else: _log.info('volttron central webroot is: {}'.format( self.runtime_config.get('webroot') )) users = self.runtime_config.get('users') self.web_sessions = SessionHandler(Authenticate(users)) _log.debug('Querying router for addresses and serverkey.') q = Query(self.core) external_addresses = q.query('addresses').get(timeout=5) self.runtime_config['local_external_address'] = external_addresses[0] self.vip.web.register_websocket(r'/vc/ws', self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) self.vip.web.register_endpoint(r'/jsonrpc', self.jsonrpc) self.vip.web.register_path(r'^/.*', self.runtime_config.get('webroot')) # Start scanning for new platforms connections as well as for # disconnects that happen. self._scan_for_platforms()
class VolttronCentralAgent(Agent): """ Agent for managing many volttron instances from a central web ui. During the """ def __init__(self, config_path, **kwargs): """ Creates a `VolttronCentralAgent` object to manage instances. Each instances that is registered must contain a running `VolttronCentralPlatform`. Through this conduit the `VolttronCentralAgent` is able to communicate securly and efficiently. :param config_path: :param kwargs: :return: """ _log.info("{} constructing...".format(self.__class__.__name__)) super(VolttronCentralAgent, self).__init__(enable_web=True, **kwargs) # Load the configuration into a dictionary config = utils.load_config(config_path) # Required users users = config.get('users', None) # Expose the webroot property to be customized through the config # file. webroot = config.get('webroot', DEFAULT_WEB_ROOT) if webroot.endswith('/'): webroot = webroot[:-1] topic_replace_list = config.get('topic-replace-list', []) # Create default configuration to be used in case of problems in the # packaged agent configuration file. self.default_config = dict( webroot=os.path.abspath(webroot), users=users, topic_replace_list=topic_replace_list ) # During the configuration update/new/delete action this will be # updated to the current configuration. self.runtime_config = None # Start using config store. self.vip.config.set_default("config", config) self.vip.config.subscribe(self.configure_main, actions=['NEW', 'UPDATE', 'DELETE'], pattern="config") # Use config store to update the settings of a platform's configuration. self.vip.config.subscribe(self.configure_platforms, actions=['NEW', 'UPDATE', 'DELETE'], pattern="platforms/*") # mapping from the real topic into the replacement. self.replaced_topic_map = {} # mapping from md5 hash of address to the actual connection to the # remote instance. self.vcp_connections = {} # Current sessions available to the self.web_sessions = None # Platform health based upon device driver publishes self.device_health = defaultdict(dict) # Used to hold scheduled reconnection event for vcp agents. self._vcp_reconnect_event = None # the registered socket endpoints so we can send out management # events to all the registered session. self._websocket_endpoints = set() self._platforms = Platforms(self) self._platform_scan_event = None self._connected_platforms = dict() @staticmethod def _get_next_time_seconds(seconds=10): now = get_aware_utc_now() next_time = now + datetime.timedelta(seconds=seconds) return next_time def _handle_platform_connection(self, platform_vip_identity): _log.info("Handling new platform connection {}".format( platform_vip_identity)) platform = self._platforms.add_platform(platform_vip_identity) def _handle_platform_disconnect(self, platform_vip_identity): _log.warn("Handling disconnection of connection from identity: {}".format( platform_vip_identity )) # TODO send alert that there was a platform disconnect. self._platforms.disconnect_platform(platform_vip_identity) def _scan_for_platforms(self): """ Scan the local bus for peers that start with 'vcp-'. Handle the connection and disconnection events here. """ if self._platform_scan_event is not None: # This won't hurt anything if we are canceling ourselves. self._platform_scan_event.cancel() # Identities of all platform agents that are connecting to us should # have an identity of platform.md5hash. connected_platforms = set([x for x in self.vip.peerlist().get(timeout=5) if x.startswith('vcp-')]) disconnected = self._platforms.get_platform_keys() - connected_platforms for vip_id in disconnected: self._handle_platform_disconnect(vip_id) not_known = connected_platforms - self._platforms.get_platform_keys() for vip_id in not_known: self._handle_platform_connection(vip_id) next_platform_scan = VolttronCentralAgent._get_next_time_seconds() # reschedule the next scan. self._platform_scan_event = self.core.schedule( next_platform_scan, self._scan_for_platforms) def configure_main(self, config_name, action, contents): """ The main configuration for volttron central. This is where validation will occur. Note this method is called: 1. When the agent first starts (with the params from packaged agent file) 2. When 'store' is called through the volttron-ctl config command line with 'config' as the name. Required Configuration: The volttron central requires a user mapping. :param config_name: :param action: :param contents: """ _log.debug('Main config updated') _log.debug('ACTION IS {}'.format(action)) _log.debug('CONTENT IS {}'.format(contents)) if action == 'DELETE': # Remove the registry and keep the service running. self.runtime_config = None # Now stop the exposition of service. else: self.runtime_config = self.default_config.copy() self.runtime_config.update(contents) problems = self._validate_config_params(self.runtime_config) if len(problems) > 0: _log.error( "The following configuration problems were detected!") for p in problems: _log.error(p) sys.exit(INVALID_CONFIGURATION_CODE) else: _log.info('volttron central webroot is: {}'.format( self.runtime_config.get('webroot') )) users = self.runtime_config.get('users') self.web_sessions = SessionHandler(Authenticate(users)) _log.debug('Querying router for addresses and serverkey.') q = Query(self.core) external_addresses = q.query('addresses').get(timeout=5) self.runtime_config['local_external_address'] = external_addresses[0] self.vip.web.register_websocket(r'/vc/ws', self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) self.vip.web.register_endpoint(r'/jsonrpc', self.jsonrpc) self.vip.web.register_path(r'^/.*', self.runtime_config.get('webroot')) # Start scanning for new platforms connections as well as for # disconnects that happen. self._scan_for_platforms() def configure_platforms(self, config_name, action, contents): _log.debug('Platform configuration updated.') _log.debug('ACTION IS {}'.format(action)) _log.debug('CONTENT IS {}'.format(contents)) def open_authenticate_ws_endpoint(self, fromip, endpoint): """ Callback method from when websockets are opened. The endpoine must be '/' delimited with the second to last section being the session of a logged in user to volttron central itself. :param fromip: :param endpoint: A string representing the endpoint of the websocket. :return: """ _log.debug("OPENED ip: {} endpoint: {}".format(fromip, endpoint)) try: session = endpoint.split('/')[-2] except IndexError: _log.error("Malformed endpoint. Must be delimited by '/'") _log.error( 'Endpoint must have valid session in second to last position') return False if not self.web_sessions.check_session(session, fromip): _log.error("Authentication error for session!") return False _log.debug('Websocket allowed.') self._websocket_endpoints.add(endpoint) return True def _ws_closed(self, endpoint): _log.debug("CLOSED endpoint: {}".format(endpoint)) try: self._websocket_endpoints.remove(endpoint) except KeyError: pass # This should never happen but protect against it anyways. def _ws_received(self, endpoint, message): _log.debug("RECEIVED endpoint: {} message: {}".format(endpoint, message)) @RPC.export def is_registered(self, address_hash=None, address=None): if address_hash is None and address is None: return False if address_hash is None: address_hash = PlatformHandler.address_hasher(address) return self._platforms.is_registered(address_hash) @RPC.export def get_publickey(self): """ RPC method allowing the caller to retrieve the publickey of this agent. This method is available for allowing :class:`VolttronCentralPlatform` agents to allow this agent to be able to connect to its instance. :return: The publickey of this volttron central agent. :rtype: str """ return self.core.publickey @RPC.export def unregister_platform(self, platform_uuid): _log.debug('unregister_platform') platform = self._registered_platforms.get(platform_uuid) if platform: connected = self._platform_connections.get(platform_uuid) if connected is not None: connected.call('unmanage') connected.kill() address = None for v in self._address_to_uuid.values(): if v == platform_uuid: address = v break if address: del self._address_to_uuid[address] del self._platform_connections[platform_uuid] del self._registered_platforms[platform_uuid] self._registered_platforms.sync() context = 'Unregistered platform {}'.format(platform_uuid) return {'status': 'SUCCESS', 'context': context} else: msg = 'Unable to unregistered platform {}'.format(platform_uuid) return {'error': {'code': UNABLE_TO_UNREGISTER_INSTANCE, 'message': msg}} def _to_jsonrpc_obj(self, jsonrpcstr): """ Convert data string into a JsonRpcData named tuple. :param object data: Either a string or a dictionary representing a json document. """ return jsonrpc.JsonRpcData.parse(jsonrpcstr) def jsonrpc(self, env, data): """ The main entry point for ^jsonrpc data This method will only accept rpcdata. The first time this method is called, per session, it must be using get_authorization. That will return a session token that must be included in every subsequent request. The session is tied to the ip address of the caller. :param object env: Environment dictionary for the request. :param object data: The JSON-RPC 2.0 method to call. :return object: An JSON-RPC 2.0 response. """ if env['REQUEST_METHOD'].upper() != 'POST': return jsonrpc.json_error('NA', INVALID_REQUEST, 'Invalid request method, only POST allowed' ) try: rpcdata = self._to_jsonrpc_obj(data) _log.info('rpc method: {}'.format(rpcdata.method)) if rpcdata.method == 'get_authorization': args = {'username': rpcdata.params['username'], 'password': rpcdata.params['password'], 'ip': env['REMOTE_ADDR']} sess = self.web_sessions.authenticate(**args) if not sess: _log.info('Invalid username/password for {}'.format( rpcdata.params['username'])) return jsonrpc.json_error( rpcdata.id, UNAUTHORIZED, "Invalid username/password specified.") _log.info('Session created for {}'.format( rpcdata.params['username'])) self.vip.web.register_websocket( "/vc/ws/{}/management".format(sess), self.open_authenticate_ws_endpoint, self._ws_closed, self._received_data) _log.info('Session created for {}'.format( rpcdata.params['username'])) gevent.sleep(1) return jsonrpc.json_result(rpcdata.id, sess) token = rpcdata.authorization ip = env['REMOTE_ADDR'] _log.debug('REMOTE_ADDR: {}'.format(ip)) session_user = self.web_sessions.check_session(token, ip) _log.debug('SESSION_USER IS: {}'.format(session_user)) if not session_user: _log.debug("Session Check Failed for Token: {}".format(token)) return jsonrpc.json_error(rpcdata.id, UNAUTHORIZED, "Invalid authentication token") _log.debug('RPC METHOD IS: {}'.format(rpcdata.method)) # Route any other method that isn't result_or_error = self._route_request(session_user, rpcdata.id, rpcdata.method, rpcdata.params) except AssertionError: return jsonrpc.json_error( 'NA', INVALID_REQUEST, 'Invalid rpc data {}'.format(data)) except Unreachable: return jsonrpc.json_error( rpcdata.id, UNAVAILABLE_PLATFORM, "Couldn't reach platform with method {} params: {}".format( rpcdata.method, rpcdata.params)) except Exception as e: return jsonrpc.json_error( 'NA', UNHANDLED_EXCEPTION, e ) return self._get_jsonrpc_response(rpcdata.id, result_or_error) def _get_jsonrpc_response(self, id, result_or_error): """ Wrap the response in either a json-rpc error or result. :param id: :param result_or_error: :return: """ if isinstance(result_or_error, dict): if 'jsonrpc' in result_or_error: return result_or_error if result_or_error is not None and isinstance(result_or_error, dict): if 'error' in result_or_error: error = result_or_error['error'] _log.debug("RPC RESPONSE ERROR: {}".format(error)) return jsonrpc.json_error(id, error['code'], error['message']) return jsonrpc.json_result(id, result_or_error) def _get_agents(self, instance_uuid, groups): """ Retrieve the list of agents on a specific platform. :param instance_uuid: :param groups: :return: """ _log.debug('_get_agents') connected_to_pa = self._platform_connections[instance_uuid] agents = connected_to_pa.agent.vip.rpc.call( 'platform.agent', 'list_agents').get(timeout=30) for a in agents: if 'admin' in groups: if "platformagent" in a['name'] or \ "volttroncentral" in a['name']: a['vc_can_start'] = False a['vc_can_stop'] = False a['vc_can_restart'] = True else: a['vc_can_start'] = True a['vc_can_stop'] = True a['vc_can_restart'] = True else: # Handle the permissions that are not admin. a['vc_can_start'] = False a['vc_can_stop'] = False a['vc_can_restart'] = False _log.debug('Agents returned: {}'.format(agents)) return agents def _setupexternal(self): _log.debug(self.vip.ping('', "PING ROUTER?").get(timeout=3)) def _configure_agent(self, endpoint, message): _log.debug('Configure agent: {} message: {}'.format(endpoint, message)) def _received_data(self, endpoint, message): print('Received from endpoint {} message: {}'.format(endpoint, message)) self.vip.web.send(endpoint, message) def set_setting(self, session_user, params): """ Sets or removes a setting from the config store. If the value is None then the item will be removed from the store. If there is an error in saving the value then a jsonrpc.json_error object is returned. :param session_user: Unused :param params: Dictionary that must contain 'key' and 'value' keys. :return: A 'SUCCESS' string or a jsonrpc.json_error object. """ if 'key' not in params or not params['key']: return jsonrpc.json_error(params['message_id'], INVALID_PARAMS, 'Invalid parameter key not set') if 'value' not in params: return jsonrpc.json_error(params['message_id'], INVALID_PARAMS, 'Invalid parameter key not set') config_key = "settings/{}".format(params['key']) value = params['value'] if value is None: try: self.vip.config.delete(config_key) except KeyError: pass else: # We handle empt string here because the config store doesn't allow # empty strings to be set as a config store. I wasn't able to # trap the ValueError that is raised on the server side. if value == "": return jsonrpc.json_error(params['message_id'], INVALID_PARAMS, 'Invalid value set (empty string?)') self.vip.config.set(config_key, value) return 'SUCCESS' def get_setting(self, session_user, params): """ Retrieve a value from the passed setting key. The params object must contain a "key" to return from the settings store. :param session_user: Unused :param params: Dictionary that must contain a 'key' key. :return: The value or a jsonrpc error object. """ config_key = "settings/{}".format(params['key']) try: value = self.vip.config.get(config_key) except KeyError: return jsonrpc.json_error(params['message_id'], INVALID_PARAMS, 'Invalid key specified') else: return value def get_setting_keys(self, session_user, params): """ Returns a list of all of the settings keys so the caller can know what settings to request. :param session_user: Unused :param params: Unused :return: A list of settings available to the caller. """ prefix = "settings/" keys = [x[len(prefix):] for x in self.vip.config.list() if x.startswith(prefix)] return keys or [] def _handle_bacnet_props(self, session_user, params): platform_uuid = params.pop('platform_uuid') id = params.pop('message_id') _log.debug('Handling bacnet_props platform: {}'.format(platform_uuid)) configure_topic = "{}/configure".format(session_user['token']) ws_socket_topic = "/vc/ws/{}".format(configure_topic) if configure_topic not in self._websocket_endpoints: self.vip.web.register_websocket(ws_socket_topic, self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) def start_sending_props(): response_topic = "configure/{}".format(session_user['token']) # Two ways we could have handled this is to pop the identity off # of the params and then passed both the identity and the response # topic. Or what I chose to do and to put the argument in a # copy of the params. cp = params.copy() cp['publish_topic'] = response_topic cp['device_id'] = int(cp['device_id']) platform = self._platforms.get_platform(platform_uuid) _log.debug('PARAMS: {}'.format(cp)) platform.call("publish_bacnet_props", **cp) gevent.spawn_later(2, start_sending_props) def _handle_bacnet_scan(self, session_user, params): platform_uuid = params.pop('platform_uuid') id = params.pop('message_id') _log.debug('Handling bacnet_scan platform: {}'.format(platform_uuid)) if not self._platforms.is_registered(platform_uuid): return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "Couldn't connect to platform {}".format( platform_uuid )) scan_length = params.pop('scan_length', 5) try: scan_length = float(scan_length) params['scan_length'] = scan_length platform = self._platforms.get_platform(platform_uuid) iam_topic = "{}/iam".format(session_user['token']) ws_socket_topic = "/vc/ws/{}".format(iam_topic) self.vip.web.register_websocket(ws_socket_topic, self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) def start_scan(): # We want the datatype (iam) to be second in the response so # we need to reposition the iam and the session id to the topic # that is passed to the rpc function on vcp iam_session_topic = "iam/{}".format(session_user['token']) platform.call("start_bacnet_scan", iam_session_topic, **params) def close_socket(): _log.debug('Closing bacnet scan for {}'.format( platform_uuid)) gevent.spawn_later(2, self.vip.web.unregister_websocket, iam_session_topic) gevent.spawn_later(scan_length, close_socket) # By starting the scan a second later we allow the websocket # client to subscribe to the newly available endpoint. gevent.spawn_later(2, start_scan) except ValueError: return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "Couldn't connect to platform {}".format( platform_uuid )) except KeyError: return jsonrpc.json_error(id, UNAUTHORIZED, "Invalid user session token") def _enable_setup_mode(self, session_user, params): id = params.pop('message_id') if 'admin' not in session_user['groups']: _log.debug('Returning json_error enable_setup_mode') return jsonrpc.json_error( id, UNAUTHORIZED, "Admin access is required to enable setup mode") auth_file = AuthFile() entries = auth_file.find_by_credentials(".*") if len(entries) > 0: return "SUCCESS" entry = AuthEntry(credentials="/.*/", comments="Un-Authenticated connections allowed here", user_id="unknown") auth_file.add(entry) return "SUCCESS" def _disable_setup_mode(self, session_user, params): id = params.pop('message_id') if 'admin' not in session_user['groups']: _log.debug('Returning json_error disable_setup_mode') return jsonrpc.json_error( id, UNAUTHORIZED, "Admin access is required to disable setup mode") auth_file = AuthFile() auth_file.remove_by_credentials("/.*/") return "SUCCESS" def _handle_management_endpoint(self, session_user, params): ws_topic = "/vc/ws/{}/management".format(session_user.get('token')) self.vip.web.register_websocket(ws_topic, self.open_authenticate_ws_endpoint, self._ws_closed, self._ws_received) return ws_topic def send_management_message(self, type, data={}): """ Send a message to any socket that has connected to the management socket. The payload sent to the client is like the following:: { "type": "UPDATE_DEVICE_STATUS", "data": "this is data that was passed" } :param type: A string defining a unique type for sending to the websockets. :param data: An object that str can be called on. :type type: str :type data: serializable """ management_sockets = [s for s in self._websocket_endpoints if s.endswith("management")] # Nothing to send if we don't have any management sockets open. if len(management_sockets) <= 0: return if data is None: data = {} payload = dict( type=type, data=str(data) ) payload = jsonapi.dumps(payload) for s in management_sockets: self.vip.web.send(s, payload) def _route_request(self, session_user, id, method, params): """ Handle the methods volttron central can or pass off to platforms. :param session_user: The authenticated user's session info. :param id: JSON-RPC id field. :param method: :param params: :return: """ _log.debug( 'inside _route_request {}, {}, {}'.format(id, method, params)) def err(message, code=METHOD_NOT_FOUND): return {'error': {'code': code, 'message': message}} self.send_management_message(method) method_split = method.split('.') # The last part of the jsonrpc method is the actual method to be called. method_check = method_split[-1] # These functions will be sent to a platform.agent on either this # instance or another. All of these functions have the same interface # and can be collected into a dictionary rather than an if tree. platform_methods = dict( # bacnet related start_bacnet_scan=self._handle_bacnet_scan, publish_bacnet_props=self._handle_bacnet_props, # config store related store_agent_config="store_agent_config", get_agent_config="get_agent_config", list_agent_configs="get_agent_config_list", # management related list_agents="get_agent_list", get_devices="get_devices", status_agents="status_agents" ) # These methods are specifically to be handled by the platform not any # agents on the platform that is why we have the length requirement. # # The jsonrpc method looks like the following # # platform.uuid.<dynamic entry>.method_on_vcp if method_check in platform_methods: platform_uuid = None if isinstance(params, dict): platform_uuid = params.pop('platform_uuid', None) if platform_uuid is None: if method_split[0] == 'platforms' and method_split[1] == 'uuid': platform_uuid = method_split[2] if not platform_uuid: return err("Invalid platform_uuid specified as parameter" .format(platform_uuid), INVALID_PARAMS) if not self._platforms.is_registered(platform_uuid): return err("Unknown or unavailable platform {} specified as " "parameter".format(platform_uuid), UNAVAILABLE_PLATFORM) try: _log.debug('Calling {} on platform {}'.format( method_check, platform_uuid )) class_method = platform_methods[method_check] platform = self._platforms.get_platform(platform_uuid) # Determine whether the method to call is on the current class # or on the platform object. if isinstance(class_method, basestring): method_ref = getattr(platform, class_method) else: method_ref = class_method # Put the platform_uuid in the params so it can be used # inside the method params['platform_uuid'] = platform_uuid except AttributeError or KeyError: return jsonrpc.json_error(id, INTERNAL_ERROR, "Attempted calling function " "{} was unavailable".format( class_method )) except ValueError: return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "Couldn't connect to platform " "{}".format(platform_uuid)) else: # pass the id through the message_id parameter. if not params: params = dict(message_id=id) else: params['message_id'] = id # Methods will all have the signature # method(session, params) # return method_ref(session_user, params) vc_methods = dict( register_management_endpoint=self._handle_management_endpoint, list_platforms=self._platforms.get_platform_list, list_performance=self._platforms.get_performance_list, # Settings set_setting=self.set_setting, get_setting=self.get_setting, get_setting_keys=self.get_setting_keys, # Setup mode enable_setup_mode=self._enable_setup_mode, disable_setup_mode=self._disable_setup_mode ) if method in vc_methods: if not params: params = dict(message_id=id) else: params['message_id'] = id response = vc_methods[method](session_user, params) _log.debug("Response is {}".format(response)) return response # vc_methods[method](session_user, params) if method == 'register_instance': if isinstance(params, list): return self._register_instance(*params) else: return self._register_instance(**params) elif method == 'unregister_platform': return self.unregister_platform(params['instance_uuid']) elif 'historian' in method: has_platform_historian = PLATFORM_HISTORIAN in \ self.vip.peerlist().get(timeout=30) if not has_platform_historian: return err( 'The VOLTTRON Central platform historian is unavailable.', UNAVAILABLE_AGENT) _log.debug('Trapping platform.historian to vc.') _log.debug('has_platform_historian: {}'.format( has_platform_historian)) if 'historian.query' in method: return self.vip.rpc.call( PLATFORM_HISTORIAN, 'query', **params).get(timeout=30) elif 'historian.get_topic_list' in method: return self.vip.rpc.call( PLATFORM_HISTORIAN, 'get_topic_list').get(timeout=30) # This isn't known as a proper method on vc or a platform. if len(method_split) < 3: return err('Unknown method {}'.format(method)) if method_split[0] != 'platforms' or method_split[1] != 'uuid': return err('Invalid format for instance must start with ' 'platforms.uuid') instance_uuid = method_split[2] _log.debug('Instance uuid is: {}'.format(instance_uuid)) if not self._platforms.is_registered(instance_uuid): return err('Unknown platform {}'.format(instance_uuid)) platform_method = '.'.join(method_split[3:]) _log.debug("Platform method is: {}".format(platform_method)) platform = self._platforms.get_platform(instance_uuid) if not platform: return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "cannot connect to platform." ) if platform_method.startswith('install'): if 'admin' not in session_user['groups']: return jsonrpc.json_error( id, UNAUTHORIZED, "Admin access is required to install agents") return platform.route_to_agent_method(id, platform_method, params) def _validate_config_params(self, config): """ Validate the configuration parameters of the default/updated parameters. This method will return a list of "problems" with the configuration. If there are no problems then an empty list is returned. :param config: Configuration parameters for the volttron central agent. :type config: dict :return: The problems if any, [] if no problems :rtype: list """ problems = [] webroot = config.get('webroot') if not webroot: problems.append('Invalid webroot in configuration.') elif not os.path.exists(webroot): problems.append( 'Webroot {} does not exist on machine'.format(webroot)) users = config.get('users') if not users: problems.append('A users node must be specified!') else: has_admin = False try: for user, item in users.items(): if 'password' not in item.keys(): problems.append('user {} must have a password!'.format( user)) elif not item['password']: problems.append('password for {} is blank!'.format( user )) if 'groups' not in item.keys(): problems.append('missing groups key for user {}'.format( user )) elif not isinstance(item['groups'], list): problems.append('groups must be a list of strings.') elif not item['groups']: problems.append( 'user {} must belong to at least one group.'.format( user)) # See if there is an adminstator present. if not has_admin and isinstance(item['groups'], list): has_admin = 'admin' in item['groups'] except AttributeError: problems.append('invalid user node.') if not has_admin: problems.append("One user must be in the admin group.") return problems
class VolttronCentralAgent(Agent): """ Agent for managing many volttron instances from a central web ui. During the """ __name__ = 'VolttronCentralAgent' def __init__(self, config_path, **kwargs): """ Creates a `VolttronCentralAgent` object to manage instances. Each instances that is registered must contain a running `VolttronCentralPlatform`. Through this conduit the `VolttronCentralAgent` is able to communicate securly and efficiently. :param config_path: :param kwargs: :return: """ _log.info("{} constructing...".format(self.__name__)) # This is a special object so only use it's identity. identity = kwargs.pop("identity", None) identity = VOLTTRON_CENTRAL super(VolttronCentralAgent, self).__init__(identity=identity, **kwargs) # Load the configuration into a dictionary self._config = utils.load_config(config_path) # Expose the webroot property to be customized through the config # file. self._webroot = self._config.get('webroot', DEFAULT_WEB_ROOT) if self._webroot.endswith('/'): self._webroot = self._webroot[:-1] _log.debug('The webroot is {}'.format(self._webroot)) # Required users self._user_map = self._config.get('users', None) _log.debug("User map is: {}".format(self._user_map)) if self._user_map is None: raise ValueError('users not specified within the config file.') # Search and replace for topics # The difference between the list and the map is that the list # specifies the search and replaces that should be done on all of the # incoming topics. Once all of the search and replaces are done then # the mapping from the original to the final is stored in the map. self._topic_replace_list = self._config.get('topic_replace_list', []) self._topic_replace_map = defaultdict(str) _log.debug('Topic replace list: {}'.format(self._topic_replace_list)) # A resource directory that contains everything that can be looked up. self._resources = ResourceDirectory() self._registry = self._resources.platform_registry # This has a dictionary mapping the platform_uuid to an agent # connected to the vip-address of the registered platform. If the # registered platform is None then that means we were unable to # connect to the platform the last time it was tried. self._pa_agents = {} # if there is a volttron central agent on this instance then this # will be resolved. self._peer_platform = None # An object that allows the checking of currently authenticated # sessions. self._sessions = SessionHandler(Authenticate(self._user_map)) self.webaddress = None self._web_info = None # A flag that tels us that we are in the process of updating already. # This will allow us to not have multiple periodic calls at the same # time which could cause unpredicatable results. self._flag_updating_deviceregistry = False self._setting_store = load_create_store( os.path.join(os.environ['VOLTTRON_HOME'], 'data', 'volttron.central.settings')) self._request_store = load_create_store( os.path.join(os.environ['VOLTTRON_HOME'], 'data', 'volttron.central.requeststore')) # @Core.periodic(60) def _reconnect_to_platforms(self): """ Attempt to reconnect to all the registered platforms. """ _log.info('Reconnecting to platforms') for entry in self._registry.get_platforms(): try: conn_to_instance = None if entry.is_local: _log.debug('connecting to vip address: {}'.format( self._local_address)) conn_to_instance = ConnectedLocalPlatform(self) elif entry.platform_uuid in self._pa_agents.keys(): conn_to_instance = self._pa_agents.get(entry.platform_uuid) try: if conn_to_instance.agent.vip.peerlist.get(timeout=15): pass except gevent.Timeout: del self._pa_agents[entry.platform_uuid] conn_to_instance = None if not conn_to_instance: _log.debug('connecting to vip address: {}'.format( entry.vip_address)) conn_to_instance = ConnectedPlatform( address=entry.vip_address, serverkey=entry.serverkey, publickey=self.core.publickey, secretkey=self.core.secretkey) # Subscribe to the underlying agent's pubsub bus. _log.debug("subscribing to platforms pubsub bus.") conn_to_instance.agent.vip.pubsub.subscribe( "pubsub", "platforms", self._on_platforms_messsage) self._pa_agents[entry.platform_uuid] = conn_to_instance _log.debug('Configuring platform to be the correct uuid.') conn_to_instance.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, "reconfigure", platform_uuid=entry.platform_uuid).get(timeout=15) except (gevent.Timeout, Unreachable) as e: _log.error("Unreachable platform address: {}".format( entry.vip_address)) self._pa_agents[entry.platform_uuid] = None @PubSub.subscribe("pubsub", "heartbeat/volttroncentralplatform") def _on_platform_heartbeat(self, peer, sender, bus, topic, headers, message): _log.debug('Got Heartbeat from: {}'.format(topic)) def _handle_pubsub_register(self, message): # register the platform if a local platform otherwise put it # in a to_register store required = ('serverkey', 'publickey', 'address') valid = True for p in required: if not p in message or not message[p]: _log.error('Invalid {} param not specified or invalid') valid = False # Exit loop if not valid. if not valid: _log.warn('Invalid message format for platform registration.') return _log.info('Attempting to register through pubsub address: {}'.format( message['address'])) passed_uuid = None if 'had_platform_uuid' in message: passed_uuid = message['had_platform_uuid'] entry = self._registry.get_platform(passed_uuid) if not entry: _log.error('{} was not found as a previously registered ' 'platform. Address: {}'.format( passed_uuid, message['address'])) return # Verify the platform address serverkey publickey are the # same. _log.debug('Refreshing registration for {}'.format( message['address'])) if self._local_address == message['address']: if passed_uuid is not None: _log.debug('Local platform entry attempting to re-register.') local_entry = self._registry.get_platform(passed_uuid) if passed_uuid in self._pa_agents.keys(): if self._pa_agents[passed_uuid]: self._pa_agents[passed_uuid].disconnect() del self._pa_agents[passed_uuid] else: _log.debug('Local platform entry attempting to register.') local_entry = PlatformRegistry.build_entry( None, None, None, is_local=True, display_name='local') self._registry.register(local_entry) connected = ConnectedLocalPlatform(self) _log.debug('Calling manage on local vcp.') pubkey = connected.agent.vip.rpc.call(VOLTTRON_CENTRAL_PLATFORM, 'manage', self._local_address, self.core.publickey, self._web_info.serverkey) _log.debug('Reconfiguring platform_uuid for local vcp.') # Change the uuid of the agent. connected.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'reconfigure', platform_uuid=local_entry.platform_uuid) self._pa_agents[local_entry.platform_uuid] = connected else: _log.debug('External platform entry attempting to register.') # TODO use the following to store data for registering # platform. # self._request_store[message['address']] = message # self._request_store.sync() connected = ConnectedPlatform(address=message['address'], serverkey=message['serverkey'], publickey=self.core.publickey, secretkey=self.core.secretkey) _log.debug('Connecting to external vcp.') connected.connect() if not connected.is_connected(): _log.error("Couldn't connect {} address".format( message['address'])) return _log.debug('Attempting to manage {} from address {}'.format( message['address'], self.core.address)) vcp_pubkey = connected.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'manage', address=self.core.address, vcserverkey=self._web_info.serverkey, vcpublickey=self.core.publickey).get(timeout=15) # Check that this pubkey is the same as the one passed through # pubsub mechanism. if not vcp_pubkey == message['publickey']: _log.error("Publickey through pubsub doesn't match " "through manage platform call. ") _log.error("Address {} was attempting to register.".format( message['address'])) return if passed_uuid: entry = self._registry.get_platform(passed_uuid) else: entry = PlatformRegistry.build_entry( vip_address=message['address'], serverkey=message['serverkey'], vcp_publickey=message['publickey'], is_local=False, discovery_address=message.get('discovery_address'), display_name=message.get('display_name')) self._registry.register(entry) self._pa_agents[entry.platform_uuid] = connected @PubSub.subscribe("pubsub", "platforms") def _on_platforms_messsage(self, peer, sender, bus, topic, headers, message): """ This method subscribes to the platforms topic. Platforms that are being managed should publish to this topic with the agent_list and other interesting things that the volttron central shsould want to know. """ topicsplit = topic.split('/') if len(topicsplit) < 2: _log.error('Invalid topic length published to volttron central') return if topicsplit[1] == 'register': self._handle_pubsub_register(message) return if topicsplit[1] == 'unregister': pass platform_uuid = topicsplit[1] if len(platform_uuid) != 36: _log.error('Invalid platform id detected {}'.format(platform_uuid)) return if not self._registry.get_platform(platform_uuid): _log.warn( 'Platform {} is not registered but sent message {}'.format( platform_uuid, message)) return if len(topicsplit) < 3: _log.warn("Invalid topic length no operation specified.") _log.debug('Doing operation: {}'.format(topicsplit[2])) self._registry.update_agent_list(platform_uuid, message) @PubSub.subscribe("pubsub", "datalogger/platforms") def _on_platform_log_message(self, peer, sender, bus, topic, headers, message): """ Receive message from a registered platform This method is called with stats from the registered platform agents. """ _log.debug('Got topic: {}'.format(topic)) _log.debug('Got message: {}'.format(message)) topicsplit = topic.split('/') platform_uuid = topicsplit[2] # For devices we use everything between devices/../all as a unique # key for determining the last time it was seen. key = '/'.join(topicsplit[:]) _log.debug("key is: {}".format(key)) uuid = topicsplit[2] point_list = [] for point, item in message.iteritems(): point_list.append(point) stats = { 'topic': key, 'points': point_list, 'last_published_utc': format_timestamp(get_aware_utc_now()) } self._registry.update_performance(platform_uuid=platform_uuid, performance=stats) @RPC.export def get_platforms(self): """ Retrieves the platforms that have been registered with VC. @return: """ return self._registry.get_platforms() @RPC.export def get_platform(self, platform_uuid): return self._registry.get_platform(platform_uuid) # @Core.periodic(15) def _auto_register_peer(self): """ Auto register a volttron central platform. This should only happen if there isn't already a peer registered and then only if there hasn't been a local platform registered already. """ pass # if not self._peer_platform: # for p in self._registry.get_platforms(): # if p.is_local: # _log.debug("Reconfiguring local to use: {}".format( # p.platform_uuid)) # self.vip.rpc.call( # VOLTTRON_CENTRAL_PLATFORM, 'reconfigure', # platform_uuid=p.platform_uuid # ) # return # # peers = self.vip.peerlist().get(timeout=30) # if 'platform.agent' in peers: # _log.debug('Auto connecting platform.agent on vc') # # the _peer_platform is set to self because we don't need # # another agent to connect to the bus instead we just use # # this agent. # self._peer_platform = self # local_entry = PlatformRegistry.build_entry( # None, None, None, is_local=True, display_name='local') # # self._registry.register(local_entry) # self._pa_agents[local_entry.platform_uuid] = self # _log.debug("Reconfiguring local to use: {}".format( # local_entry.platform_uuid)) # self.vip.rpc.call( # VOLTTRON_CENTRAL_PLATFORM, 'reconfigure', # platform_uuid=local_entry.platform_uuid # ) def _disconnect_peer_platform(self, sender, **kwargs): _log.debug("disconnecting peer_platform") self._peer_platform = None @RPC.export def list_platform_details(self): _log.debug('list_platform_details {}', self._registry.get_platforms()) return self._registry.get_platforms() @RPC.export def unregister_platform(self, platform_uuid): _log.debug('unregister_platform') platform = self._registry.get_platform(platform_uuid) if platform: self._registry.unregister(platform.vip_address) self._store_registry() if platform_uuid in self._pa_agents.keys(): connected = self._pa_agents[platform_uuid] # Don't stop the local platform because that is this # agent. if not platform.is_local: connected.disconnect() del self._pa_agents[platform_uuid] del connected if platform.is_local: self._peer_platform = None context = 'Unregistered platform {}'.format(platform_uuid) return {'status': 'SUCCESS', 'context': context} else: msg = 'Unable to unregistered platform {}'.format(platform_uuid) return { 'error': { 'code': UNABLE_TO_UNREGISTER_INSTANCE, 'message': msg } } @RPC.export def register_instance(self, discovery_address): """ Adds discovery_address to proposed list of agents. This method is called from a configured agent to hone into the `VolttronCentralAgent`. An administrator must then choose to accept the call from this agent before the agent will be granted status. :param string: The url of the discovery_address for the platform. """ _log.debug('register_instance called via RPC') self._register_instance(discovery_address, display_name=discovery_address) def _register_instance(self, discovery_address, display_name=None, provisional=False): """ Register an instance with VOLTTRON Central based on jsonrpc. NOTE: This method is meant to be called from the jsonrpc method. The registration of the instance will fail in the following cases: - no discoverable instance at the passed uri - no platform.agent installed at the discoverable instance - is a different volttron central managing the discoverable instance. If the display name is not set then the display name becomes the same as the discovery_address. This will be used in the volttron central ui. :param discovery_address: A ip:port for an instance of volttron discovery. :param display_name: :return: dictionary: The dictionary will hold either an error object or a result object. """ _log.info('Attempting to register name: {} with address: {}'.format( display_name, discovery_address)) try: discovery_response = DiscoveryInfo.request_discovery_info( discovery_address) except DiscoveryError as e: return {'error': {'code': DISCOVERY_ERROR, 'message': e.message}} pa_instance_serverkey = discovery_response.serverkey pa_vip_address = discovery_response.vip_address assert pa_instance_serverkey _log.debug('connecting to pa_instance') try: connected_to_pa = ConnectedPlatform( address=pa_vip_address, serverkey=pa_instance_serverkey, secretkey=self.core.secretkey, publickey=self.core.publickey) connected_to_pa.connect() if not connected_to_pa.is_connected(): return { 'error': { 'code': UNABLE_TO_REGISTER_INSTANCE, 'message': 'Could not connect to {}'.format(pa_vip_address) } } except gevent.Timeout: return { 'error': { 'code': UNABLE_TO_REGISTER_INSTANCE, 'message': 'Could not connect to {}'.format(pa_vip_address) } } except Exception as ex: return { 'error': { 'code': UNHANDLED_EXCEPTION, 'message': ex.message } } _log.debug('Connected to address') peers = connected_to_pa.agent.vip.peerlist().get(timeout=30) if VOLTTRON_CENTRAL_PLATFORM not in peers: connected_to_pa.core.stop() return { 'error': { 'code': UNABLE_TO_REGISTER_INSTANCE, 'message': '{} not present.'.format(VOLTTRON_CENTRAL_PLATFORM) } } # The call to manage should return a public key for that agent result = connected_to_pa.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'manage', self._web_info.vip_address, self._web_info.serverkey, self.core.publickey).get(timeout=30) # Magic number 43 is the length of a encoded public key. if len(result) != 43: return { 'error': { 'code': UNABLE_TO_REGISTER_INSTANCE, 'message': 'Invalid publickey returned from {}'.format( VOLTTRON_CENTRAL_PLATFORM) } } # Add the pa's public key so it can connect back to us. auth_file = AuthFile() auth_entry = AuthEntry(credentials="CURVE:{}".format(result), capabilities=['managing']) auth_file.add(auth_entry) # TODO: figure out if we are local or not entry = PlatformRegistry.build_entry(pa_vip_address, pa_instance_serverkey, discovery_address, display_name, False) self._registry.register(entry) self._pa_agents[entry.platform_uuid] = connected_to_pa _log.debug("Adding {}".format(entry.platform_uuid)) instance_name = display_name if display_name else discovery_address context = 'Registered instance {}'.format(instance_name) connected_to_pa.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'reconfigure', platform_uuid=entry.platform_uuid).get(timeout=30) return {'status': 'SUCCESS', 'context': context} def _store_registry(self): self._store('registry', self._registry.package()) @Core.receiver('onsetup') def _setup(self, sender, **kwargs): if not os.environ.get('VOLTTRON_HOME', None): raise ValueError('VOLTTRON_HOME environment must be set!') db_path = os.path.join(os.environ.get('VOLTTRON_HOME'), 'data/volttron.central') db_dir = os.path.dirname(db_path) try: os.makedirs(db_dir) except OSError as exc: if exc.errno != errno.EEXIST or not os.path.isdir(db_dir): raise self.persistence_path = db_path # Returns None if there has been no registration of any platforms. registered = self._load('registry') if registered: self._registry.unpackage(registered) def _to_jsonrpc_obj(self, jsonrpcstr): """ Convert data string into a JsonRpcData named tuple. :param object data: Either a string or a dictionary representing a json document. """ return jsonrpc.JsonRpcData.parse(jsonrpcstr) @RPC.export def jsonrpc(self, env, data): """ The main entry point for ^jsonrpc data This method will only accept rpcdata. The first time this method is called, per session, it must be using get_authorization. That will return a session token that must be included in every subsequent request. The session is tied to the ip address of the caller. :param object env: Environment dictionary for the request. :param object data: The JSON-RPC 2.0 method to call. :return object: An JSON-RPC 2.0 response. """ if env['REQUEST_METHOD'].upper() != 'POST': return jsonrpc.json_error('NA', INVALID_REQUEST, 'Invalid request method') try: rpcdata = self._to_jsonrpc_obj(data) _log.info('rpc method: {}'.format(rpcdata.method)) if rpcdata.method == 'get_authorization': args = { 'username': rpcdata.params['username'], 'password': rpcdata.params['password'], 'ip': env['REMOTE_ADDR'] } sess = self._sessions.authenticate(**args) if not sess: _log.info('Invalid username/password for {}'.format( rpcdata.params['username'])) return jsonrpc.json_error( rpcdata.id, UNAUTHORIZED, "Invalid username/password specified.") _log.info('Session created for {}'.format( rpcdata.params['username'])) return jsonrpc.json_result(rpcdata.id, sess) token = rpcdata.authorization ip = env['REMOTE_ADDR'] _log.debug('REMOTE_ADDR: {}'.format(ip)) session_user = self._sessions.check_session(token, ip) _log.debug('SESSION_USER IS: {}'.format(session_user)) if not session_user: _log.debug("Session Check Failed for Token: {}".format(token)) return jsonrpc.json_error(rpcdata.id, UNAUTHORIZED, "Invalid authentication token") _log.debug('RPC METHOD IS: {}'.format(rpcdata.method)) # Route any other method that isn't result_or_error = self._route_request(session_user, rpcdata.id, rpcdata.method, rpcdata.params) except AssertionError: return jsonrpc.json_error('NA', INVALID_REQUEST, 'Invalid rpc data {}'.format(data)) except Exception as e: return jsonrpc.json_error('NA', UNHANDLED_EXCEPTION, e) _log.debug("RETURNING: {}".format( self._get_jsonrpc_response(rpcdata.id, result_or_error))) return self._get_jsonrpc_response(rpcdata.id, result_or_error) def _get_jsonrpc_response(self, id, result_or_error): """ Wrap the response in either a json-rpc error or result. :param id: :param result_or_error: :return: """ if 'error' in result_or_error: error = result_or_error['error'] _log.debug("RPC RESPONSE ERROR: {}".format(error)) return jsonrpc.json_error(id, error['code'], error['message']) return jsonrpc.json_result(id, result_or_error) def _get_agents(self, platform_uuid, groups): """ Retrieve the list of agents on a specific platform. :param platform_uuid: :param groups: :return: """ _log.debug('_get_agents') connected_to_pa = self._pa_agents[platform_uuid] agents = connected_to_pa.agent.vip.rpc.call( 'platform.agent', 'list_agents').get(timeout=30) for a in agents: if 'admin' in groups: if "platformagent" in a['name'] or \ "volttroncentral" in a['name']: a['vc_can_start'] = False a['vc_can_stop'] = False a['vc_can_restart'] = True else: a['vc_can_start'] = True a['vc_can_stop'] = True a['vc_can_restart'] = True else: # Handle the permissions that are not admin. a['vc_can_start'] = False a['vc_can_stop'] = False a['vc_can_restart'] = False _log.debug('Agents returned: {}'.format(agents)) return agents @Core.receiver('onstart') def _starting(self, sender, **kwargs): """ Starting of the platform :param sender: :param kwargs: :return: """ self.vip.heartbeat.start() q = query.Query(self.core) self._external_addresses = q.query('addresses').get(timeout=30) # TODO: Use all addresses for fallback, #114 _log.debug("external addresses are: {}".format( self._external_addresses)) self._local_address = q.query('local_address').get(timeout=30) _log.debug('Local address is? {}'.format(self._local_address)) _log.debug('Registering jsonrpc and /.* routes') self.vip.rpc.call(MASTER_WEB, 'register_agent_route', r'^/jsonrpc.*', self.core.identity, 'jsonrpc').get(timeout=30) self.vip.rpc.call(MASTER_WEB, 'register_path_route', VOLTTRON_CENTRAL, r'^/.*', self._webroot).get(timeout=30) self.webaddress = self.vip.rpc.call( MASTER_WEB, 'get_bind_web_address').get(timeout=30) assert self.core.publickey assert self.core.secretkey assert self.webaddress self._web_info = DiscoveryInfo.request_discovery_info(self.webaddress) # Reconnect to the platforms that are in the registry. self._reconnect_to_platforms() def __load_persist_data(self): persist_kv = None if os.path.exists(self.persistence_path): try: with open(self.persistence_path, 'rb') as file: persist_kv = jsonapi.loads(file.read()) file.close() except Exception as err: _log.error("Couldn't read persistence data {}".format( err.message)) return persist_kv def _store(self, key, data): persist = self.__load_persist_data() if not persist: persist = {} persist[key] = data with open(self.persistence_path, 'wb') as file: file.write(jsonapi.dumps(persist)) def _load(self, key): persist = self.__load_persist_data() value = None if persist: value = persist.get(key, None) return value def _sync_connected_platforms(self): """ Sync the registry entries with the connections to vcp agents """ _log.debug("len pa_agents {}".format(len(self._pa_agents))) pakeys = set(self._pa_agents.keys()) _log.debug("Syncing with {}".format(pakeys)) for p in self._registry.get_platforms(): if p.platform_uuid in pakeys: pakeys.remove(p.platform_uuid) for k in pakeys: _log.debug('Removing {} from pa_agents'.format(k)) if k in self._pa_agents.keys(): if self._pa_agents[k]: self._pa_agents[k].disconnect() del self._pa_agents[k] @Core.receiver('onstop') def _stopping(self, sender, **kwargs): """ Clean up the agent code before the agent is killed """ for v in self._pa_agents.values(): v.disconnect() self._pa_agents.clear() self.vip.rpc.call(MASTER_WEB, 'unregister_all_agent_routes', self.core.identity).get(timeout=30) @Core.periodic(10) def _update_device_registry(self): """ Updating the device registery from registered platforms. :return: """ try: if not self._flag_updating_deviceregistry: _log.debug("Updating device registry") self._flag_updating_deviceregistry = True self._sync_connected_platforms() unreachable = [] # Loop over the connections to the registered agent platforms. for k, v in self._pa_agents.items(): _log.debug('updating for {}'.format(k)) # Only attempt update if we have a connection to the # agent instance. if v is not None: try: devices = v.agent.vip.rpc.call( VOLTTRON_CENTRAL_PLATFORM, 'get_devices').get(timeout=30) anon_devices = defaultdict(dict) # for each device returned from the query to # get_devices we need to anonymize the k1 in the # anon_devices dictionary. for k1, v1 in devices.items(): _log.debug("before anon: {}, {}".format( k1, v1)) # now we need to do a search/replace on the # self._topic_list so that the devices are # known as the correct itme nin the tree. anon_topic = self._topic_replace_map[k1] # if replaced has not already been replaced if not anon_topic: anon_topic = k1 for sr in self._topic_replace_list: anon_topic = anon_topic.replace( sr['from'], sr['to']) self._topic_replace_map[k1] = anon_topic anon_devices[anon_topic] = v1 _log.debug( 'Anon devices are: {}'.format(anon_devices)) self._registry.update_devices(k, anon_devices) except (gevent.Timeout, Unreachable) as e: _log.error( 'Error getting devices from platform {}'. format(k)) unreachable.append(k) for k in unreachable: if self._pa_agents[k]: self._pa_agents[k].disconnect() del self._pa_agents[k] finally: self._flag_updating_deviceregistry = False def _handle_list_performance(self): _log.debug('Listing performance topics from vc') return [{ 'platform.uuid': x.platform_uuid, 'performance': self._registry.get_performance(x.platform_uuid) } for x in self._registry.get_platforms() if self._registry.get_performance(x.platform_uuid)] def _handle_list_devices(self): _log.debug('Listing devices from vc') return [{ 'platform.uuid': x.platform_uuid, 'devices': self._registry.get_devices(x.platform_uuid) } for x in self._registry.get_platforms() if self._registry.get_devices(x.platform_uuid)] def _handle_list_platforms(self): def get_status(platform_uuid): cn = self._pa_agents.get(platform_uuid) if cn is None: _log.debug('cn is NONE so status is BAD for uuid {}'.format( platform_uuid)) return Status.build(BAD_STATUS, "Platform Unreachable.").as_dict() try: _log.debug('TRYING TO REACH {}'.format(platform_uuid)) health = cn.agent.vip.rpc.call(VOLTTRON_CENTRAL_PLATFORM, 'get_health').get(timeout=30) except Unreachable: health = Status.build(UNKNOWN_STATUS, "Platform Agent Unreachable").as_dict() return health _log.debug('Listing platforms: {}'.format( self._registry.get_platforms())) return [{ 'uuid': x.platform_uuid, 'name': x.display_name, 'health': get_status(x.platform_uuid) } for x in self._registry.get_platforms()] def _route_request(self, session_user, id, method, params): '''Route request to either a registered platform or handle here.''' _log.debug('inside _route_request {}, {}, {}'.format( id, method, params)) def err(message, code=METHOD_NOT_FOUND): return {'error': {'code': code, 'message': message}} if method == 'register_instance': if isinstance(params, list): return self._register_instance(*params) else: return self._register_instance(**params) elif method == 'list_deivces': return self._handle_list_devices() elif method == 'list_performance': return self._handle_list_performance() elif method == 'list_platforms': return self._handle_list_platforms() elif method == 'unregister_platform': return self.unregister_platform(params['platform_uuid']) elif method == 'get_setting': if 'key' not in params or not params['key']: return err('Invalid parameter key not set', INVALID_PARAMS) value = self._setting_store.get(params['key'], None) if value is None: return err('Invalid key specified', INVALID_PARAMS) return value elif method == 'get_setting_keys': return self._setting_store.keys() elif method == 'set_setting': if 'key' not in params or not params['key']: return err('Invalid parameter key not set', INVALID_PARAMS) _log.debug('VALUE: {}'.format(params)) if 'value' not in params: return err('Invalid parameter value not set', INVALID_PARAMS) # if passing None value then remove the value from the keystore # don't raise an error if the key isn't present in the store. if params['value'] is None: if params['key'] in self._setting_store: del self._setting_store[params['key']] else: self._setting_store[params['key']] = params['value'] self._setting_store.sync() return 'SUCCESS' elif 'historian' in method: has_platform_historian = PLATFORM_HISTORIAN in \ self.vip.peerlist().get(timeout=30) if not has_platform_historian: return err( 'The VOLTTRON Central platform historian is unavailable.', UNAVAILABLE_AGENT) _log.debug('Trapping platform.historian to vc.') _log.debug( 'has_platform_historian: {}'.format(has_platform_historian)) if 'historian.query' in method: return self.vip.rpc.call(PLATFORM_HISTORIAN, 'query', **params).get(timeout=30) elif 'historian.get_topic_list' in method: return self.vip.rpc.call(PLATFORM_HISTORIAN, 'get_topic_list').get(timeout=30) fields = method.split('.') if len(fields) < 3: return err('Unknown method {}'.format(method)) platform_uuid = fields[2] platform = self._registry.get_platform(platform_uuid) if not platform: return err('Unknown platform {}'.format(platform_uuid)) platform_method = '.'.join(fields[3:]) _log.debug(platform_uuid) # Get a connection object associated with the platform uuid. cn = self._pa_agents.get(platform_uuid) if not cn: return jsonrpc.json_error(id, UNAVAILABLE_PLATFORM, "Cannot connect to platform.") _log.debug('Routing to {}'.format(VOLTTRON_CENTRAL_PLATFORM)) if platform_method == 'install': if 'admin' not in session_user['groups']: return jsonrpc.json_error( id, UNAUTHORIZED, "Admin access is required to install agents") if platform_method == 'list_agents': _log.debug('Callling list_agents') agents = self._registry.get_agent_list(platform_uuid) if agents is None: _log.warn('No agents found for platform_uuid {}'.format( platform_uuid)) agents = [] for a in agents: if 'admin' not in session_user['groups']: a['permissions'] = { 'can_stop': False, 'can_start': False, 'can_restart': False, 'can_remove': False } else: _log.debug('Permissionse for {} are {}'.format( a['name'], a['permissions'])) return agents else: try: return cn.agent.vip.rpc.call(VOLTTRON_CENTRAL_PLATFORM, 'route_request', id, platform_method, params).get(timeout=30) except (Unreachable, gevent.Timeout) as e: del self._pa_agents[platform_uuid] return err("Can't route to platform", UNAVAILABLE_PLATFORM)
def get(self): self.tv["show_breadcrumb"] = False if self.user: session = SessionHandler().session query = LoginCode.query() query = query.filter(LoginCode.session == session.key) code = query.get() logging.info(query) logging.info(code) if self.GET("r"): if get_cookie(self, name="_lt_"): self.tv["logged_in"] = True url = urllib.unquote(self.GET("r")) url_split = url.split("?") logging.info(url_split) base_url = url_split[0] logging.info(base_url) base_url += "?code=" + get_cookie(self, name="_lt_") logging.info(base_url) if len(url_split) > 1: parameters = url_split[1] logging.info(parameters) # if "?" in parameters: # parameters.replace("?", "&") # logging.info(parameters) # base_url += "&" + parameters # logging.info(base_url) for i in parameters.split("&"): i = "=".join([ i.split("=")[0], urllib.quote(i.split("=")[1]) ]) if "?" not in base_url: base_url += "?" + i else: base_url += "&" + i self.tv["verify_url"] = base_url clear_cookie(self, name="_lt_") self.tv["hide_footer"] = True self.render("login-api.html") else: if self.GET("w"): self.tv["logged_in"] = True self.tv["hide_footer"] = True url = urllib.unquote(self.GET("r")) url_split = url.split("?") logging.info(url_split) base_url = url_split[0] logging.info(base_url) base_url += "?code=" + code.login_code logging.info(base_url) if len(url_split) > 1: parameters = url_split[1] logging.info(parameters) # if "?" in parameters: # parameters.replace("?", "&") # logging.info(parameters) # base_url += "&" + parameters # logging.info(base_url) for i in parameters.split("&"): i = "=".join([ i.split("=")[0], urllib.quote(i.split("=")[1]) ]) if "?" not in base_url: base_url += "?" + i else: base_url += "&" + i self.tv["verify_url"] = base_url self.render("login-api.html") else: url = urllib.unquote(self.GET("r")) if code: url += "?code=" + code.login_code if len(url.split("?")) > 1: if code: url += "&" + urllib.quote( str(url.split("?")[1])) else: url += "?" + urllib.quote( str(url.split("?")[1])) self.redirect(str(url)) else: url = self.request.referer if code: url += "?code=" + code.login_code if len(url.split("?")) > 1: if code: url += "&" + urllib.quote(str(url.split("?")[1])) else: url += "?" + urllib.quote(str(url.split("?")[1])) self.redirect(url) return else: if self.GET("admin"): self.tv["admin"] = True # Redirect URL from the login geostore button if self.GET("r"): self.tv["redirect"] = self.GET("r") else: self.tv["redirect"] = self.request.referer if self.GET("w"): self.tv["hide_footer"] = True self.render("login-api.html") else: self.render("login.html")
def post(self): """ Handles the /login endpoint. Logs in users. """ url = "/login/authorize" if self.POST("email") and self.POST("password"): redirect = None email = self.POST("email").strip().lower() query = User.query() query = query.filter(User.current_email == email) user = query.get() if self.POST("redirect"): redirect = urllib.quote(self.POST("redirect")) if redirect: url += "?r=" + str(redirect) if self.POST("login_window"): url += "?r=" + urllib.quote(self.POST("url")) url += "&w=popup" if not user: error = "Invalid email or password." error_message(self, error) self.redirect(url) return if user.verify_password(self.POST("password")): error = "Invalid email or password." error_message(self, error) self.redirect(url) return if user.status == "PENDING": error = "Your account has not been verified. " error += "Please verify your account by opening the " error += "verification email we sent you. " error_message(self, error) self.redirect(url) return if user.role in ["AGENCYADMIN", "USER"]: if user.status == "VERIFIED": error = "Your account is still pending approval. " error += "Once your account is approved, you will be able " error += "to login. You will receive an email once your " error += "account is approved." error_message(self, error) self.redirect(url) return if user.status == "DISAPPROVED": error = "Your account has been disapproved. " error += "Please contact the Geostore Admin." error_message(self, error) self.redirect(url) return session = SessionHandler(user) session.login() code = session.generate_login_code() expires = datetime.datetime.now() expires += datetime.timedelta(hours=8) if not self.POST("login_window"): set_cookie(self, name="_ut_", value=code, expires=expires) if self.POST("redirect"): url = str(urllib.unquote(self.POST("redirect"))) elif self.POST("login_window"): url = urllib.quote(self.POST("url")) url = "/login/authorize?r=" + url set_cookie(self, name="_lt_", value=code, expires=expires) self.redirect(url) return else: url = self.request.referer logging.info(url) if len(url.split("?")) > 1: url += "&code" + code else: url += "?code=" + code self.redirect(url) return error = "Please enter your email and password." error_message(self, error) self.redirect(url)
def post(self): """ Handles the /password/reset endpoint. Resets password of the user. """ if self.POST("email"): email = self.POST("email").lower().strip() query = User.query() query = query.filter(User.current_email == email) user = query.get() if user: user.password_token = generate_token() user.put() content = { "token": user.password_token, "uid": str(user.key.id()), "receiver_name": user.first_name, "receiver_email": user.current_email, "subject": "Reset Password", "email_type": "password_reset" } taskqueue.add(url="/tasks/email/send", params=content, method="POST") success = "We sent an email to " success += self.POST("email") + ". Please open the " success += "email and click on the password reset link " success += "to reset your password." success_message(self, success) self.redirect("/password/reset") else: error = "Sorry, " + self.POST("email") error += " does not belong to an existing account." error_message(self, error) self.redirect("/password/reset") elif self.POST("new_password") and self.POST("confirm_password") \ and self.GET("uid") and self.GET("password_token"): if self.POST("new_password") == self.POST("confirm_password"): user = User.get_by_id(int(self.GET("uid"))) if user: if user.password_token == self.GET("password_token"): password = user.hash_password( self.POST("new_password")) user.password_token = generate_token() user.previous_passwords.append(password) user.password_update = datetime.datetime.now() user.hashed_password = password user.put() session = SessionHandler(user) session.login() code = session.generate_login_code() if self.POST("redirect"): self.redirect( urllib.unquote(str(self.POST("redirect")))) else: self.redirect("/dashboard") return else: error = "Sorry, your password reset request has expired." error += " Please create a new request." error_message(self, error) self.redirect("/password/reset") else: error = "Sorry, we couldn't process your request. " error += "Please try again." error_message(self, error) self.redirect("/password/reset") else: error = "Passwords do not match." error_message(self, error) url = "/password/reset?password_token=" + self.POST( "password_token") url += "&uid=" + self.POST("uid") self.redirect(url) else: error = "Please fill all required fields." error_message(self, error) self.redirect("/password/reset")