Exemple #1
0
    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)
Exemple #2
0
    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")
Exemple #3
0
    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()
Exemple #4
0
    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
Exemple #5
0
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
Exemple #6
0
    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()
Exemple #7
0
    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")
Exemple #8
0
    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'))
Exemple #9
0
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)
Exemple #10
0
    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()
Exemple #11
0
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
Exemple #12
0
    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'))
Exemple #13
0
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)
Exemple #14
0
    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")
Exemple #15
0
    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)
Exemple #16
0
    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")