# # Copyright 2017 Ettus Research (National Instruments) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """ Implemented RPC Servers """ from __future__ import print_function from random import choice from string import ascii_letters, digits from multiprocessing import Process from gevent.server import StreamServer from gevent.pool import Pool from gevent import signal from gevent import spawn_later from gevent import Greenlet from gevent import monkey monkey.patch_all() from builtins import str, bytes from builtins import range from mprpc import RPCServer from .mpmlog import get_main_logger TIMEOUT_INTERVAL = 3.0 # Seconds before claim expires TOKEN_LEN = 16 # Length of the token string def no_claim(func): " Decorator for functions that require no token check " func._notok = True return func def no_rpc(func): " Decorator for functions that should not be exposed via RPC " func._norpc = True return func class MPMServer(RPCServer): """ Main MPM RPC class which holds the periph_manager object and translates RPC calls to appropiate calls in the periph_manager and dboard_managers. Claiming and unclaiming is implemented in python only """ _db_methods = [] _mb_methods = [] def __init__(self, state, mgr, *args, **kwargs): self.log = get_main_logger().getChild('RPCServer') self._state = state self._timer = Greenlet() self.session_id = None self.periph_manager = mgr # add public mboard methods without namespace self._update_component_commands(mgr, '', '_mb_methods') # add public dboard methods in `db__` namespace for db_slot, dboard in enumerate(mgr.dboards): self._update_component_commands(dboard, 'db_' + str(db_slot) + '_', '_db_methods') super(MPMServer, self).__init__(*args, **kwargs) def _check_token_valid(self, token): """ Returns True iff: - The device is currently claimed - The claim token matches the one passed in """ return self._state.claim_status.value and \ len(token) == TOKEN_LEN and \ self._state.claim_token.value == bytes(token, 'ascii') def _update_component_commands(self, component, namespace, storage): """ Detect available methods for an object and add them to the RPC server. We skip all private methods, and all methods that use the @no_rpc decorator. """ for method_name in ( m for m in dir(component) if not m.startswith('_') \ and callable(getattr(component, m)) \ and not getattr(getattr(component, m), '_norpc', False) ): new_rpc_method = getattr(component, method_name) command_name = namespace + method_name if getattr(new_rpc_method, '_notok', False): self._add_safe_command(new_rpc_method, command_name) else: self._add_claimed_command(new_rpc_method, command_name) getattr(self, storage).append(command_name) def _add_claimed_command(self, function, command): """ Adds a method with the name command to the RPC server This command will require an acquired claim on the device, and a valid token needs to be passed in for it to not fail. If the method does not require a token, use _add_safe_command(). """ self.log.trace("adding command %s pointing to %s", command, function) def new_claimed_function(token, *args): " Define a function that requires a claim token check " if not self._check_token_valid(token): self.log.warning( "Thwarted attempt to access function `{}' with invalid " \ "token `{}'.".format(command, token) ) raise RuntimeError("Invalid token!") return function(*args) new_claimed_function.__doc__ = function.__doc__ setattr(self, command, new_claimed_function) def _add_safe_command(self, function, command): """ Add a safe method which does not require a claim on the device. If the method should only be called by claimers, use _add_claimed_command(). """ self.log.trace("adding safe command %s pointing to %s", command, function) setattr(self, command, function) def list_methods(self): """ Returns a tuple of public methods and corresponding docs of this RPC server """ return [(met, getattr(self, met).__doc__) for met in dir(self) if not met.startswith('_') and callable(getattr(self, met))] def ping(self, data=None): """ Take in data as argument and send it back This is a safe method which can be called without a claim on the device """ self.log.debug("I was pinged from: %s:%s", self.client_host, self.client_port) return data def claim(self, session_id): """ claim `token` - tries to claim MPM device and provides a human readable session_id This is a safe method which can be called without a claim on the device """ self._state.lock.acquire() if self._state.claim_status.value: return "" self.log.debug( "Claiming from: %s, Session ID: %s", self.client_host, session_id ) self._state.claim_token.value = bytes(''.join( choice(ascii_letters + digits) for _ in range(TOKEN_LEN) ), 'ascii') self._state.claim_status.value = True self._state.lock.release() self.session_id = session_id self._reset_timer() self.log.debug("giving token: %s to host: %s", self._state.claim_token.value, self.client_host) return self._state.claim_token.value def reclaim(self, token): """ reclaim a MPM device with a token. This operation will fail if the device is claimed and the token doesn't match. Or if the device is not claimed at all. """ self._state.lock.acquire() if self._state.claim_status.value: if self._check_token_valid(token): self._state.lock.release() self.log.debug("reclaimed from: %s", self.client_host) self._reset_timer() return True self._state.lock.release() self.log.debug( "reclaim failed from: %s Invalid token: %s", self.client_host, token[:TOKEN_LEN] ) return False self.log.debug( "trying to reclaim unclaimed device from: %s", self.client_host ) return False def _unclaim(self): """ unconditional unclaim - for internal use """ self.log.debug("Releasing claim on session `{}' by `{}'".format( self.session_id, self.client_host )) self._state.claim_status.value = False self._state.claim_token.value = b'' self.session_id = None self.periph_manager.claimed = False self.periph_manager.deinit() self._timer.kill() def _reset_timer(self): """ reset unclaim timer """ self._timer.kill() self._timer = spawn_later(TIMEOUT_INTERVAL, self._unclaim) def unclaim(self, token): """ unclaim `token` - unclaims the MPM device if it is claimed with this token """ if self._check_token_valid(token): self._unclaim() return True self.log.warning("Attempt to unclaim session with invalid token!") return False def get_device_info(self): """ get device information This is as safe method which can be called without a claim on the device """ info = self.periph_manager._get_device_info() if self.client_host in ["127.0.0.1", "::1"]: info["connection"] = "local" else: info["connection"] = "remote" return info def allocate_sid(self, token, *args): """ Forwards the call to periph_manager._allocate_sid with the client ip addresss as argument. Should be used to setup interfaces """ if not self._check_token_valid(token): self.log.warning("Attempt to allocate SID without valid token!") return None return self.periph_manager._allocate_sid(self.client_host, *args) def _rpc_server_process(shared_state, port, mgr): """ Start the RPC server """ connections = Pool(1000) server = StreamServer( ('0.0.0.0', port), handle=MPMServer(shared_state, mgr), spawn=connections) # catch signals and stop the stream server signal(signal.SIGTERM, lambda *args: server.stop()) signal(signal.SIGINT, lambda *args: server.stop()) server.serve_forever() def spawn_rpc_process(state, udp_port, mgr): """ Returns a process that contains the RPC server """ proc_args = [udp_port, state, mgr] proc = Process(target=_rpc_server_process, args=proc_args) proc.start() return proc