rucio-clients 35.7.0__py3-none-any.whl → 37.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rucio-clients might be problematic. Click here for more details.
- rucio/alembicrevision.py +1 -1
- rucio/cli/__init__.py +14 -0
- rucio/cli/account.py +216 -0
- rucio/cli/bin_legacy/__init__.py +13 -0
- rucio_clients-35.7.0.data/scripts/rucio → rucio/cli/bin_legacy/rucio.py +769 -486
- rucio_clients-35.7.0.data/scripts/rucio-admin → rucio/cli/bin_legacy/rucio_admin.py +476 -423
- rucio/cli/command.py +272 -0
- rucio/cli/config.py +72 -0
- rucio/cli/did.py +191 -0
- rucio/cli/download.py +128 -0
- rucio/cli/lifetime_exception.py +33 -0
- rucio/cli/replica.py +162 -0
- rucio/cli/rse.py +293 -0
- rucio/cli/rule.py +158 -0
- rucio/cli/scope.py +40 -0
- rucio/cli/subscription.py +73 -0
- rucio/cli/upload.py +60 -0
- rucio/cli/utils.py +226 -0
- rucio/client/accountclient.py +0 -1
- rucio/client/baseclient.py +33 -24
- rucio/client/client.py +45 -1
- rucio/client/didclient.py +5 -3
- rucio/client/downloadclient.py +6 -8
- rucio/client/replicaclient.py +0 -2
- rucio/client/richclient.py +317 -0
- rucio/client/rseclient.py +4 -4
- rucio/client/uploadclient.py +26 -12
- rucio/common/bittorrent.py +234 -0
- rucio/common/cache.py +66 -29
- rucio/common/checksum.py +168 -0
- rucio/common/client.py +122 -0
- rucio/common/config.py +22 -35
- rucio/common/constants.py +61 -3
- rucio/common/didtype.py +72 -24
- rucio/common/exception.py +65 -8
- rucio/common/extra.py +5 -10
- rucio/common/logging.py +13 -13
- rucio/common/pcache.py +8 -7
- rucio/common/plugins.py +59 -27
- rucio/common/policy.py +12 -3
- rucio/common/schema/__init__.py +84 -34
- rucio/common/schema/generic.py +0 -17
- rucio/common/schema/generic_multi_vo.py +0 -17
- rucio/common/test_rucio_server.py +12 -6
- rucio/common/types.py +132 -52
- rucio/common/utils.py +93 -643
- rucio/rse/__init__.py +3 -3
- rucio/rse/protocols/bittorrent.py +11 -1
- rucio/rse/protocols/cache.py +0 -11
- rucio/rse/protocols/dummy.py +0 -11
- rucio/rse/protocols/gfal.py +14 -9
- rucio/rse/protocols/globus.py +1 -1
- rucio/rse/protocols/http_cache.py +1 -1
- rucio/rse/protocols/posix.py +2 -2
- rucio/rse/protocols/protocol.py +84 -317
- rucio/rse/protocols/rclone.py +2 -1
- rucio/rse/protocols/rfio.py +10 -1
- rucio/rse/protocols/ssh.py +2 -1
- rucio/rse/protocols/storm.py +2 -13
- rucio/rse/protocols/webdav.py +74 -30
- rucio/rse/protocols/xrootd.py +2 -1
- rucio/rse/rsemanager.py +170 -53
- rucio/rse/translation.py +260 -0
- rucio/vcsversion.py +4 -4
- rucio/version.py +7 -0
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/etc/rucio.cfg.atlas.client.template +3 -2
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/etc/rucio.cfg.template +3 -19
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/requirements.client.txt +11 -7
- rucio_clients-37.0.0.data/scripts/rucio +133 -0
- rucio_clients-37.0.0.data/scripts/rucio-admin +97 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/METADATA +18 -14
- rucio_clients-37.0.0.dist-info/RECORD +104 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/licenses/AUTHORS.rst +3 -0
- rucio/common/schema/atlas.py +0 -413
- rucio/common/schema/belleii.py +0 -408
- rucio/common/schema/domatpc.py +0 -401
- rucio/common/schema/escape.py +0 -426
- rucio/common/schema/icecube.py +0 -406
- rucio/rse/protocols/gsiftp.py +0 -92
- rucio_clients-35.7.0.dist-info/RECORD +0 -88
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/etc/rse-accounts.cfg.template +0 -0
- {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/rucio_client/merge_rucio_configs.py +0 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/WHEEL +0 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/licenses/LICENSE +0 -0
- {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/top_level.txt +0 -0
rucio/cli/scope.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from rucio.cli.bin_legacy.rucio_admin import add_scope, list_scopes
|
|
17
|
+
from rucio.cli.utils import Arguments
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.group()
|
|
21
|
+
def scope():
|
|
22
|
+
"""Interact with scopes - a logical grouping of DIDs"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@scope.command("add")
|
|
26
|
+
@click.argument("scope-name")
|
|
27
|
+
@click.option("-a", "--account", help="Associated account", required=True)
|
|
28
|
+
@click.pass_context
|
|
29
|
+
def add_(ctx, account, scope_name):
|
|
30
|
+
"""Add a new scope with name [SCOPE-NAME]"""
|
|
31
|
+
args = Arguments({"scope": scope_name, "account": account})
|
|
32
|
+
add_scope(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@scope.command("list")
|
|
36
|
+
@click.option("-a", "--account", help="Filter by associated account", required=False)
|
|
37
|
+
@click.pass_context
|
|
38
|
+
def list_(ctx, account):
|
|
39
|
+
"""List existing scopes"""
|
|
40
|
+
list_scopes(Arguments({"account": account}), ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from rucio.cli.bin_legacy.rucio_admin import add_subscription, list_subscriptions, reevaluate_did_for_subscription, update_subscription
|
|
17
|
+
from rucio.cli.utils import Arguments
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.group()
|
|
21
|
+
def subscription():
|
|
22
|
+
"The methods for automated and regular processing of some specific rules"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@subscription.command("show")
|
|
26
|
+
@click.option("-a", "--account", help="Account associated with the subscription")
|
|
27
|
+
@click.option("--long", default=False, is_flag=True, help="Show extended information about the subscription")
|
|
28
|
+
@click.argument("subscription-name")
|
|
29
|
+
@click.pass_context
|
|
30
|
+
def list_(ctx, subscription_name, account, long):
|
|
31
|
+
"""Show the attributes of a subscription [SUBSCRIPTION-NAME]"""
|
|
32
|
+
args = Arguments({"subs_account": account, "name": subscription_name, "long": long})
|
|
33
|
+
list_subscriptions(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@subscription.command("update")
|
|
37
|
+
@click.argument("subscription-name")
|
|
38
|
+
@click.option("--filter", "did_filter", help='Json serializable DID filter (eg \'{"scope": ["tests"], "project": ["data12_8TeV"]}\')', required=True)
|
|
39
|
+
@click.option("--rule", help='List of replication rules (eg \'[{"activity": "Functional Tests", "copies": 2, "rse_expression": "tier=2", "lifetime": 3600, "weight": "mou"}]\')', required=True)
|
|
40
|
+
@click.option("--comment", help="Comments on subscription")
|
|
41
|
+
@click.option("--lifetime", type=int, help="Subscription lifetime (in days)")
|
|
42
|
+
@click.option("--account", help="Account name")
|
|
43
|
+
@click.option("--priority", help="The priority of the subscription")
|
|
44
|
+
@click.pass_context
|
|
45
|
+
def update(ctx, subscription_name, did_filter, rule, comment, lifetime, account, priority):
|
|
46
|
+
"""Update a subscription [SUBSCRIPTION-NAME] to have new properties"""
|
|
47
|
+
args = Arguments({"name": subscription_name, "filter": did_filter, "replication_rules": rule, "comments": comment, "lifetime": lifetime, "subs_account": account, "priority": priority})
|
|
48
|
+
update_subscription(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@subscription.command("add")
|
|
52
|
+
@click.argument("subscription-name")
|
|
53
|
+
@click.option("--filter", "did_filter", help='Json serializable DID filter (eg \'{"scope": ["tests"], "project": ["data12_8TeV"]}\')', required=True)
|
|
54
|
+
@click.option("--rule", help='List of replication rules (eg \'[{"activity": "Functional Tests", "copies": 2, "rse_expression": "tier=2", "lifetime": 3600, "weight": "mou"}]\')', required=True)
|
|
55
|
+
@click.option("--comment", help="Comments on subscription")
|
|
56
|
+
@click.option("--lifetime", type=int, help="Subscription lifetime (in days)")
|
|
57
|
+
@click.option("--account", help="Account name")
|
|
58
|
+
@click.option("--priority", help="The priority of the subscription")
|
|
59
|
+
@click.pass_context
|
|
60
|
+
def add_(ctx, subscription_name, did_filter, rule, comment, lifetime, account, priority):
|
|
61
|
+
"""Create a new subscription with the name [SUBSCRIPTION-NAME]"""
|
|
62
|
+
args = Arguments({"name": subscription_name, "filter": did_filter, "replication_rules": rule, "comments": comment, "lifetime": lifetime, "subs_account": account, "priority": priority})
|
|
63
|
+
add_subscription(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@subscription.command("touch")
|
|
67
|
+
@click.argument("dids", nargs=-1)
|
|
68
|
+
@click.pass_context
|
|
69
|
+
def touch(ctx, dids):
|
|
70
|
+
"""Reevaluate list of DIDs against all active subscriptions"""
|
|
71
|
+
# TODO make reeval accept dids as a list
|
|
72
|
+
dids = ",".join(dids)
|
|
73
|
+
reevaluate_did_for_subscription(Arguments({"dids": dids}), ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
|
rucio/cli/upload.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from rucio.cli.bin_legacy.rucio import upload
|
|
17
|
+
from rucio.cli.utils import Arguments
|
|
18
|
+
from rucio.common.config import config_get_float
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.command("upload")
|
|
22
|
+
@click.argument("file-paths", nargs=-1)
|
|
23
|
+
@click.option("--rse", "--rse-name", help="Rucio Storage Element (RSE) name", required=True)
|
|
24
|
+
@click.option("--lifetime", type=int, help="Lifetime of the rule in seconds")
|
|
25
|
+
@click.option("--expiration-date", help="The date when the rule expires in UTC, format: <year>-<month>-<day>-<hour>:<minute>:<second>. E.g. 2022-10-20-20:00:00")
|
|
26
|
+
@click.option("--scope", help="Scope name.")
|
|
27
|
+
@click.option("--impl", type=click.Choice([]), help="Transfer protocol implementation to use (e.g: xrootd, gfal.NoRename, webdav, ssh.Rsync, rclone)")
|
|
28
|
+
# The --no-register option is hidden. This is pilot ONLY. Users should not use this. Will lead to unregistered data on storage!
|
|
29
|
+
@click.option("--no-register", is_flag=True, default=False, hidden=True)
|
|
30
|
+
@click.option("--register-after-upload", is_flag=True, default=False, help="Register the file _only_ after successful upload")
|
|
31
|
+
@click.option("--summary", is_flag=True, default=False, help="Create rucio_upload.json summary file")
|
|
32
|
+
@click.option("--guid", help="Manually specify the GUID for the file.")
|
|
33
|
+
@click.option("--protocol", help="Force the protocol to use")
|
|
34
|
+
@click.option("--pfn", help="Specify the exact PFN for the upload")
|
|
35
|
+
@click.option("--lfn", help="Specify the exact LFN for the upload")
|
|
36
|
+
@click.option("--transfer-timeout", type=float, default=config_get_float("upload", "transfer_timeout", False, 360), help="Transfer timeout (in seconds)")
|
|
37
|
+
@click.option("-r", "--recursive", is_flag=True, default=False, help="Convert recursively the folder structure into collections")
|
|
38
|
+
@click.pass_context
|
|
39
|
+
def upload_command(ctx, file_paths, rse, lifetime, expiration_date, scope, impl, no_register, register_after_upload, summary, guid, protocol, pfn, lfn, transfer_timeout, recursive):
|
|
40
|
+
"""Upload file(s) to a Rucio RSE"""
|
|
41
|
+
args = Arguments(
|
|
42
|
+
{
|
|
43
|
+
"args": file_paths,
|
|
44
|
+
"rse": rse,
|
|
45
|
+
"lifetime": lifetime,
|
|
46
|
+
"expiration_date": expiration_date,
|
|
47
|
+
"scope": scope,
|
|
48
|
+
"impl": impl,
|
|
49
|
+
"no_register": no_register,
|
|
50
|
+
"register_after_upload": register_after_upload,
|
|
51
|
+
"protocol": protocol,
|
|
52
|
+
"summary": summary,
|
|
53
|
+
"guid": guid,
|
|
54
|
+
"pfn": pfn,
|
|
55
|
+
"name": lfn,
|
|
56
|
+
"transfer_timeout": transfer_timeout,
|
|
57
|
+
"recursive": recursive,
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
upload(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
|
rucio/cli/utils.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import errno
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import signal
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import traceback
|
|
21
|
+
from configparser import NoOptionError, NoSectionError
|
|
22
|
+
from functools import wraps
|
|
23
|
+
|
|
24
|
+
from rucio.client.client import Client
|
|
25
|
+
from rucio.common.config import config_get
|
|
26
|
+
from rucio.common.exception import (
|
|
27
|
+
AccessDenied,
|
|
28
|
+
CannotAuthenticate,
|
|
29
|
+
DataIdentifierAlreadyExists,
|
|
30
|
+
DataIdentifierNotFound,
|
|
31
|
+
Duplicate,
|
|
32
|
+
DuplicateContent,
|
|
33
|
+
InvalidObject,
|
|
34
|
+
InvalidRSEExpression,
|
|
35
|
+
MissingDependency,
|
|
36
|
+
RSENotFound,
|
|
37
|
+
RucioException,
|
|
38
|
+
RuleNotFound,
|
|
39
|
+
UnsupportedOperation,
|
|
40
|
+
)
|
|
41
|
+
from rucio.common.utils import setup_logger
|
|
42
|
+
|
|
43
|
+
SUCCESS = 0
|
|
44
|
+
FAILURE = 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def exception_handler(function):
|
|
48
|
+
verbosity = ("-v" in sys.argv) or ("--verbose" in sys.argv)
|
|
49
|
+
logger = setup_logger(module_name=__name__, logger_name="user", verbose=verbosity)
|
|
50
|
+
|
|
51
|
+
@wraps(function)
|
|
52
|
+
def new_funct(*args, **kwargs):
|
|
53
|
+
try:
|
|
54
|
+
return function(*args, **kwargs)
|
|
55
|
+
except NotImplementedError as error:
|
|
56
|
+
logger.error(f"Cannot run that operation/command combination {error}")
|
|
57
|
+
return FAILURE
|
|
58
|
+
except InvalidObject as error:
|
|
59
|
+
logger.error(error)
|
|
60
|
+
return error.error_code
|
|
61
|
+
except DataIdentifierNotFound as error:
|
|
62
|
+
logger.error(error)
|
|
63
|
+
logger.debug("This means that the Data IDentifier you provided is not known by Rucio.")
|
|
64
|
+
return error.error_code
|
|
65
|
+
except AccessDenied as error:
|
|
66
|
+
logger.error(error)
|
|
67
|
+
logger.debug("This error is a permission issue. You cannot run this command with your account.")
|
|
68
|
+
return error.error_code
|
|
69
|
+
except DataIdentifierAlreadyExists as error:
|
|
70
|
+
logger.error(error)
|
|
71
|
+
logger.debug("This means that the Data IDentifier you try to add is already registered in Rucio.")
|
|
72
|
+
return error.error_code
|
|
73
|
+
except RSENotFound as error:
|
|
74
|
+
logger.error(error)
|
|
75
|
+
logger.debug("This means that the Rucio Storage Element you provided is not known by Rucio.")
|
|
76
|
+
return error.error_code
|
|
77
|
+
except InvalidRSEExpression as error:
|
|
78
|
+
logger.error(error)
|
|
79
|
+
logger.debug("This means the RSE expression you provided is not syntactically correct.")
|
|
80
|
+
return error.error_code
|
|
81
|
+
except DuplicateContent as error:
|
|
82
|
+
logger.error(error)
|
|
83
|
+
logger.debug("This means that the DID you want to attach is already in the target DID.")
|
|
84
|
+
return error.error_code
|
|
85
|
+
except Duplicate as error:
|
|
86
|
+
logger.error(error)
|
|
87
|
+
logger.debug("This means that you are trying to add something that already exists.")
|
|
88
|
+
return error.error_code
|
|
89
|
+
except TypeError as error:
|
|
90
|
+
logger.error(error)
|
|
91
|
+
logger.debug("This means the parameter you passed has a wrong type.")
|
|
92
|
+
return FAILURE
|
|
93
|
+
except RuleNotFound as error:
|
|
94
|
+
logger.error(error)
|
|
95
|
+
logger.debug("This means the rule you specified does not exist.")
|
|
96
|
+
return error.error_code
|
|
97
|
+
except UnsupportedOperation as error:
|
|
98
|
+
logger.error(error)
|
|
99
|
+
logger.debug("This means you cannot change the status of the DID.")
|
|
100
|
+
return error.error_code
|
|
101
|
+
except MissingDependency as error:
|
|
102
|
+
logger.error(error)
|
|
103
|
+
logger.debug("This means one dependency is missing.")
|
|
104
|
+
return error.error_code
|
|
105
|
+
except KeyError as error:
|
|
106
|
+
if "x-rucio-auth-token" in str(error):
|
|
107
|
+
used_account = None
|
|
108
|
+
try: # get the configured account from the configuration file
|
|
109
|
+
used_account = "%s (from rucio.cfg)" % config_get("client", "account")
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
try: # are we overriden by the environment?
|
|
113
|
+
used_account = "%s (from RUCIO_ACCOUNT)" % os.environ["RUCIO_ACCOUNT"]
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
logger.error("Specified account %s does not have an associated identity." % used_account)
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
logger.debug(traceback.format_exc())
|
|
120
|
+
contact = config_get("policy", "support", raise_exception=False)
|
|
121
|
+
support = ("Please follow up with all relevant information at: " + contact) if contact else ""
|
|
122
|
+
logger.error("\nThe object is missing this property: %s\n" 'This should never happen. Please rerun the last command with the "-v" option to gather more information.\n' "%s" % (str(error), support))
|
|
123
|
+
return FAILURE
|
|
124
|
+
except RucioException as error:
|
|
125
|
+
logger.error(error)
|
|
126
|
+
return error.error_code
|
|
127
|
+
except Exception as error:
|
|
128
|
+
if isinstance(error, IOError) and getattr(error, "errno", None) == errno.EPIPE:
|
|
129
|
+
# Ignore Broken Pipe
|
|
130
|
+
# While in python3 we can directly catch 'BrokenPipeError', in python2 it doesn't exist.
|
|
131
|
+
|
|
132
|
+
# Python flushes standard streams on exit; redirect remaining output
|
|
133
|
+
# to devnull to avoid another BrokenPipeError at shutdown
|
|
134
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
135
|
+
os.dup2(devnull, sys.stdout.fileno())
|
|
136
|
+
return SUCCESS
|
|
137
|
+
logger.debug(traceback.format_exc())
|
|
138
|
+
logger.error(error)
|
|
139
|
+
contact = config_get("policy", "support", raise_exception=False)
|
|
140
|
+
support = ("If it's a problem concerning your experiment or if you're unsure what to do, please follow up at: %s\n" % contact) if contact else ""
|
|
141
|
+
contact = config_get("policy", "support_rucio", default="https://github.com/rucio/rucio/issues")
|
|
142
|
+
support += "If you're sure there is a problem with Rucio itself, please follow up at: " + contact
|
|
143
|
+
logger.error("\nRucio exited with an unexpected/unknown error.\n" 'Please rerun the last command with the "-v" option to gather more information.\n' "%s" % support)
|
|
144
|
+
return FAILURE
|
|
145
|
+
|
|
146
|
+
return new_funct
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_client(args, logger):
|
|
150
|
+
"""
|
|
151
|
+
Returns a new client object.
|
|
152
|
+
"""
|
|
153
|
+
if hasattr(args, "config") and (args.config is not None):
|
|
154
|
+
os.environ["RUCIO_CONFIG"] = args.config
|
|
155
|
+
|
|
156
|
+
if logger is None:
|
|
157
|
+
logger = setup_logger(module_name=__name__, logger_name="user", verbose=args.verbose)
|
|
158
|
+
|
|
159
|
+
if not args.auth_strategy:
|
|
160
|
+
if "RUCIO_AUTH_TYPE" in os.environ:
|
|
161
|
+
auth_type = os.environ["RUCIO_AUTH_TYPE"].lower()
|
|
162
|
+
else:
|
|
163
|
+
try:
|
|
164
|
+
auth_type = config_get("client", "auth_type").lower()
|
|
165
|
+
except (NoOptionError, NoSectionError):
|
|
166
|
+
logger.error("Cannot get AUTH_TYPE")
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
else:
|
|
169
|
+
auth_type = args.auth_strategy.lower()
|
|
170
|
+
|
|
171
|
+
if auth_type in ["userpass", "saml"] and args.username is not None and args.password is not None:
|
|
172
|
+
creds = {"username": args.username, "password": args.password}
|
|
173
|
+
elif auth_type == "oidc":
|
|
174
|
+
if args.oidc_issuer:
|
|
175
|
+
args.oidc_issuer = args.oidc_issuer.lower()
|
|
176
|
+
creds = {
|
|
177
|
+
"oidc_auto": args.oidc_auto,
|
|
178
|
+
"oidc_scope": args.oidc_scope,
|
|
179
|
+
"oidc_audience": args.oidc_audience,
|
|
180
|
+
"oidc_polling": args.oidc_polling,
|
|
181
|
+
"oidc_refresh_lifetime": args.oidc_refresh_lifetime,
|
|
182
|
+
"oidc_issuer": args.oidc_issuer,
|
|
183
|
+
"oidc_username": args.oidc_username,
|
|
184
|
+
"oidc_password": args.oidc_password,
|
|
185
|
+
}
|
|
186
|
+
elif auth_type == "x509":
|
|
187
|
+
creds = {"client_cert": args.certificate, "client_key": args.client_key}
|
|
188
|
+
else:
|
|
189
|
+
creds = None
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
client = Client(rucio_host=args.host, auth_host=args.auth_host, account=args.issuer, auth_type=auth_type, creds=creds, ca_cert=args.ca_certificate, timeout=args.timeout, user_agent=args.user_agent, vo=args.vo, logger=logger)
|
|
193
|
+
except CannotAuthenticate as error:
|
|
194
|
+
logger.error(error)
|
|
195
|
+
if "alert certificate expired" in str(error):
|
|
196
|
+
logger.error("The server certificate expired.")
|
|
197
|
+
elif auth_type.lower() == "x509_proxy":
|
|
198
|
+
logger.error("Please verify that your proxy is still valid and renew it if needed.")
|
|
199
|
+
sys.exit(1)
|
|
200
|
+
return client
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def signal_handler(sig, frame, logger):
|
|
204
|
+
logger.warning("You pressed Ctrl+C! Exiting gracefully")
|
|
205
|
+
child_processes = subprocess.Popen("ps -o pid --ppid %s --noheaders" % os.getpid(), shell=True, stdout=subprocess.PIPE)
|
|
206
|
+
child_processes = child_processes.stdout.read() # type: ignore
|
|
207
|
+
for pid in child_processes.split("\n")[:-1]: # type: ignore
|
|
208
|
+
try:
|
|
209
|
+
os.kill(int(pid), signal.SIGTERM)
|
|
210
|
+
except Exception:
|
|
211
|
+
print("Cannot kill child process")
|
|
212
|
+
sys.exit(1)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def setup_gfal2_logger():
|
|
216
|
+
gfal2_logger = logging.getLogger("gfal2")
|
|
217
|
+
gfal2_logger.setLevel(logging.CRITICAL)
|
|
218
|
+
gfal2_logger.addHandler(logging.StreamHandler())
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class Arguments(dict):
|
|
222
|
+
"""dot.notation access to dictionary attributes"""
|
|
223
|
+
|
|
224
|
+
__getattr__ = dict.get
|
|
225
|
+
__setattr__ = dict.__setitem__
|
|
226
|
+
__delattr__ = dict.__delitem__
|
rucio/client/accountclient.py
CHANGED
|
@@ -200,7 +200,6 @@ class AccountClient(BaseClient):
|
|
|
200
200
|
:param account: The account name.
|
|
201
201
|
:param identity: The identity key name. For example x509 DN, or a username.
|
|
202
202
|
:param authtype: The type of the authentication (x509, gss, userpass).
|
|
203
|
-
:param default: If True, the account should be used by default with the provided identity.
|
|
204
203
|
"""
|
|
205
204
|
|
|
206
205
|
data = dumps({'identity': identity, 'authtype': authtype})
|
rucio/client/baseclient.py
CHANGED
|
@@ -22,13 +22,11 @@ import os
|
|
|
22
22
|
import secrets
|
|
23
23
|
import sys
|
|
24
24
|
import time
|
|
25
|
-
from collections.abc import Generator
|
|
26
25
|
from configparser import NoOptionError, NoSectionError
|
|
27
|
-
from logging import Logger
|
|
28
26
|
from os import environ, fdopen, geteuid, makedirs, path
|
|
29
27
|
from shutil import move
|
|
30
28
|
from tempfile import mkstemp
|
|
31
|
-
from typing import Any, Optional
|
|
29
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
32
30
|
from urllib.parse import urlparse
|
|
33
31
|
|
|
34
32
|
import requests
|
|
@@ -40,10 +38,14 @@ from requests.status_codes import codes
|
|
|
40
38
|
from rucio import version
|
|
41
39
|
from rucio.common import exception
|
|
42
40
|
from rucio.common.config import config_get, config_get_bool, config_get_int
|
|
43
|
-
from rucio.common.exception import CannotAuthenticate, ClientProtocolNotSupported, ConfigNotFound, MissingClientParameter, MissingModuleException, NoAuthInformation, ServerConnectionException
|
|
41
|
+
from rucio.common.exception import CannotAuthenticate, ClientProtocolNotFound, ClientProtocolNotSupported, ConfigNotFound, MissingClientParameter, MissingModuleException, NoAuthInformation, ServerConnectionException
|
|
44
42
|
from rucio.common.extra import import_extras
|
|
45
43
|
from rucio.common.utils import build_url, get_tmp_dir, my_key_generator, parse_response, setup_logger, ssh_sign
|
|
46
44
|
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from collections.abc import Generator
|
|
47
|
+
from logging import Logger
|
|
48
|
+
|
|
47
49
|
EXTRA_MODULES = import_extras(['requests_kerberos'])
|
|
48
50
|
|
|
49
51
|
if EXTRA_MODULES['requests_kerberos']:
|
|
@@ -90,7 +92,7 @@ class BaseClient:
|
|
|
90
92
|
timeout: Optional[int] = 600,
|
|
91
93
|
user_agent: Optional[str] = 'rucio-clients',
|
|
92
94
|
vo: Optional[str] = None,
|
|
93
|
-
logger: Logger = LOG) -> None:
|
|
95
|
+
logger: 'Logger' = LOG) -> None:
|
|
94
96
|
"""
|
|
95
97
|
Constructor of the BaseClient.
|
|
96
98
|
:param rucio_host: The address of the rucio server, if None it is read from the config file.
|
|
@@ -155,11 +157,18 @@ class BaseClient:
|
|
|
155
157
|
rucio_scheme = urlparse(self.host).scheme
|
|
156
158
|
auth_scheme = urlparse(self.auth_host).scheme
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
rucio_scheme_allowed = ['http', 'https']
|
|
161
|
+
auth_scheme_allowed = ['http', 'https']
|
|
162
|
+
|
|
163
|
+
if not rucio_scheme:
|
|
164
|
+
raise ClientProtocolNotFound(host=self.host, protocols_allowed=rucio_scheme_allowed)
|
|
165
|
+
elif rucio_scheme not in rucio_scheme_allowed:
|
|
166
|
+
raise ClientProtocolNotSupported(host=self.host, protocol=rucio_scheme, protocols_allowed=rucio_scheme_allowed)
|
|
160
167
|
|
|
161
|
-
if
|
|
162
|
-
raise
|
|
168
|
+
if not auth_scheme:
|
|
169
|
+
raise ClientProtocolNotFound(host=self.auth_host, protocols_allowed=auth_scheme_allowed)
|
|
170
|
+
elif auth_scheme not in auth_scheme_allowed:
|
|
171
|
+
raise ClientProtocolNotSupported(host=self.auth_host, protocol=auth_scheme, protocols_allowed=auth_scheme_allowed)
|
|
163
172
|
|
|
164
173
|
if (rucio_scheme == 'https' or auth_scheme == 'https') and ca_cert is None:
|
|
165
174
|
self.logger.debug('HTTPS is required, but no ca_cert was passed. Trying to get it from X509_CERT_DIR.')
|
|
@@ -364,7 +373,7 @@ class BaseClient:
|
|
|
364
373
|
else:
|
|
365
374
|
return exception.RucioException, "%s: %s" % (exc_cls, exc_msg)
|
|
366
375
|
|
|
367
|
-
def _load_json_data(self, response: requests.Response) -> Generator[Any, Any, Any]:
|
|
376
|
+
def _load_json_data(self, response: requests.Response) -> 'Generator[Any, Any, Any]':
|
|
368
377
|
"""
|
|
369
378
|
Helper method to correctly load json data based on the content type of the http response.
|
|
370
379
|
|
|
@@ -517,7 +526,7 @@ class BaseClient:
|
|
|
517
526
|
self.auth_token = result.headers['x-rucio-auth-token']
|
|
518
527
|
return True
|
|
519
528
|
|
|
520
|
-
def
|
|
529
|
+
def __refresh_token_oidc(self) -> bool:
|
|
521
530
|
"""
|
|
522
531
|
Checks if there is active refresh token and if so returns
|
|
523
532
|
either active token with expiration timestamp or requests a new
|
|
@@ -571,7 +580,7 @@ class BaseClient:
|
|
|
571
580
|
\nRucio Auth Server when attempting token refresh.")
|
|
572
581
|
return False
|
|
573
582
|
|
|
574
|
-
def
|
|
583
|
+
def __get_token_oidc(self) -> bool:
|
|
575
584
|
"""
|
|
576
585
|
First authenticates the user via a Identity Provider server
|
|
577
586
|
(with user's username & password), by specifying oidc_scope,
|
|
@@ -600,14 +609,14 @@ class BaseClient:
|
|
|
600
609
|
request_auth_url = build_url(self.auth_host, path='auth/oidc')
|
|
601
610
|
# requesting authorization URL specific to the user & Rucio OIDC Client
|
|
602
611
|
self.logger.debug("Initial auth URL request headers %s to files" % str(headers))
|
|
603
|
-
|
|
604
|
-
self.logger.debug("Response headers %s and text %s" % (str(
|
|
612
|
+
oidc_auth_res = self._send_request(request_auth_url, headers=headers, get_token=True)
|
|
613
|
+
self.logger.debug("Response headers %s and text %s" % (str(oidc_auth_res.headers), str(oidc_auth_res.text)))
|
|
605
614
|
# with the obtained authorization URL we will contact the Identity Provider to get to the login page
|
|
606
|
-
if 'X-Rucio-OIDC-Auth-URL' not in
|
|
615
|
+
if 'X-Rucio-OIDC-Auth-URL' not in oidc_auth_res.headers:
|
|
607
616
|
print("Rucio Client did not succeed to get AuthN/Z URL from the Rucio Auth Server. \
|
|
608
617
|
\nThis could be due to wrongly requested/configured scope, audience or issuer.")
|
|
609
618
|
return False
|
|
610
|
-
auth_url =
|
|
619
|
+
auth_url = oidc_auth_res.headers['X-Rucio-OIDC-Auth-URL']
|
|
611
620
|
if not self.creds['oidc_auto']:
|
|
612
621
|
print("\nPlease use your internet browser, go to:")
|
|
613
622
|
print("\n " + auth_url + " \n")
|
|
@@ -692,7 +701,7 @@ class BaseClient:
|
|
|
692
701
|
with fdopen(file_d, "w") as f_exp_epoch:
|
|
693
702
|
f_exp_epoch.write(str(self.token_exp_epoch))
|
|
694
703
|
move(file_n, self.token_exp_epoch_file)
|
|
695
|
-
self.
|
|
704
|
+
self.__refresh_token_oidc()
|
|
696
705
|
return True
|
|
697
706
|
|
|
698
707
|
def __get_token_x509(self) -> bool:
|
|
@@ -832,11 +841,11 @@ class BaseClient:
|
|
|
832
841
|
url = build_url(self.auth_host, path='auth/saml')
|
|
833
842
|
|
|
834
843
|
result = None
|
|
835
|
-
|
|
836
|
-
if
|
|
837
|
-
return
|
|
838
|
-
|
|
839
|
-
result = self._send_request(
|
|
844
|
+
saml_auth_result = self._send_request(url, get_token=True)
|
|
845
|
+
if saml_auth_result.headers['X-Rucio-Auth-Token']:
|
|
846
|
+
return saml_auth_result.headers['X-Rucio-Auth-Token']
|
|
847
|
+
saml_auth_url = saml_auth_result.headers['X-Rucio-SAML-Auth-URL']
|
|
848
|
+
result = self._send_request(saml_auth_url, type_='POST', data=userpass, verify=False)
|
|
840
849
|
result = self._send_request(url, get_token=True)
|
|
841
850
|
|
|
842
851
|
if not result:
|
|
@@ -868,7 +877,7 @@ class BaseClient:
|
|
|
868
877
|
raise CannotAuthenticate('x509 authentication failed for account=%s with identity=%s' % (self.account,
|
|
869
878
|
self.creds))
|
|
870
879
|
elif self.auth_type == 'oidc':
|
|
871
|
-
if not self.
|
|
880
|
+
if not self.__get_token_oidc():
|
|
872
881
|
raise CannotAuthenticate('OIDC authentication failed for account=%s' % self.account)
|
|
873
882
|
|
|
874
883
|
elif self.auth_type == 'gss':
|
|
@@ -912,7 +921,7 @@ class BaseClient:
|
|
|
912
921
|
except Exception:
|
|
913
922
|
raise
|
|
914
923
|
if self.auth_oidc_refresh_active and self.auth_type == 'oidc':
|
|
915
|
-
self.
|
|
924
|
+
self.__refresh_token_oidc()
|
|
916
925
|
self.logger.debug('got token from file')
|
|
917
926
|
return True
|
|
918
927
|
|
rucio/client/client.py
CHANGED
|
@@ -57,7 +57,51 @@ class Client(AccountClient,
|
|
|
57
57
|
DiracClient,
|
|
58
58
|
LifetimeClient):
|
|
59
59
|
|
|
60
|
-
"""
|
|
60
|
+
"""
|
|
61
|
+
Main client class for accessing Rucio resources. Handles the authentication.
|
|
62
|
+
|
|
63
|
+
Note:
|
|
64
|
+
Used to access all client methods. Each entity client *can* be used to access methods, but using the main client class is recommended for ease of use.
|
|
65
|
+
|
|
66
|
+
For using general methods -
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
from rucio.client import Client
|
|
71
|
+
|
|
72
|
+
client = Client() # authenticate with config or environ settings
|
|
73
|
+
client.add_replication_rule(...)
|
|
74
|
+
|
|
75
|
+
client = Client(
|
|
76
|
+
rucio_host = "my_host",
|
|
77
|
+
auth_host = "my_auth_host",
|
|
78
|
+
account = "jdoe12345",
|
|
79
|
+
auth_type = "userpass",
|
|
80
|
+
creds = {
|
|
81
|
+
"username": "jdoe12345",
|
|
82
|
+
"password": "******",
|
|
83
|
+
}
|
|
84
|
+
) # authenticate with kwargs
|
|
85
|
+
client.list_replicas(...)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For using the upload and download clients -
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
from rucio.client import Client
|
|
92
|
+
from rucio.client.uploadclient import UploadClient
|
|
93
|
+
from rucio.client.downloadclient import DownloadClient
|
|
94
|
+
|
|
95
|
+
client = Client(...) # Initialize a client using your preferred method
|
|
96
|
+
|
|
97
|
+
upload_client = UploadClient(client) # Pass forward the initialized client
|
|
98
|
+
upload_client.upload(items=...)
|
|
99
|
+
|
|
100
|
+
download_client = DownloadClient(client)
|
|
101
|
+
download_client.download_dids(items=...)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
"""
|
|
61
105
|
|
|
62
106
|
def __init__(self, **args):
|
|
63
107
|
"""
|
rucio/client/didclient.py
CHANGED
|
@@ -538,14 +538,16 @@ class DIDClient(BaseClient):
|
|
|
538
538
|
def get_metadata_bulk(
|
|
539
539
|
self,
|
|
540
540
|
dids: "Sequence[Mapping[str, Any]]",
|
|
541
|
-
inherit: bool = False
|
|
541
|
+
inherit: bool = False,
|
|
542
|
+
plugin: str = "JSON",
|
|
542
543
|
) -> "Iterator[dict[str, Any]]":
|
|
543
544
|
"""
|
|
544
545
|
Bulk get data identifier metadata
|
|
545
|
-
:param inherit: A boolean. If set to true, the metadata of the parent are concatenated.
|
|
546
546
|
:param dids: A list of dids.
|
|
547
|
+
:param inherit: A boolean. If set to true, the metadata of the parent are concatenated.
|
|
548
|
+
:param plugin: The metadata plugin to query, 'ALL' for all available plugins
|
|
547
549
|
"""
|
|
548
|
-
data = {'dids': dids, 'inherit': inherit}
|
|
550
|
+
data = {'dids': dids, 'inherit': inherit, 'plugin': plugin}
|
|
549
551
|
path = '/'.join([self.DIDS_BASEURL, 'bulkmeta'])
|
|
550
552
|
url = build_url(choice(self.list_hosts), path=path)
|
|
551
553
|
r = self._send_request(url, type_='POST', data=dumps(data))
|