def build_agent_connection(self, identity, instance_name): """ Check if RabbitMQ user and certs exists for this agent, if not create a new one. Add access control/permissions if necessary. Return connection parameters. :param identity: Identity of agent :param instance_name: instance name of the platform :param is_ssl: Flag to indicate if SSL connection or not :return: Return connection parameters """ rmq_user = get_fq_identity(identity, instance_name) permissions = self.get_default_permissions(rmq_user) if self.is_ssl: self.rmq_config.crts.create_ca_signed_cert(rmq_user, overwrite=False) param = None try: self.create_user_with_permissions(rmq_user, permissions, ssl_auth=self.is_ssl) param = self.build_connection_param(rmq_user, ssl_auth=self.is_ssl) except AttributeError: _log.error( "Unable to create RabbitMQ user for the agent. Check if RabbitMQ broker is running" ) return param
def send_alert(self, alert_key, statusobj): """ An alert_key is a quasi-unique key. A listener to the alert can determine whether to pass the alert on to a higher level based upon the frequency of this alert. :param alert_key: :param context: :return: """ if not isinstance(statusobj, Status): raise ValueError('statusobj must be a Status object.') agent_class = self._owner.__class__.__name__ fq_identity = get_fq_identity(self._core().identity) # RMQ and other message buses can't handle '.' because it's used as the separator. This # causes us to change the alert topic's agent_identity to have '_' rather than '.'. topic = topics.ALERTS(agent_class=agent_class, agent_identity=fq_identity.replace('.', '_')) headers = dict(alert_key=alert_key) self._owner.vip.pubsub.publish( "pubsub", topic=topic.format(), headers=headers, message=statusobj.as_json()).get(timeout=10)
def __init__(self, url, identity, instance_name, reconnect_delay=30, vc_url=None): """ The `RMQConnection` class provides a connection to the rabbitmq message bus for both local and remote VOLTTRON instances. The idenity parameter must be non qualified from the reference of the VOLTTRON instance this class is connecting to. In other words if this is a local connection the non qualified identity should be used. But in the case of a remote connection the identity should be prefixed with the <localinstancename.>. An example is as follows: With the following: - local agent identity: platform.agent - local instance name: v1 - remote instance name: v2 For the local connection to v1, platform.agent should be passed as the identy parameter to this class. For a remote connection to v2, v1.platform.agent should be passed to the identity parameter. :param url: :param identity: :param instance_name: :param reconnect_delay: :param vc_url: """ super(RMQConnection, self).__init__(url, identity, instance_name) self._connection = None self.channel = None self._closing = False self._consumer_tag = None self._error_tag = None if vc_url: self._url = url self._connection_param = url self.routing_key = self._vip_queue_name = self._rmq_userid = get_fq_identity( identity, instance_name) self.exchange = 'volttron' self._connect_callback = None self._connect_error_callback = None self._queue_properties = dict() self._explicitly_closed = False self._reconnect_delay = reconnect_delay self._vip_handler = None self._error_handler = None self._instance_name = instance_name self._identity = identity
def install_agent(self, agent_wheel, vip_identity=None, publickey=None, secretkey=None): if self.secure_agent_user: _log.info("Installing secure Volttron agent...") while True: agent_uuid = str(uuid.uuid4()) if agent_uuid in self.agents: continue agent_path = os.path.join(self.install_dir, agent_uuid) try: os.mkdir(agent_path) break except OSError as exc: if exc.errno != errno.EEXIST: raise try: if auth is not None and self.env.verify_agents: unpacker = auth.VolttronPackageWheelFile(agent_wheel, certsobj=certs.Certs()) unpacker.unpack(dest=agent_path) else: unpack(agent_wheel, dest=agent_path) # Is it ok to remove the wheel file after unpacking? os.remove(agent_wheel) final_identity = self._setup_agent_vip_id( agent_uuid, vip_identity=vip_identity) keystore = self.get_agent_keystore(agent_uuid, publickey, secretkey) self._authorize_agent_keys(agent_uuid, final_identity, keystore.public) if self.message_bus == 'rmq': rmq_user = get_fq_identity(final_identity, self.instance_name) certs.Certs().create_signed_cert_files(rmq_user, overwrite=False) if self.secure_agent_user: # When installing, we always create a new user, as anything # that already exists is untrustworthy created_user = self.add_agent_user(self.agent_name(agent_uuid), agent_path) self.set_agent_user_permissions(created_user, agent_uuid, agent_path) except Exception: shutil.rmtree(agent_path) raise return agent_uuid
def test_unhandled_cache_read_exception(volttron_instance, weather, query_agent): try: location = {"location": "fake_location"} query_agent.alert_callback.reset_mock() results1 = query_agent.vip.rpc.call(identity, "get_current_weather", [location]).get(timeout=10)[0] # results should be got from remote assert results1['weather_results'] # cache should be working assert query_agent.alert_callback.call_count == 0 # closing the sqlite connection will force reads and writes to fail weather._cache.close() gevent.sleep(1) results2 = query_agent.vip.rpc.call(identity, "get_current_weather", [location]).get(timeout=10)[0] gevent.sleep(1) assert query_agent.alert_callback.call_count == 2 first_call = query_agent.alert_callback.call_args_list[0][0] second_call = query_agent.alert_callback.call_args_list[1][0] fq_identity = get_fq_identity(weather.core.identity).replace('.', '_') assert first_call[3] == second_call[3] == \ "alerts/BasicWeatherAgent/{}".format(fq_identity) assert first_call[4]['alert_key'] == \ "Cache read failed" assert ujson.loads(first_call[5])['context'] == \ "Weather agent failed to read from cache" assert second_call[4]['alert_key'] == "Cache write failed" assert ujson.loads(second_call[5])['context'] == \ "Weather agent failed to write to cache" # results should be retrieved from the remote api assert len(results2["weather_results"]["points"]) == 4 # results should not have the same timestamps assert results1["observation_time"] != results2["observation_time"] # ensure the correct warning has been given read_warning = False for warning in results2["weather_warnings"]: if warning == "Weather agent failed to read from cache": read_warning = True break assert read_warning finally: # make sure the cache is ready to be used again weather._cache._sqlite_conn = sqlite3.connect(weather._database_file)
def build_agent_connection(self, identity, instance_name): """ Check if RabbitMQ user and certs exists for this agent, if not create a new one. Add access control/permissions if necessary. Return connection parameters. :param identity: Identity of agent :param instance_name: instance name of the platform :param is_ssl: Flag to indicate if SSL connection or not :return: Return connection parameters """ rmq_user = get_fq_identity(identity, instance_name) permissions = self.get_default_permissions(rmq_user) if self.is_ssl: # This could fail with permission error when running in secure mode # and agent was installed when volttron was running on ZMQ instance # and then switched to RMQ instance. In that case # vctl certs create-ssl-keypair should be used to create a cert/key pair # and then agents should be started. try: _log.info("Creating ca signed certs for {}".format(rmq_user)) self.rmq_config.crts.create_signed_cert_files(rmq_user, overwrite=False) except Exception as e: _log.error("Exception creating certs. {}".format(e)) raise RuntimeError(e) param = None try: root_ca_name, server_cert, admin_user = \ certs.Certs.get_admin_cert_names(self.rmq_config.instance_name) if os.access(self.rmq_config.crts.private_key_file(admin_user), os.R_OK): # this must be called from service agents. Create rmq user with permissions # for installed agent this would be done by aip at start of agent self.create_user_with_permissions(rmq_user, permissions, ssl_auth=self.is_ssl) param = self.build_connection_param(rmq_user, ssl_auth=self.is_ssl) except AttributeError: _log.error( "Unable to create RabbitMQ user for the agent. Check if RabbitMQ broker is running" ) return param
def startupagent(self, sender, **kwargs): import urlparse parsed = urlparse.urlparse(self.bind_web_address) ssl_key = self.web_ssl_key ssl_cert = self.web_ssl_cert if parsed.scheme == 'https': # Admin interface is only availble to rmq at present. if self.core.messagebus == 'rmq': self._admin_endpoints = AdminEndpoints( self.core.rmq_mgmt, self._certs.get_cert_public_key( get_fq_identity(self.core.identity))) if ssl_key is None or ssl_cert is None: # Because the master.web service certificate is a client to rabbitmq we # can't use it directly therefore we use the -server on the file to specify # the server based file. base_filename = get_fq_identity(self.core.identity) + "-server" ssl_cert = self._certs.cert_file(base_filename) ssl_key = self._certs.private_key_file(base_filename) if not os.path.isfile(ssl_cert) or not os.path.isfile(ssl_key): self._certs.create_ca_signed_cert(base_filename, type='server') hostname = parsed.hostname port = parsed.port _log.info('Starting web server binding to {}://{}:{}.'.format( parsed.scheme, hostname, port)) # Handle the platform.web routes here. self.registeredroutes.append( (re.compile('^/discovery/$'), 'callable', self._get_discovery)) self.registeredroutes.append( (re.compile('^/discovery/allow$'), 'callable', self._allow)) # these routes are only available for rmq based message bus # at present. if self.core.messagebus == 'rmq': # We need reference to the object so we can change the behavior of # whether or not to have auto certs be created or not. self._csr_endpoints = CSREndpoints(self.core) for rt in self._csr_endpoints.get_routes(): self.registeredroutes.append(rt) for rt in self._admin_endpoints.get_routes(): self.registeredroutes.append(rt) ssl_private_key = self._certs.get_private_key( get_fq_identity(self.core.identity)) for rt in AuthenticateEndpoints(ssl_private_key).get_routes(): self.registeredroutes.append(rt) static_dir = os.path.join(os.path.dirname(__file__), "static") self.registeredroutes.append((re.compile('^/.*$'), 'path', static_dir)) port = int(port) vhome = os.environ.get('VOLTTRON_HOME') logdir = os.path.join(vhome, "log") if not os.path.exists(logdir): os.makedirs(logdir) self.appContainer = WebApplicationWrapper(self, hostname, port) if ssl_key and ssl_cert: svr = WSGIServer((hostname, port), self.appContainer, certfile=ssl_cert, keyfile=ssl_key) else: svr = WSGIServer((hostname, port), self.appContainer) self._server_greenlet = gevent.spawn(svr.serve_forever)
def app_routing(self, env, start_response): """ The main routing function that maps the incoming request to a response. Depending on the registered routes map the request data onto an rpc function or a specific named file. """ path_info = env['PATH_INFO'] if path_info.startswith('/http://'): path_info = path_info[path_info.index('/', len('/http://')):] # only expose a partial list of the env variables to the registered # agents. envlist = [ 'HTTP_USER_AGENT', 'PATH_INFO', 'QUERY_STRING', 'REQUEST_METHOD', 'SERVER_PROTOCOL', 'REMOTE_ADDR', 'HTTP_ACCEPT_ENCODING', 'HTTP_COOKIE', 'CONTENT_TYPE', 'HTTP_AUTHORIZATION', 'SERVER_NAME', 'wsgi.url_scheme', 'HTTP_HOST' ] data = env['wsgi.input'].read() passenv = dict((envlist[i], env[envlist[i]]) for i in range(0, len(envlist)) if envlist[i] in env.keys()) _log.debug('path_info is: {}'.format(path_info)) # Get the peer responsible for dealing with the endpoint. If there # isn't a peer then fall back on the other methods of routing. (peer, res_type) = self.endpoints.get(path_info, (None, None)) _log.debug('Peer path_info is associated with: {}'.format(peer)) if self.is_json_content(env): data = json.loads(data) # Only if https available and rmq for the admin area. if env['wsgi.url_scheme'] == 'https' and self.core.messagebus == 'rmq': # Load the publickey that was used to sign the login message through the env # parameter so agents can use it to verify the Bearer has specific # jwt claims passenv['WEB_PUBLIC_KEY'] = env[ 'WEB_PUBLIC_KEY'] = self._certs.get_cert_public_key( get_fq_identity(self.core.identity)) # if we have a peer then we expect to call that peer's web subsystem # callback to perform whatever is required of the method. if peer: _log.debug('Calling peer {} back with env={} data={}'.format( peer, passenv, data)) res = self.vip.rpc.call(peer, 'route.callback', passenv, data).get(timeout=60) if res_type == "jsonrpc": return self.create_response(res, start_response) elif res_type == "raw": return self.create_raw_response(res, start_response) env['JINJA2_TEMPLATE_ENV'] = tplenv # if ws4pi.socket is set then this connection is a web socket # and so we return the websocket response. if 'ws4py.socket' in env: return env['ws4py.socket'](env, start_response) for k, t, v in self.registeredroutes: if k.match(path_info): _log.debug( "MATCHED:\npattern: {}, path_info: {}\n v: {}".format( k.pattern, path_info, v)) _log.debug('registered route t is: {}'.format(t)) if t == 'callable': # Generally for locally called items. # Changing signature of the "locally" called points to return # a Response object. Our response object then will in turn # be processed and the response will be written back to the # calling client. try: retvalue = v(env, start_response, data) except TypeError: retvalue = self.process_response( start_response, v(env, data)) if isinstance(retvalue, Response): return self.process_response(start_response, retvalue) else: return retvalue elif t == 'peer_route': # RPC calls from agents on the platform _log.debug('Matched peer_route with pattern {}'.format( k.pattern)) peer, fn = (v[0], v[1]) res = self.vip.rpc.call(peer, fn, passenv, data).get(timeout=120) _log.debug(res) return self.create_response(res, start_response) elif t == 'path': # File service from agents on the platform. if path_info == '/': return self._redirect_index(env, start_response) server_path = v + path_info # os.path.join(v, path_info) _log.debug('Serverpath: {}'.format(server_path)) return self._sendfile(env, start_response, server_path) start_response('404 Not Found', [('Content-Type', 'text/html')]) return [b'<h1>Not Found</h1>']
def connect_remote_platform( self, address, serverkey=None, agent_class=None ): """ Agent attempts to connect to a remote platform to exchange data. address must start with http, https, tcp, ampq, or ampqs or a ValueError will be raised If this function is successful it will return an instance of the `agent_class` parameter if not then this function will return None. If the address parameter begins with http or https TODO: use the known host functionality here the agent will attempt to use Discovery to find the values associated with it. Discovery should return either an rmq-address or a vip-address or both. In that situation the connection will be made using zmq. In the event that fails then rmq will be tried. If both fail then None is returned from this function. """ from volttron.platform.vip.agent.utils import build_agent from volttron.platform.vip.agent import Agent if agent_class is None: agent_class = Agent parsed_address = urlparse(address) _log.debug("Begining auth.connect_remote_platform: {}".format(address)) value = None if parsed_address.scheme == "tcp": # ZMQ connection hosts = KnownHostsStore() temp_serverkey = hosts.serverkey(address) if not temp_serverkey: _log.info( "Destination serverkey not found in known hosts file, " "using config" ) destination_serverkey = serverkey elif not serverkey: destination_serverkey = temp_serverkey else: if temp_serverkey != serverkey: raise ValueError( "server_key passed and known hosts serverkey do not " "" "match!" ) destination_serverkey = serverkey publickey, secretkey = ( self._core().publickey, self._core().secretkey, ) _log.debug( "Connecting using: %s", get_fq_identity(self._core().identity) ) value = build_agent( agent_class=agent_class, identity=get_fq_identity(self._core().identity), serverkey=destination_serverkey, publickey=publickey, secretkey=secretkey, message_bus="zmq", address=address, ) elif parsed_address.scheme in ("https", "http"): from volttron.platform.web import DiscoveryInfo from volttron.platform.web import DiscoveryError try: # TODO: Use known host instead of looking up for discovery # info if possible. # We need to discover which type of bus is at the other end. info = DiscoveryInfo.request_discovery_info(address) remote_identity = "{}.{}.{}".format( info.instance_name, get_platform_instance_name(), self._core().identity, ) # if the current message bus is zmq then we need # to connect a zmq on the remote, whether that be the # rmq router or proxy. Also note that we are using the # fully qualified # version of the identity because there will be conflicts if # volttron central has more than one platform.agent connecting if get_messagebus() == "zmq": if not info.vip_address or not info.serverkey: err = ( "Discovery from {} did not return serverkey " "and/or vip_address".format(address) ) raise ValueError(err) _log.debug( "Connecting using: %s", get_fq_identity(self._core().identity), ) # use fully qualified identity value = build_agent( identity=get_fq_identity(self._core().identity), address=info.vip_address, serverkey=info.serverkey, secretkey=self._core().secretkey, publickey=self._core().publickey, agent_class=agent_class, ) else: # we are on rmq messagebus # This is if both remote and local are rmq message buses. if info.messagebus_type == "rmq": _log.debug("Both remote and local are rmq messagebus.") fqid_local = get_fq_identity(self._core().identity) # Check if we already have the cert, if so use it # instead of requesting cert again remote_certs_dir = self.get_remote_certs_dir() remote_cert_name = "{}.{}".format( info.instance_name, fqid_local ) certfile = os.path.join( remote_certs_dir, remote_cert_name + ".crt" ) if os.path.exists(certfile): response = certfile else: response = self.request_cert( address, fqid_local, info ) if response is None: _log.error("there was no response from the server") value = None elif isinstance(response, tuple): if response[0] == "PENDING": _log.info( "Waiting for administrator to accept a " "CSR request." ) value = None # elif isinstance(response, dict): # response elif os.path.exists(response): # info = DiscoveryInfo.request_discovery_info( # address) # From the remote platforms perspective the # remote user name is # remoteinstance.localinstance.identity, # this is what we must # pass to the build_remote_connection_params # for a successful remote_rmq_user = get_fq_identity( fqid_local, info.instance_name ) _log.debug( "REMOTE RMQ USER IS: %s", remote_rmq_user ) remote_rmq_address = self._core().rmq_mgmt.build_remote_connection_param( remote_rmq_user, info.rmq_address, ssl_auth=True, cert_dir=self.get_remote_certs_dir(), ) value = build_agent( identity=fqid_local, address=remote_rmq_address, instance_name=info.instance_name, publickey=self._core().publickey, secretkey=self._core().secretkey, message_bus="rmq", enable_store=False, agent_class=agent_class, ) else: raise ValueError( "Unknown path through discovery process!" ) else: # TODO: cache the connection so we don't always have # to ping the server to connect. # This branch happens when the message bus is not # the same note # this writes to the agent-data directory of this # agent if the agent # is installed. if get_messagebus() == "rmq": if not os.path.exists("keystore.json"): with open("keystore.json", "w") as file_pointer: file_pointer.write( jsonapi.dumps( KeyStore.generate_keypair_dict() ) ) with open("keystore.json") as file_pointer: keypair = jsonapi.loads(file_pointer.read()) value = build_agent( agent_class=agent_class, identity=remote_identity, serverkey=info.serverkey, publickey=keypair.get("publickey"), secretkey=keypair.get("secretekey"), message_bus="zmq", address=info.vip_address, ) except DiscoveryError: _log.error( "Couldn't connect to %s or incorrect response returned " "response was %s", address, value, ) else: raise ValueError( "Invalid configuration found the address: {} has an invalid " "scheme".format(address) ) return value
def get_user_claim_from_bearer(bearer): certs = Certs() pubkey = certs.get_cert_public_key(get_fq_identity(MASTER_WEB)) return jwt.decode(bearer, pubkey, algorithms='RS256')
def start_agent(self, agent_uuid): name = self.agent_name(agent_uuid) agent_dir = os.path.join(self.install_dir, agent_uuid) agent_path_with_name = os.path.join(agent_dir, name) execenv = self.agents.get(agent_uuid) if execenv and execenv.process.poll() is None: _log.warning('request to start already running agent %s', agent_path_with_name) raise ValueError('agent is already running') pkg = UnpackedPackage(agent_path_with_name) if auth is not None and self.env.verify_agents: auth.UnpackedPackageVerifier(pkg.distinfo).verify() metadata = pkg.metadata try: exports = metadata['extensions']['python.exports'] except KeyError: try: exports = metadata['exports'] except KeyError: raise ValueError('no entry points exported') try: module = exports['volttron.agent']['launch'] except KeyError: try: module = exports['setuptools.installation']['eggsecutable'] except KeyError: _log.error('no agent launch class specified in package %s', agent_path_with_name) raise ValueError('no agent launch class specified in package') config = os.path.join(pkg.distinfo, 'config') tag = self.agent_tag(agent_uuid) environ = os.environ.copy() environ['PYTHONPATH'] = ':'.join([agent_path_with_name] + sys.path) environ['PATH'] = (os.path.abspath(os.path.dirname(sys.executable)) + ':' + environ['PATH']) if os.path.exists(config): environ['AGENT_CONFIG'] = config else: environ.pop('AGENT_CONFIG', None) if tag: environ['AGENT_TAG'] = tag else: environ.pop('AGENT_TAG', None) environ['AGENT_SUB_ADDR'] = self.subscribe_address environ['AGENT_PUB_ADDR'] = self.publish_address environ['AGENT_UUID'] = agent_uuid environ['_LAUNCHED_BY_PLATFORM'] = '1' # For backwards compatibility create the identity file if it does not # exist. identity_file = os.path.join(self.install_dir, agent_uuid, "IDENTITY") if not os.path.exists(identity_file): _log.debug( 'IDENTITY FILE MISSING: CREATING IDENTITY FILE WITH VALUE: {}'. format(agent_uuid)) with open(identity_file, 'w') as fp: fp.write(agent_uuid) with open(identity_file, 'r') as fp: agent_vip_identity = fp.read() environ['AGENT_VIP_IDENTITY'] = agent_vip_identity module, _, func = module.partition(':') # if func: # code = '__import__({0!r}, fromlist=[{1!r}]).{1}()'.format(module, # func) # argv = [sys.executable, '-c', code] # else: argv = [sys.executable, '-m', module] resmon = getattr(self.env, 'resmon', None) agent_user = None data_dir = self._get_agent_data_dir(agent_path_with_name) if self.secure_agent_user: _log.info("Starting agent securely...") user_id_path = os.path.join(agent_dir, "USER_ID") try: with open(user_id_path, "r") as user_id_file: volttron_agent_id = user_id_file.readline() pwd.getpwnam(volttron_agent_id) agent_user = volttron_agent_id _log.info("Found secure volttron agent user {}".format( agent_user)) except (IOError, KeyError) as err: _log.info("No existing volttron agent user was found at {} due " "to {}".format(user_id_path, err)) # May be switched from normal to secure mode with existing agents. To handle this case # create users and also set permissions again for existing files agent_user = self.add_agent_user(name, agent_dir) self.set_agent_user_permissions(agent_user, agent_uuid, agent_dir) # additionally give permissions to contents of agent-data dir. # This is needed only for agents installed before switching to # secure mode. Agents installed in secure mode will own files # in agent-data dir # Moved this to the top so that "agent-data" directory gets # created in the beginning #data_dir = self._get_agent_data_dir(agent_path_with_name) for (root, directories, files) in os.walk(data_dir, topdown=True): for directory in directories: self.set_acl_for_path("rwx", agent_user, os.path.join(root, directory)) for f in files: self.set_acl_for_path("rwx", agent_user, os.path.join(root, f)) if self.message_bus == 'rmq': rmq_user = get_fq_identity(agent_vip_identity, self.instance_name) _log.info("Create RMQ user {} for agent {}".format(rmq_user, agent_vip_identity)) self.rmq_mgmt.create_user_with_permissions(rmq_user, self.rmq_mgmt.get_default_permissions(rmq_user), ssl_auth=True) key_file = certs.Certs().private_key_file(rmq_user) if not os.path.exists(key_file): # This could happen when user switches from zmq to rmq after installing agent _log.info(f"agent certs don't exists. creating certs for agent") certs.Certs().create_signed_cert_files(rmq_user, overwrite=False) if self.secure_agent_user: # give read access to user to its own private key file. self.set_acl_for_path("r", agent_user, key_file) if resmon is None: if agent_user: execenv = SecureExecutionEnvironment(agent_user=agent_user) else: execenv = ExecutionEnvironment() else: execreqs = self._read_execreqs(pkg.distinfo) execenv = self._reserve_resources(resmon, execreqs, agent_user=agent_user) execenv.name = name or agent_path_with_name _log.info('starting agent %s', agent_path_with_name) # data_dir = self._get_agent_data_dir(agent_path_with_name) _log.info("starting agent using {} ".format(type(execenv))) execenv.execute(argv, cwd=agent_path_with_name, env=environ, close_fds=True, stdin=open(os.devnull), stdout=PIPE, stderr=PIPE) self.agents[agent_uuid] = execenv proc = execenv.process _log.info('agent %s has PID %s', agent_path_with_name, proc.pid) gevent.spawn(log_stream, 'agents.stderr', name, proc.pid, argv[0], log_entries('agents.log', name, proc.pid, logging.ERROR, proc.stderr)) gevent.spawn(log_stream, 'agents.stdout', name, proc.pid, argv[0], ((logging.INFO, line) for line in (l.splitlines() for l in proc.stdout))) return self.agent_status(agent_uuid)