def test_read_custom_api_url(self, read_file_mock): """ Test API URL reading """ # No config with patch('os.getenv', return_value=None): read_file_mock.side_effect = IOError() self.assertTrue(helpers.read_custom_api_url() is None) # Only File with patch('os.getenv', return_value=None): read_file_mock.side_effect = None read_file_mock.return_value = 'API_URL_CUSTOM' self.assertEqual('API_URL_CUSTOM', helpers.read_custom_api_url()) # Only env variable with patch('os.getenv', return_value='API_URL_2'): read_file_mock.side_effect = IOError() self.assertEqual('API_URL_2', helpers.read_custom_api_url()) # File priority over env variable with patch('os.getenv', return_value='API_URL_2'): read_file_mock.side_effect = None read_file_mock.return_value = 'API_URL_CUSTOM' self.assertEqual('API_URL_CUSTOM', helpers.read_custom_api_url())
def test_read_custom_api_url(self, read_file_mock): """ Test API URL reading """ # No config with patch('os.getenv', return_value=None): read_file_mock.side_effect = IOError() self.assertTrue(helpers.read_custom_api_url() is None) # Only File with patch('os.getenv', return_value=None): read_file_mock.side_effect = None read_file_mock.return_value = 'API_URL_CUSTOM' self.assertEqual('API_URL_CUSTOM', helpers.read_custom_api_url()) # Only env variable with patch('os.getenv', return_value='API_URL_2'): read_file_mock.side_effect = IOError() self.assertEqual('API_URL_2', helpers.read_custom_api_url()) # File priority over env variable with patch('os.getenv', return_value='API_URL_2'): read_file_mock.side_effect = None read_file_mock.return_value = 'API_URL_CUSTOM' self.assertEqual('API_URL_CUSTOM', helpers.read_custom_api_url())
""" import requests import json from requests.auth import HTTPBasicAuth try: # pylint: disable=import-error,no-name-in-module from urllib.parse import urljoin except ImportError: # pragma: no cover # pylint: disable=import-error,no-name-in-module from urlparse import urljoin from iotlabcli import helpers API_URL = helpers.read_custom_api_url() or 'https://www.iot-lab.info/rest/' # pylint: disable=maybe-no-member,no-member class Api(object): """ IoT-Lab REST API """ _cache = {} def __init__(self, username, password, url=API_URL): """ :param username: username for Basic password auth :param password: password for Basic auth :param url: url of API. """ self.url = url self.auth = HTTPBasicAuth(username, password)
class Api(object): # pylint:disable=too-many-public-methods """ IoT-Lab REST API """ _cache = {} url = helpers.read_custom_api_url() or 'https://www.iot-lab.info/rest/' def __init__(self, username, password): """ :param username: username for Basic password auth :param password: password for Basic auth :param url: url of API. """ self.auth = HTTPBasicAuth(username, password) def get_resources(self, list_id=False, site=None, **selections): """ Get testbed resources description :param list_id: return result in 'exp_list' format '3-12+35' :param site: restrict to site :param **selections: other selections than site """ if site: selections['site'] = site url = 'experiments?%s' % ('id' if list_id else 'resources') for selection, value in sorted(selections.items()): url += '&{0}={1}'.format(selection, value) return self.method(url) def submit_experiment(self, files): """ Submit user experiment :param files: experiment description and firmware(s) :type files: dictionnary :returns JSONObject """ return self.method('experiments', 'post', files=files) def get_experiments(self, state='Running', limit=0, offset=0): """ Get user's experiment :returns JSONObject """ queryset = 'state=%s&limit=%u&offset=%u' % (state, limit, offset) return self.method('experiments?%s' % queryset) def get_experiment_info(self, expid, option=''): """ Get user experiment description. :param expid: experiment id submission (e.g. OAR scheduler) :param option: Restrict to some values: * '': experiment submission * 'resources': resources list * 'id': resources id list: (1-34+72 format) * 'state': experiment state * 'data': experiment tar.gz with description and firmwares * 'start': expected start time """ assert option in ('', 'resources', 'id', 'state', 'data', 'start') url = 'experiments/%s' % expid if option: url += '?%s' % option return self.method(url, raw=(option == 'data')) @classmethod def get_any_experiment_state(cls, expid, username): """Get any experiment state.""" url = 'experiments/{expid}?anystate&user={username}' url = url.format(expid=expid, username=username) # Get value api = cls(None, None) return api.method(url) def stop_experiment(self, expid): """ Stop user experiment. :param id: experiment id submission (e.g. OAR scheduler) """ return self.method('experiments/%s' % expid, 'delete') def reload_experiment(self, expid, exp_json=None): """Reload user experiment. :param expid: experiment id submission (e.g. OAR scheduler) :param exp_json: experiment duration and reservation configuration :returns JSONObject """ url = 'experiments/%d?reload' % expid return self.method(url, 'post', json=exp_json) # Node commands def node_command(self, command, expid, nodes=(), option=None): """ Lanch 'command' on user experiment list nodes :param id: experiment id submission (e.g. OAR scheduler) :param nodes: list of nodes, if empty apply on all nodes :param opt: additional string to pass as option in the query string :returns: dict """ option = option or '' # case of None return self.method( 'experiments/%s/nodes?%s%s' % (expid, command, option), 'post', json=nodes) def node_update(self, expid, files): """ Launch update command (flash firmware) on user experiment list nodes :param id: experiment id submission (e.g. OAR scheduler) :param files: nodes list description and firmware :type files: dict :returns: dict """ return self.method('experiments/%s/nodes?update' % expid, 'post', files=files) def node_profile_load(self, expid, files): """Update profile with profile json on user experiment list nodes :param id: experiment id submission (e.g. OAR scheduler) :param files: nodes list description and firmware :type files: dict :returns: dict """ return self.method('experiments/%s/nodes?profile-load' % expid, 'post', files=files) # script def script_command(self, expid, command, files=None, json=None): """Execute scripts on sites. :param expid: experiment id submission (e.g. OAR scheduler) :param command: in ('run', 'kill', 'status') :param files: 'run' only: script-site association and scripts content :param json: 'kill/status' only: sites list, may be empty for all sites """ # Only json or files and for the correct command (inverted checks) assert json is not None or command in ('run',) assert files is not None or command in ('kill', 'status',) url = 'experiments/%s/script?%s' % (expid, command) return self.method(url, 'post', files=files, json=json) # Profile methods def get_profiles(self, archi=None): """ Get user's list profile description :returns JSONObject """ url = 'profiles' if archi is not None: url += '?archi={0}'.format(archi) return self.method(url) def get_profile(self, name): """ Get user profile description. :param name: profile name :type name: string :returns JSONObject """ return self.method('profiles/%s' % name) def add_profile(self, name, profile): """ Add user profile :param profile: profile description :type profile: JSONObject. """ # dict has no __dict__ and load_profile gives a dict # requests wants a 'simple' type like dict profile = profile if isinstance(profile, dict) else profile.__dict__ ret = self.method('profiles/%s' % name, 'post', json=profile) return ret def del_profile(self, name): """ Delete user profile :param profile_name: name :type profile_name: string """ ret = self.method('profiles/%s' % name, 'delete') return ret def check_credential(self): """ Check that the credentials are valid """ try: self.method('users/%s?login' % self.auth.username, raw=True) return True except HTTPError as err: if err.code == 401: return False raise # pragma no cover # robot def robot_command(self, command, expid, nodes=()): """Run 'status' on user experiment robot list nodes. :param id: experiment id submission (e.g. OAR scheduler) :param nodes: list of nodes, if empty apply on all nodes """ assert command in ('status',) return self.method('experiments/%s/robots' % expid, 'post', json=nodes) def robot_update_mobility(self, expid, name, site, nodes=()): """Update mobility on user experiment robot list nodes. :param id: experiment id submission (e.g. OAR scheduler) :param nodes: list of nodes, if empty apply on all nodes """ url = 'experiments/%s/robots?mobility' % expid url += '&name=%s&site=%s' % (name, site) return self.method(url, 'post', json=nodes) @classmethod def mobility_predefined_list(cls): """List predefined mobilities.""" return cls._get_with_cache('robots/mobility') def mobility_user_list(self): """List user mobilities.""" return self.method('robots/mobility?user') def mobility_user_get(self, name, site): """Get user mobilities.""" return self.method('robots/mobility/{site}/{name}'.format(name=name, site=site)) def method(self, url, method='get', # pylint:disable=too-many-arguments json=None, files=None, raw=False): """Call http `method` on iot-lab-url/'url'. :param url: url of API. :param method: request method :param json: send as 'post' json encoded data :param files: send as 'post' multipart data :param raw: Should data be loaded as json or not """ assert method in ('get', 'post', 'delete') assert (method == 'post') or (files is None and json is None) _url = urljoin(self.url, url) req = self._request(_url, method, auth=self.auth, json=json, files=files) if requests.codes.ok == req.status_code: return req.content if raw else req.json() self._raise_http_error(_url, req) @staticmethod def _request(url, method, **kwargs): """ Call http `method` on 'url' :param url: url of API. :param method: request method :param **kwargs: requests.request additional arguments """ try: return requests.request(method, url, **kwargs) except: # show issue with old requests versions raise RuntimeError(sys.exc_info()) @staticmethod def _raise_http_error(url, req): """ Raises HTTP error for 'url' and 'req' """ # Indent req.text to pretty print it later indented_lines = ['\t' + l for l in req.text.splitlines(True)] msg = '\n' + ''.join(indented_lines) raise HTTPError(url, req.status_code, msg, req.headers, None) @classmethod def get_robot_mapfile(cls, site, mapfile): """ Download robot mapfile. :params site: Map info for site :params mapfile: select type in ('mapconfig', 'mapimage', 'dockconfig') :returns: Image content or json loaded structure """ assert mapfile in ('mapconfig', 'mapimage', 'dockconfig') raw = mapfile in ('mapimage',) api = cls(None, None) url = 'robots/mobility/map/%s?%s' % (site, mapfile) return api.method(url, raw=raw) @classmethod def get_sites(cls): """ Get testbed sites description May be run unauthicated :returns JSONObject """ return cls._get_with_cache('experiments?sites') @classmethod def _get_with_cache(cls, url): """ Get resource from either cache or rest :returns JSONObject """ try: return cls._cache[url] except KeyError: api = cls(None, None) # unauthenticated request return cls._cache.setdefault(url, api.method(url))
first parameter to the function. """ import requests import json from requests.auth import HTTPBasicAuth try: # pylint: disable=import-error,no-name-in-module from urllib.parse import urljoin except ImportError: # pragma: no cover # pylint: disable=import-error,no-name-in-module from urlparse import urljoin from iotlabcli import helpers API_URL = helpers.read_custom_api_url() or 'https://www.iot-lab.info/rest/' # pylint: disable=maybe-no-member,no-member class Api(object): """ IoT-Lab REST API """ _cache = {} def __init__(self, username, password, url=API_URL): """ :param username: username for Basic password auth :param password: password for Basic auth :param url: url of API. """ self.url = url self.auth = HTTPBasicAuth(username, password)
def get_api_url(): """ Return API url """ return read_custom_api_url() or API_URL
class Api(): # pylint:disable=too-many-public-methods """ IoT-Lab REST API """ _cache = {} url = helpers.read_custom_api_url() or 'https://www.iot-lab.info/api/' def __init__(self, username, password): """ :param username: username for Basic password auth :param password: password for Basic auth :param url: url of API. """ self.auth = HTTPBasicAuth(username, password) def get_sites_details(self): """ Get testbed sites details """ return self.method('sites/details') def get_nodes(self, list_id=False, site=None, **selections): """ Get testbed nodes description :param list_id: return result in 'exp_list' format '3-12+35' :param site: restrict to site :param **selections: other selections than site """ if site: selections['site'] = site url = f"nodes{'/ids' if list_id else ''}" if selections: # the order of parameters in the encoded string # will match the order of tuples list url += '?' + urlencode(sorted(list(selections.items()))) return self.method(url) def submit_experiment(self, files): """ Submit user experiment :param files: experiment description and firmware(s) :type files: dictionnary :returns JSONObject """ return self.method('experiments', 'post', files=files) def get_experiments(self, state='Running', limit=0, offset=0): """ Get user's experiment :returns JSONObject """ queryset = f'state={state}&limit={limit}&offset={offset}' return self.method(f'experiments?{queryset}') def get_running_experiments(self): """ Get testbed running experiments """ return self.method('experiments/running') def get_experiment_info(self, expid, option=''): """ Get user experiment description. :param expid: experiment id submission (e.g. OAR scheduler) :param option: Restrict to some values: * '': experiment submission * 'nodes': nodes list * 'nodes_ids': nodes id list: (1-34+72 format) * 'data': experiment tar.gz with description and firmwares * 'deployment': deployment info """ assert option in ('', 'nodes', 'nodes_ids', 'data', 'deployment') url = f'experiments/{expid}' if option: url += f'/{option}' return self.method(url, raw=(option == 'data')) def stop_experiment(self, expid): """ Stop user experiment. :param id: experiment id submission (e.g. OAR scheduler) """ return self.method(f'experiments/{expid}', 'delete') def reload_experiment(self, expid, exp_json=None): """Reload user experiment. :param expid: experiment id submission (e.g. OAR scheduler) :param exp_json: experiment duration and reservation configuration :returns JSONObject """ url = f'experiments/{expid}/reload' return self.method(url, 'post', json=exp_json) # Node commands def node_command(self, command, expid, nodes=(), option=None): """ Lanch 'command' on user experiment list nodes :param id: experiment id submission (e.g. OAR scheduler) :param nodes: list of nodes, if empty apply on all nodes :param opt: additional string to pass as option in the url :returns: dict """ url = f'experiments/{expid}/nodes/{command}' if option: url += f'/{option}' return self.method(url, 'post', json=nodes) def node_update(self, expid, files, binary=False): """ Launch update command (flash firmware) on user experiment list nodes :param id: experiment id submission (e.g. OAR scheduler) :param files: nodes list description and firmware :type files: dict :returns: dict """ url = f'experiments/{expid}/nodes/flash' if binary: url += '/binary' return self.method(url, 'post', files=files) def node_profile_load(self, expid, files): """Update profile with profile json on user experiment list nodes :param id: experiment id submission (e.g. OAR scheduler) :param files: nodes list description and firmware :type files: dict :returns: dict """ return self.method(f'experiments/{expid}/nodes/monitoring', 'post', files=files) # script def script_command(self, expid, command, files=None, json=None): """Execute scripts on sites. :param expid: experiment id submission (e.g. OAR scheduler) :param command: in ('run', 'kill', 'status') :param files: 'run' only: script-site association and scripts content :param json: 'kill/status' only: sites list, may be empty for all sites """ # Only json or files and for the correct command (inverted checks) assert json is not None or command in ('run',) assert files is not None or command in ('kill', 'status',) url = f'experiments/{expid}/scripts/{command}' return self.method(url, 'post', files=files, json=json) # Profile methods def get_profiles(self, archi=None): """ Get user's list profile description :returns JSONObject """ url = 'monitoring' if archi is not None: url += f'?archi={archi}' return self.method(url) def get_profile(self, name): """ Get user profile description. :param name: profile name :type name: string :returns JSONObject """ return self.method(f'monitoring/{name}') def add_profile(self, profile): """ Add user profile :param profile: profile description :type profile: JSONObject. """ # dict has no __dict__ and load_profile gives a dict # requests wants a 'simple' type like dict profile = profile if isinstance(profile, dict) else profile.__dict__ ret = self.method('monitoring', 'post', json=profile) return ret def del_profile(self, name): """ Delete user profile :param profile_name: name :type profile_name: string """ ret = self.method(f'monitoring/{name}', 'delete') return ret def check_credential(self): """ Check that the credentials are valid """ try: self.method('user') return True except HTTPError as err: if err.code == 401: return False raise # pragma no cover # ssh keys api def get_ssh_keys(self): """ Get user's registered ssh keys """ ret = self.method('user/keys') return ret def set_ssh_keys(self, ssh_keys_json): """ Set user's ssh keys """ self.method('user/keys', 'post', json=ssh_keys_json, raw=True) # robot def robot_command(self, command, expid, nodes=()): """Run 'status' on user experiment robot list nodes. :param id: experiment id submission (e.g. OAR scheduler) :param nodes: list of nodes, if empty apply on all nodes """ assert command in ('status',) return self.method(f'experiments/{expid}/robots/{command}', 'post', json=nodes) def robot_update_mobility(self, expid, name, nodes=()): """Update mobility on user experiment robot list nodes. :param id: experiment id submission (e.g. OAR scheduler) :param nodes: list of nodes, if empty apply on all nodes """ url = f'experiments/{expid}/robots/mobility/{name}' return self.method(url, 'post', json=nodes) @classmethod def get_robot_mapfile(cls, site, mapfile): """ Download robot mapfile. :params site: Map info for site :params mapfile: select type in ('mapconfig', 'mapimage', 'dockconfig') :returns: Image content or json loaded structure """ assert mapfile in ('map/config', 'map/image', 'dock/config') raw = mapfile in ('map/image',) api = cls(None, None) url = f'robots/{site}/{mapfile}' return api.method(url, raw=raw) def get_circuits(self, **selections): """List circuits mobilities.""" url = 'mobilities/circuits' if selections: # the order of parameters in the encoded string # will match the order of tuples list url += '?' + urlencode(sorted(list(selections.items()))) return self.method(url) def get_circuit(self, name): """Get user mobilities.""" return self.method(f'mobilities/circuits/{name}') def method(self, url, method='get', # pylint:disable=too-many-arguments json=None, files=None, raw=False): """Call http `method` on iot-lab-url/'url'. :param url: url of API. :param method: request method :param json: send as 'post' json encoded data :param files: send as 'post' multipart data :param raw: Should data be loaded as json or not """ assert method in ('get', 'post', 'delete') assert (method == 'post') or (files is None and json is None) _url = urljoin(self.url, url) req = self._request(_url, method, auth=self.auth, json=json, files=files) if requests.codes.ok == req.status_code: return req.content if raw else req.json() if requests.codes.no_content == req.status_code: return None return self._raise_http_error(_url, req) @staticmethod def _request(url, method, **kwargs): """ Call http `method` on 'url' :param url: url of API. :param method: request method :param **kwargs: requests.request additional arguments """ try: return requests.request(method, url, **kwargs) except Exception: # show issue with old requests versions raise RuntimeError(sys.exc_info()) @staticmethod def _raise_http_error(url, req): """ Raises HTTP error for 'url' and 'req' """ # Indent req.text to pretty print it later indented_lines = ['\t' + line for line in req.text.splitlines(True)] msg = '\n' + ''.join(indented_lines) raise HTTPError(url, req.status_code, msg, req.headers, None) @classmethod def get_sites(cls): """ Get testbed sites description May be run unauthicated :returns JSONObject """ return cls._get_with_cache('sites') @classmethod def _get_with_cache(cls, url): """ Get resource from either cache or rest :returns JSONObject """ try: return cls._cache[url] except KeyError: api = cls(None, None) # unauthenticated request return cls._cache.setdefault(url, api.method(url))