diff --git a/lib/rucio/client/baseclient.py b/lib/rucio/client/baseclient.py index 66faea8e77..f66617090a 100644 --- a/lib/rucio/client/baseclient.py +++ b/lib/rucio/client/baseclient.py @@ -27,6 +27,7 @@ from os import environ, fdopen, path, makedirs, geteuid from shutil import move from tempfile import mkstemp +from typing import Any, Callable, Optional from urllib.parse import urlparse from dogpile.cache import make_region @@ -39,7 +40,8 @@ from rucio.common.config import config_get, config_get_bool, config_get_int from rucio.common.exception import (CannotAuthenticate, ClientProtocolNotSupported, NoAuthInformation, MissingClientParameter, - MissingModuleException, ServerConnectionException) + MissingModuleException, ServerConnectionException, + ConfigNotFound) from rucio.common.extra import import_extras from rucio.common.utils import build_url, get_tmp_dir, my_key_generator, parse_response, ssh_sign, setup_logger @@ -79,7 +81,17 @@ class BaseClient(object): TOKEN_PREFIX = 'auth_token_' TOKEN_EXP_PREFIX = 'auth_token_exp_' - def __init__(self, rucio_host=None, auth_host=None, account=None, ca_cert=None, auth_type=None, creds=None, timeout=600, user_agent='rucio-clients', vo=None, logger=None): + def __init__(self, + rucio_host: Optional[str] = None, + auth_host: Optional[str] = None, + account: Optional[str] = None, + ca_cert: Optional[str] = None, + auth_type: Optional[str] = None, + creds: Optional[dict[str, Any]] = None, + timeout: Optional[int] = 600, + user_agent: Optional[str] = 'rucio-clients', + vo: Optional[str] = None, + logger: Callable = LOG): # type: ignore """ Constructor of the BaseClient. :param rucio_host: The address of the rucio server, if None it is read from the config file. @@ -97,9 +109,8 @@ def __init__(self, rucio_host=None, auth_host=None, account=None, ca_cert=None, """ self.host = rucio_host - self.list_hosts = [] self.auth_host = auth_host - self.logger = logger or LOG + self.logger = logger self.session = Session() self.user_agent = "%s/%s" % (user_agent, version.version_string()) # e.g. "rucio-clients/0.2.13" sys.argv[0] = sys.argv[0].split('/')[-1] @@ -116,110 +127,26 @@ def __init__(self, rucio_host=None, auth_host=None, account=None, ca_cert=None, try: self.trace_host = config_get('trace', 'trace_host') - except (NoOptionError, NoSectionError): + except (NoOptionError, NoSectionError, ConfigNotFound): self.trace_host = self.host self.logger.debug('No trace_host passed. Using rucio_host instead') + self.list_hosts = [self.host] self.account = account self.vo = vo self.ca_cert = ca_cert - self.auth_type = auth_type - self.creds = creds - self.auth_token = None - self.auth_token_file_path = config_get('client', 'auth_token_file_path', False, None) + self.auth_token = "" self.headers = {} self.timeout = timeout self.request_retries = self.REQUEST_RETRIES self.token_exp_epoch = None - self.token_exp_epoch_file = None self.auth_oidc_refresh_active = config_get_bool('client', 'auth_oidc_refresh_active', False, False) + # defining how many minutes before token expires, oidc refresh (if active) should start self.auth_oidc_refresh_before_exp = config_get_int('client', 'auth_oidc_refresh_before_exp', False, 20) - if auth_type is None: - self.logger.debug('No auth_type passed. Trying to get it from the environment variable RUCIO_AUTH_TYPE and config file.') - if 'RUCIO_AUTH_TYPE' in environ: - if environ['RUCIO_AUTH_TYPE'] not in ['userpass', 'x509', 'x509_proxy', 'gss', 'ssh', 'saml', 'oidc']: - raise MissingClientParameter('Possible RUCIO_AUTH_TYPE values: userpass, x509, x509_proxy, gss, ssh, saml, oidc, vs. ' + environ['RUCIO_AUTH_TYPE']) - self.auth_type = environ['RUCIO_AUTH_TYPE'] - else: - try: - self.auth_type = config_get('client', 'auth_type') - except (NoOptionError, NoSectionError) as error: - raise MissingClientParameter('Option \'%s\' cannot be found in config file' % error.args[0]) - - if self.auth_type == 'oidc': - if not self.creds: - self.creds = {} - # if there are defautl values, check if rucio.cfg does not specify them, otherwise put default - if 'oidc_refresh_lifetime' not in self.creds or self.creds['oidc_refresh_lifetime'] is None: - self.creds['oidc_refresh_lifetime'] = config_get('client', 'oidc_refresh_lifetime', False, None) - if 'oidc_issuer' not in self.creds or self.creds['oidc_issuer'] is None: - self.creds['oidc_issuer'] = config_get('client', 'oidc_issuer', False, None) - if 'oidc_audience' not in self.creds or self.creds['oidc_audience'] is None: - self.creds['oidc_audience'] = config_get('client', 'oidc_audience', False, None) - if 'oidc_auto' not in self.creds or self.creds['oidc_auto'] is False: - self.creds['oidc_auto'] = config_get_bool('client', 'oidc_auto', False, False) - if self.creds['oidc_auto']: - if 'oidc_username' not in self.creds or self.creds['oidc_username'] is None: - self.creds['oidc_username'] = config_get('client', 'oidc_username', False, None) - if 'oidc_password' not in self.creds or self.creds['oidc_password'] is None: - self.creds['oidc_password'] = config_get('client', 'oidc_password', False, None) - if 'oidc_scope' not in self.creds or self.creds['oidc_scope'] == 'openid profile': - self.creds['oidc_scope'] = config_get('client', 'oidc_scope', False, 'openid profile') - if 'oidc_polling' not in self.creds or self.creds['oidc_polling'] is False: - self.creds['oidc_polling'] = config_get_bool('client', 'oidc_polling', False, False) - - if not self.creds: - self.logger.debug('No creds passed. Trying to get it from the config file.') - self.creds = {} - try: - if self.auth_type in ['userpass', 'saml']: - self.creds['username'] = config_get('client', 'username') - self.creds['password'] = config_get('client', 'password') - elif self.auth_type == 'x509': - if "RUCIO_CLIENT_CERT" in environ: - client_cert = environ["RUCIO_CLIENT_CERT"] - else: - client_cert = config_get('client', 'client_cert') - self.creds['client_cert'] = path.abspath(path.expanduser(path.expandvars(client_cert))) - if not path.exists(self.creds['client_cert']): - raise MissingClientParameter('X.509 client certificate not found: %s' % self.creds['client_cert']) - - if "RUCIO_CLIENT_KEY" in environ: - client_key = environ["RUCIO_CLIENT_KEY"] - else: - client_key = config_get('client', 'client_key') - self.creds['client_key'] = path.abspath(path.expanduser(path.expandvars(client_key))) - if not path.exists(self.creds['client_key']): - raise MissingClientParameter('X.509 client key not found: %s' % self.creds['client_key']) - else: - perms = oct(os.stat(self.creds['client_key']).st_mode)[-3:] - if perms not in ['400', '600']: - raise CannotAuthenticate('X.509 authentication selected, but private key (%s) permissions are liberal (required: 400 or 600, found: %s)' % (self.creds['client_key'], perms)) - - elif self.auth_type == 'x509_proxy': - try: - self.creds['client_proxy'] = path.abspath(path.expanduser(path.expandvars(config_get('client', 'client_x509_proxy')))) - except NoOptionError: - # Recreate the classic GSI logic for locating the proxy: - # - $X509_USER_PROXY, if it is set. - # - /tmp/x509up_u`id -u` otherwise. - # If neither exists (at this point, we don't care if it exists but is invalid), then rethrow - if 'X509_USER_PROXY' in environ: - self.creds['client_proxy'] = environ['X509_USER_PROXY'] - else: - fname = '/tmp/x509up_u%d' % geteuid() - if path.exists(fname): - self.creds['client_proxy'] = fname - else: - raise MissingClientParameter('Cannot find a valid X509 proxy; not in %s, $X509_USER_PROXY not set, and ' - '\'x509_proxy\' not set in the configuration file.' % fname) - elif self.auth_type == 'ssh': - self.creds['ssh_private_key'] = path.abspath(path.expanduser(path.expandvars(config_get('client', 'ssh_private_key')))) - except (NoOptionError, NoSectionError) as error: - if error.args[0] != 'client_key': - raise MissingClientParameter('Option \'%s\' cannot be found in config file' % error.args[0]) + self.auth_type = self._get_auth_type(auth_type) + self.creds = self._get_creds(creds) rucio_scheme = urlparse(self.host).scheme auth_scheme = urlparse(self.auth_host).scheme @@ -241,8 +168,6 @@ def __init__(self, rucio_host=None, auth_host=None, account=None, ca_cert=None, self.logger.debug('No ca_cert found in configuration. Falling back to Mozilla default CA bundle (certifi).') self.ca_cert = True - self.list_hosts = [self.host] - if account is None: self.logger.debug('No account passed. Trying to get it from the RUCIO_ACCOUNT environment variable or the config file.') try: @@ -265,28 +190,124 @@ def __init__(self, rucio_host=None, auth_host=None, account=None, ca_cert=None, self.logger.debug('No VO found. Using default VO.') self.vo = 'def' - token_filename_suffix = "for_default_account" if self.account is None else "for_account_" + self.account + self.auth_token_file_path, self.token_exp_epoch_file, self.token_file, self.token_path = self._get_auth_tokens() + self.__authenticate() + try: + self.request_retries = config_get_int('client', 'request_retries') + except (NoOptionError, ConfigNotFound): + self.logger.debug('request_retries not specified in config file. Taking default.') + except ValueError: + self.logger.debug('request_retries must be an integer. Taking default.') + + def _get_auth_tokens(self) -> tuple[Optional[str], str, str, str]: # if token file path is defined in the rucio.cfg file, use that file. Currently this prevents authenticating as another user or VO. - if self.auth_token_file_path: - self.token_file = self.auth_token_file_path - self.token_path = '/'.join(self.token_file.split('/')[:-1]) + auth_token_file_path = config_get('client', 'auth_token_file_path', False, None) + token_filename_suffix = "for_default_account" if self.account is None else "for_account_" + self.account + + if auth_token_file_path: + token_file = auth_token_file_path + token_path = '/'.join(auth_token_file_path.split('/')[:-1]) + else: - self.token_path = self.TOKEN_PATH_PREFIX + getpass.getuser() + token_path = self.TOKEN_PATH_PREFIX + getpass.getuser() if self.vo != 'def': - self.token_path += '@%s' % self.vo - self.token_file = self.token_path + '/' + self.TOKEN_PREFIX + token_filename_suffix + token_path += '@%s' % self.vo - self.token_exp_epoch_file = self.token_path + '/' + self.TOKEN_EXP_PREFIX + token_filename_suffix + token_file = token_path + '/' + self.TOKEN_PREFIX + token_filename_suffix - self.__authenticate() + token_exp_epoch_file = token_path + '/' + self.TOKEN_EXP_PREFIX + token_filename_suffix + return auth_token_file_path, token_exp_epoch_file, token_file, token_path - try: - self.request_retries = config_get_int('client', 'request_retries') - except (NoOptionError, RuntimeError): - LOG.debug('request_retries not specified in config file. Taking default.') - except ValueError: - self.logger.debug('request_retries must be an integer. Taking default.') + def _get_auth_type(self, auth_type: Optional[str]) -> str: + if auth_type is None: + self.logger.debug('No auth_type passed. Trying to get it from the environment variable RUCIO_AUTH_TYPE and config file.') + if 'RUCIO_AUTH_TYPE' in environ: + if environ['RUCIO_AUTH_TYPE'] not in ['userpass', 'x509', 'x509_proxy', 'gss', 'ssh', 'saml', 'oidc']: + raise MissingClientParameter('Possible RUCIO_AUTH_TYPE values: userpass, x509, x509_proxy, gss, ssh, saml, oidc, vs. ' + environ['RUCIO_AUTH_TYPE']) + auth_type = environ['RUCIO_AUTH_TYPE'] + else: + try: + auth_type = config_get('client', 'auth_type') + except (NoOptionError, NoSectionError) as error: + raise MissingClientParameter('Option \'%s\' cannot be found in config file' % error.args[0]) + return auth_type + + def _get_creds(self, creds: Optional[dict[str, Any]]) -> dict[str, Any]: + if self.auth_type == 'oidc': + if not creds: + creds = {} + # if there are defautl values, check if rucio.cfg does not specify them, otherwise put default + if 'oidc_refresh_lifetime' not in creds or creds['oidc_refresh_lifetime'] is None: + creds['oidc_refresh_lifetime'] = config_get('client', 'oidc_refresh_lifetime', False, None) + if 'oidc_issuer' not in creds or creds['oidc_issuer'] is None: + creds['oidc_issuer'] = config_get('client', 'oidc_issuer', False, None) + if 'oidc_audience' not in creds or creds['oidc_audience'] is None: + creds['oidc_audience'] = config_get('client', 'oidc_audience', False, None) + if 'oidc_auto' not in creds or creds['oidc_auto'] is False: + creds['oidc_auto'] = config_get_bool('client', 'oidc_auto', False, False) + if creds['oidc_auto']: + if 'oidc_username' not in creds or creds['oidc_username'] is None: + creds['oidc_username'] = config_get('client', 'oidc_username', False, None) + if 'oidc_password' not in creds or creds['oidc_password'] is None: + creds['oidc_password'] = config_get('client', 'oidc_password', False, None) + if 'oidc_scope' not in creds or creds['oidc_scope'] == 'openid profile': + creds['oidc_scope'] = config_get('client', 'oidc_scope', False, 'openid profile') + if 'oidc_polling' not in creds or creds['oidc_polling'] is False: + creds['oidc_polling'] = config_get_bool('client', 'oidc_polling', False, False) + + if creds is None: + self.logger.debug('No creds passed. Trying to get it from the config file.') + creds = {} + try: + if self.auth_type in ['userpass', 'saml']: + creds['username'] = config_get('client', 'username') + creds['password'] = config_get('client', 'password') + elif self.auth_type == 'x509': + if "RUCIO_CLIENT_CERT" in environ: + client_cert = environ["RUCIO_CLIENT_CERT"] + else: + client_cert = config_get('client', 'client_cert') + creds['client_cert'] = path.abspath(path.expanduser(path.expandvars(client_cert))) + if not path.exists(creds['client_cert']): + raise MissingClientParameter('X.509 client certificate not found: %s' % creds['client_cert']) + + if "RUCIO_CLIENT_KEY" in environ: + client_key = environ["RUCIO_CLIENT_KEY"] + else: + client_key = config_get('client', 'client_key') + creds['client_key'] = path.abspath(path.expanduser(path.expandvars(client_key))) + if not path.exists(creds['client_key']): + raise MissingClientParameter('X.509 client key not found: %s' % creds['client_key']) + else: + perms = oct(os.stat(creds['client_key']).st_mode)[-3:] + if perms not in ['400', '600']: + raise CannotAuthenticate('X.509 authentication selected, but private key (%s) permissions are liberal (required: 400 or 600, found: %s)' % (creds['client_key'], perms)) + + elif self.auth_type == 'x509_proxy': + try: + creds['client_proxy'] = path.abspath(path.expanduser(path.expandvars(config_get('client', 'client_x509_proxy')))) + except NoOptionError: + # Recreate the classic GSI logic for locating the proxy: + # - $X509_USER_PROXY, if it is set. + # - /tmp/x509up_u`id -u` otherwise. + # If neither exists (at this point, we don't care if it exists but is invalid), then rethrow + if 'X509_USER_PROXY' in environ: + creds['client_proxy'] = environ['X509_USER_PROXY'] + else: + fname = '/tmp/x509up_u%d' % geteuid() + if path.exists(fname): + creds['client_proxy'] = fname + else: + raise MissingClientParameter( + 'Cannot find a valid X509 proxy; not in %s, $X509_USER_PROXY not set, and ' + '\'x509_proxy\' not set in the configuration file.' % fname) + elif self.auth_type == 'ssh': + creds['ssh_private_key'] = path.abspath(path.expanduser(path.expandvars(config_get('client', 'ssh_private_key')))) + except (NoOptionError, NoSectionError) as error: + if error.args[0] != 'client_key': + raise MissingClientParameter('Option \'%s\' cannot be found in config file' % error.args[0]) + return creds def _get_exception(self, headers, status_code=None, data=None): """ @@ -667,7 +688,7 @@ def __get_token_x509(self): url = build_url(self.auth_host, path='auth/x509_proxy') client_cert = self.creds['client_proxy'] - if not path.exists(client_cert): + if (client_cert is not None) and not (path.exists(client_cert)): self.logger.error('given client cert (%s) doesn\'t exist' % client_cert) return False if client_key is not None and not path.exists(client_key): diff --git a/lib/rucio/common/config.py b/lib/rucio/common/config.py index 1732d5e103..f1b72ceca9 100644 --- a/lib/rucio/common/config.py +++ b/lib/rucio/common/config.py @@ -189,12 +189,12 @@ def config_get( """ try: return convert_type_fnc(get_config().get(section, option)) - except (configparser.NoOptionError, configparser.NoSectionError, RuntimeError) as err: + except (configparser.NoOptionError, configparser.NoSectionError, ConfigNotFound) as err: try: legacy_config = get_legacy_config(section, option) if legacy_config is not None: return convert_type_fnc(legacy_config) - except RuntimeError: + except ConfigNotFound: pass from rucio.common.utils import is_client @@ -730,7 +730,7 @@ def get_lfn2pfn_algorithm_default(): default_lfn2pfn = "hash" try: default_lfn2pfn = config_get('policy', 'lfn2pfn_algorithm_default') - except (configparser.NoOptionError, configparser.NoSectionError, RuntimeError): + except (configparser.NoOptionError, configparser.NoSectionError, ConfigNotFound, RuntimeError): pass return default_lfn2pfn @@ -786,11 +786,13 @@ def __init__(self): configs = [os.path.join(confdir, 'rucio.cfg') for confdir in get_config_dirs()] self.configfile = next(iter(filter(os.path.exists, configs)), None) if self.configfile is None: - raise RuntimeError('Could not load Rucio configuration file. ' - 'Rucio looked in the following paths for a configuration file, in order:' - '\n\t' + '\n\t'.join(configs)) + raise ConfigNotFound( + 'Could not load Rucio configuration file. ' + 'Rucio looked in the following paths for a configuration file, in order:' + '\n\t' + '\n\t'.join(configs)) if not self.parser.read(self.configfile) == [self.configfile]: - raise RuntimeError('Could not load Rucio configuration file. ' - 'Rucio tried loading the following configuration file:' - '\n\t' + self.configfile) + raise ConfigNotFound( + 'Could not load Rucio configuration file. ' + 'Rucio tried loading the following configuration file:' + '\n\t' + self.configfile) diff --git a/lib/rucio/common/utils.py b/lib/rucio/common/utils.py index 34e7b8b013..61243ad398 100644 --- a/lib/rucio/common/utils.py +++ b/lib/rucio/common/utils.py @@ -52,7 +52,7 @@ from rucio.common.config import config_get, config_has_section from rucio.common.exception import MissingModuleException, InvalidType, InputValidationError, MetalinkJsonParsingError, RucioException, \ - DuplicateCriteriaInDIDFilter, DIDFilterSyntaxError, InvalidAlgorithmName, PolicyPackageVersionError + DuplicateCriteriaInDIDFilter, DIDFilterSyntaxError, InvalidAlgorithmName, PolicyPackageVersionError, ConfigNotFound from rucio.common.extra import import_extras from rucio.common.types import InternalAccount, InternalScope @@ -1773,7 +1773,7 @@ def is_client(): client_mode = True else: client_mode = False - except RuntimeError: + except (RuntimeError, ConfigNotFound): # If no configuration file is found the default value should be True client_mode = True else: diff --git a/lib/rucio/tests/common.py b/lib/rucio/tests/common.py index 43b3d080b7..3efee05e24 100644 --- a/lib/rucio/tests/common.py +++ b/lib/rucio/tests/common.py @@ -22,6 +22,8 @@ from random import choice, choices from string import ascii_uppercase, ascii_letters, digits from typing import Optional +from os import rename +from functools import wraps import pytest import requests @@ -29,6 +31,7 @@ from rucio.common.config import config_get, config_get_bool, get_config_dirs from rucio.common.utils import generate_uuid as uuid, execute + skip_rse_tests_with_accounts = pytest.mark.skipif(not any(os.path.exists(os.path.join(d, 'rse-accounts.cfg')) for d in get_config_dirs()), reason='fails if no rse-accounts.cfg found') skiplimitedsql = pytest.mark.skipif('RDBMS' in os.environ and os.environ['RDBMS'] == 'sqlite', @@ -241,4 +244,24 @@ def load_test_conf_file(file_name): return json.load(f) +def remove_config(func): + @wraps(func) + def wrapper(*args, **kwargs): + for configfile in get_config_dirs(): + # Rename the config to .tmp + try: + rename(f"{configfile}rucio.cfg", f"{configfile}rucio.cfg.tmp") + except FileNotFoundError: + pass # When a test uses a os.env assigned conf, there's nothing stating the default location has something + try: + # Execute the test + func(*args, **kwargs) + finally: + # And put the config back + for configfile in get_config_dirs(): + rename(f"{configfile}rucio.cfg.tmp", f"{configfile}rucio.cfg") + + return wrapper + + RSE_namedtuple = namedtuple('RSE_namedtuple', ['name', 'id']) diff --git a/tests/test_clients.py b/tests/test_clients.py index 8aad23db46..2cb3ef291a 100644 --- a/tests/test_clients.py +++ b/tests/test_clients.py @@ -14,16 +14,11 @@ # limitations under the License. from datetime import datetime, timedelta - -from os import rename - import pytest -from rucio.client.baseclient import BaseClient -from rucio.client.client import Client -from rucio.common.config import config_get, config_set, Config from rucio.common.exception import CannotAuthenticate, ClientProtocolNotSupported, RucioException from rucio.common.utils import execute +from rucio.tests.common import remove_config from tests.mocks.mock_http_server import MockServer @@ -32,6 +27,7 @@ def client_token_path_override(file_config_mock, function_scope_prefix, tmp_path """ Ensure each running client has a different path for the token, otherwise tests cannot run in parallel """ + from rucio.common.config import config_set config_set('client', 'auth_token_file_path', str(tmp_path / f'{function_scope_prefix}token')) @@ -39,6 +35,8 @@ def client_token_path_override(file_config_mock, function_scope_prefix, tmp_path class TestBaseClient: """ To test Clients""" + from rucio.common.config import config_get + cacert = config_get('test', 'cacert') usercert = config_get('test', 'usercert') userkey = config_get('test', 'userkey') @@ -46,23 +44,30 @@ class TestBaseClient: def testUserpass(self, vo): """ CLIENTS (BASECLIENT): authenticate with userpass.""" creds = {'username': 'ddmlab', 'password': 'secret'} + from rucio.client.baseclient import BaseClient client = BaseClient(account='root', ca_cert=self.cacert, auth_type='userpass', creds=creds, vo=vo) print(client) def testUserpassWrongCreds(self, vo): """ CLIENTS (BASECLIENT): try to authenticate with wrong username.""" creds = {'username': 'wrong', 'password': 'secret'} + from rucio.client.baseclient import BaseClient + with pytest.raises(CannotAuthenticate): BaseClient(account='root', ca_cert=self.cacert, auth_type='userpass', creds=creds, vo=vo) def testUserpassNoCACert(self, vo): """ CLIENTS (BASECLIENT): authenticate with userpass without ca cert.""" creds = {'username': 'wrong', 'password': 'secret'} + from rucio.client.baseclient import BaseClient + with pytest.raises(CannotAuthenticate): BaseClient(account='root', auth_type='userpass', creds=creds, vo=vo) def testx509(self, vo): """ CLIENTS (BASECLIENT): authenticate with x509.""" + from rucio.client.baseclient import BaseClient + creds = {'client_cert': self.usercert, 'client_key': self.userkey} BaseClient(account='root', ca_cert=self.cacert, auth_type='x509', creds=creds, vo=vo) @@ -70,17 +75,22 @@ def testx509(self, vo): def testx509NonExistingCert(self, vo): """ CLIENTS (BASECLIENT): authenticate with x509 with missing certificate.""" creds = {'client_cert': '/opt/rucio/etc/web/notthere.crt'} + from rucio.client.baseclient import BaseClient + with pytest.raises(CannotAuthenticate): BaseClient(account='root', ca_cert=self.cacert, auth_type='x509', creds=creds, vo=vo) def testClientProtocolNotSupported(self, vo): """ CLIENTS (BASECLIENT): try to pass an host with a not supported protocol.""" creds = {'username': 'ddmlab', 'password': 'secret'} + from rucio.client.baseclient import BaseClient + with pytest.raises(ClientProtocolNotSupported): BaseClient(rucio_host='localhost', auth_host='junk://localhost', account='root', auth_type='userpass', creds=creds, vo=vo) def testRetryOn502AlwaysFail(self, vo): """ CLIENTS (BASECLIENT): Ensure client retries on 502 error codes, but fails on repeated errors""" + from rucio.client.baseclient import BaseClient class AlwaysFailWith502(MockServer.Handler): def do_GET(self): @@ -90,6 +100,7 @@ def do_GET(self): with pytest.raises(CannotAuthenticate): creds = {'username': 'ddmlab', 'password': 'secret'} BaseClient(rucio_host=server.base_url, auth_host=server.base_url, account='root', auth_type='userpass', creds=creds, vo=vo) + with pytest.raises(RucioException): creds = {'client_cert': self.usercert, 'client_key': self.userkey} @@ -98,6 +109,7 @@ def do_GET(self): def testRetryOn502SucceedsEventually(self, vo): """ CLIENTS (BASECLIENT): Ensure client retries on 502 error codes""" invocations = [] + from rucio.client.baseclient import BaseClient class FailTwiceWith502(MockServer.Handler): def do_GET(self, invocations=invocations): @@ -121,18 +133,22 @@ def do_GET(self, invocations=invocations): class TestRucioClients: """ To test Clients""" - cacert = config_get('test', 'cacert') marker = '$> ' def test_ping(self, vo): """ PING (CLIENT): Ping Rucio """ + creds = {'username': 'ddmlab', 'password': 'secret'} - client = Client(account='root', ca_cert=self.cacert, auth_type='userpass', creds=creds, vo=vo) + from rucio.client.client import Client + from rucio.common.config import config_get + cacert = config_get('test', 'cacert') + client = Client(account='root', ca_cert=cacert, auth_type='userpass', creds=creds, vo=vo) print(client.ping()) @pytest.mark.noparallel(reason='We temporarily remove the config file.') + @remove_config def test_import_without_config_file(self, vo): """ The Client should be importable without a config file, since it is @@ -142,13 +158,47 @@ def test_import_without_config_file(self, vo): thus have to disable the access to it (move it) and make sure to run the code in a different environment. """ - configfile = Config().configfile - rename(configfile, f"{configfile}.tmp") - try: - exitcode, _, err = execute("python -c 'from rucio.client import Client'") - print(exitcode, err) - assert exitcode == 0 - assert "RuntimeError: Could not load Rucio configuration file." not in err - finally: - # This is utterly important to not mess up the environment. - rename(f"{configfile}.tmp", configfile) + + exitcode, _, err = execute("python -c 'from rucio.client import Client'") + print(exitcode, err) + assert exitcode == 0 + assert "Could not load Rucio configuration file." not in err + + @pytest.mark.noparallel(reason='We temporarily remove the config file.') + @remove_config + def test_run_client_no_config(self, vo): + rucio_host = "https://rucio:443" + auth_host = "https://rucio:443" + ca_cert = "/etc/grid-security/certificates/5fca1cb1.0" + creds = {'username': 'ddmlab', 'password': 'secret'} + + from rucio.client.client import Client + + client = Client( + rucio_host=rucio_host, + auth_host=auth_host, + ca_cert=ca_cert, + account='root', + auth_type='userpass', + creds=creds, + vo=vo) + + # Couple of basic calls + # account + assert client.whoami()['account'] == 'root' + assert client.get_account('jdoe')['account'] == "jdoe" + + # config + assert client.set_config_option("mock_section", "mock_option", value=0) + + # did + assert list(client.list_dids(scope='mock', filters={})) is not None + + # replica + assert list(client.list_replicas(dids=[])) is not None + + # rse + assert list(client.list_rses()) is not None + + # rule + assert list(client.list_replication_rules()) is not None