pegasus-wms.worker 5.1.2__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.
- Pegasus/__init__.py +1 -0
- Pegasus/cli/__init__.py +1 -0
- Pegasus/cli/pegasus-checkpoint.py +317 -0
- Pegasus/cli/pegasus-globus-online-init.py +103 -0
- Pegasus/cli/pegasus-globus-online.py +394 -0
- Pegasus/cli/pegasus-integrity.py +629 -0
- Pegasus/cli/pegasus-s3.py +15 -0
- Pegasus/cli/pegasus-transfer.py +38 -0
- Pegasus/s3.py +996 -0
- Pegasus/tools/__init__.py +1 -0
- Pegasus/tools/worker_utils.py +324 -0
- Pegasus/transfer.py +5458 -0
- pegasus_wms_worker-5.1.2.dist-info/METADATA +65 -0
- pegasus_wms_worker-5.1.2.dist-info/RECORD +17 -0
- pegasus_wms_worker-5.1.2.dist-info/WHEEL +5 -0
- pegasus_wms_worker-5.1.2.dist-info/licenses/LICENSE +201 -0
- pegasus_wms_worker-5.1.2.dist-info/top_level.txt +1 -0
Pegasus/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
Pegasus/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import importlib
|
|
4
|
+
import logging
|
|
5
|
+
import logging.handlers
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import signal
|
|
9
|
+
import sys
|
|
10
|
+
import tarfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Iterable, List, Set
|
|
15
|
+
|
|
16
|
+
# PEGASUS_PYTHONPATH is set by the pegasus-python-wrapper script
|
|
17
|
+
peg_path = os.environ.get("PEGASUS_PYTHONPATH")
|
|
18
|
+
if peg_path:
|
|
19
|
+
for p in reversed(peg_path.split(":")):
|
|
20
|
+
if p not in sys.path:
|
|
21
|
+
sys.path.insert(0, p)
|
|
22
|
+
|
|
23
|
+
ptransfer_module = importlib.import_module("Pegasus.cli.pegasus-transfer")
|
|
24
|
+
pegasus_transfer = ptransfer_module.pegasus_transfer
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger("pegasus-checkpoint")
|
|
27
|
+
|
|
28
|
+
# name of archived & compressed checkpoints
|
|
29
|
+
CHECKPOINT_FILENAME = "pegasus.checkpoint.tar.gz"
|
|
30
|
+
|
|
31
|
+
# pegasus-transfer url file expected to be generated by pegasus-lite
|
|
32
|
+
PEGASUS_TRANSFER_URL_FILE = "pegasus_checkpoint_transfer_urls.json"
|
|
33
|
+
|
|
34
|
+
# file to which this process's PID will be written upon startup
|
|
35
|
+
PEGASUS_CHECKPOINT_PID_FILE = "pegasus_checkpoint.pid"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_args(args: List[str] = sys.argv[1:]) -> argparse.Namespace:
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
description="Periodically transfer checkpoints back to staging site."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"-p",
|
|
45
|
+
"--pattern",
|
|
46
|
+
action="append",
|
|
47
|
+
dest="patterns",
|
|
48
|
+
metavar="PATTERN",
|
|
49
|
+
help="""regex patterns to match when searching for files to checkpoint""",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def validate_interval(value: str) -> int:
|
|
53
|
+
"""
|
|
54
|
+
Ensure that given interval is an integer and is nonzero.
|
|
55
|
+
|
|
56
|
+
:param value: the given interval to validate
|
|
57
|
+
:type value: str
|
|
58
|
+
:raises argparse.ArgumentTypeError: interval cannot be casted to an int or is a negative integer
|
|
59
|
+
:return: the given value
|
|
60
|
+
:rtype: int
|
|
61
|
+
"""
|
|
62
|
+
is_valid = True
|
|
63
|
+
try:
|
|
64
|
+
value = int(value)
|
|
65
|
+
except ValueError:
|
|
66
|
+
is_valid = False
|
|
67
|
+
|
|
68
|
+
if not is_valid or value < 0:
|
|
69
|
+
raise argparse.ArgumentTypeError(
|
|
70
|
+
f"interval {value} must be a nonnegative integer"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"-i",
|
|
77
|
+
"--interval",
|
|
78
|
+
type=validate_interval,
|
|
79
|
+
metavar="SECONDS",
|
|
80
|
+
help="""interval in seconds at which pegasus-checkpoint will send
|
|
81
|
+
checkpoint file back to the staging site
|
|
82
|
+
""",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# TODO: add --once to indicate pegasus-checkpoint is to run CheckpointWorker.run once
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"-d", "--debug", action="store_true", default=False, help="enable debug logging"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"-l",
|
|
93
|
+
"--log-to-file",
|
|
94
|
+
action="store_true",
|
|
95
|
+
default=False,
|
|
96
|
+
help="enable logging to file pegasus-checkpoint.log",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return parser.parse_args(args)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def configure_logging(level: int, log_to_file: bool):
|
|
103
|
+
"""
|
|
104
|
+
Setup logging.
|
|
105
|
+
|
|
106
|
+
:param level: log level to use; set to :code:`logging.DEBUG` to enable debug logging
|
|
107
|
+
:type level: int
|
|
108
|
+
:param log_to_file: whether or not to write logs to the file pegasus-checkpoint.log
|
|
109
|
+
:type log_to_file: bool
|
|
110
|
+
"""
|
|
111
|
+
if level == logging.DEBUG:
|
|
112
|
+
level = logging.DEBUG
|
|
113
|
+
else:
|
|
114
|
+
level = logging.INFO
|
|
115
|
+
|
|
116
|
+
handlers = [logging.StreamHandler()]
|
|
117
|
+
if log_to_file:
|
|
118
|
+
handlers.append(
|
|
119
|
+
logging.handlers.RotatingFileHandler(
|
|
120
|
+
filename="pegasus-checkpoint.log",
|
|
121
|
+
mode="a",
|
|
122
|
+
maxBytes=(1 << 20),
|
|
123
|
+
backupCount=2,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
logging.basicConfig(
|
|
128
|
+
level=level,
|
|
129
|
+
format="%(asctime)s %(threadName)s [%(levelname)s] %(message)s",
|
|
130
|
+
handlers=handlers,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def write_pid():
|
|
135
|
+
"""Write PID to :code:`PEGASUS_CHECKPOINT_PID_FILE`"""
|
|
136
|
+
with open(PEGASUS_CHECKPOINT_PID_FILE, "w") as f:
|
|
137
|
+
pid = os.getpid()
|
|
138
|
+
f.write(str(pid))
|
|
139
|
+
|
|
140
|
+
log.debug(f"started with PID: {pid}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class PeriodicCheckpointNotifier(threading.Thread):
|
|
144
|
+
"""
|
|
145
|
+
Thread to periodically signal to the worker thread to start saving and
|
|
146
|
+
staging out checkpoint files.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
log = logging.getLogger("pegasus-checkpoint.PeriodicCheckcpointNotifier")
|
|
150
|
+
|
|
151
|
+
def __init__(self, interval: int, notify: threading.Event):
|
|
152
|
+
"""Constructor
|
|
153
|
+
|
|
154
|
+
:param interval: interval in seconds to sleep for
|
|
155
|
+
:type interval: int
|
|
156
|
+
:param notify: event to set, which will notify checkpoint worker thread to wake up and run
|
|
157
|
+
:type notify: threading.Event
|
|
158
|
+
"""
|
|
159
|
+
super().__init__(group=None, target=None, name="PeriodicCheckpointNotifier")
|
|
160
|
+
self.interval = interval
|
|
161
|
+
self.notify = notify
|
|
162
|
+
|
|
163
|
+
def run(self):
|
|
164
|
+
"""
|
|
165
|
+
Repeatedly sleep for the given interval, then notify checkpoint worker
|
|
166
|
+
thread to start working upon waking up.
|
|
167
|
+
"""
|
|
168
|
+
self.log.debug(f"{self.name} started with interval: {self.interval} seconds")
|
|
169
|
+
while True:
|
|
170
|
+
time.sleep(self.interval)
|
|
171
|
+
self.notify.set()
|
|
172
|
+
self.log.debug("notify event set")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class CheckpointWorker(threading.Thread):
|
|
176
|
+
"""Thread to handle saving and staging out checkpoint files."""
|
|
177
|
+
|
|
178
|
+
log = logging.getLogger("pegasus-checkpoint.CheckpointWorker")
|
|
179
|
+
|
|
180
|
+
def __init__(self, notify: threading.Event, patterns: List[str]):
|
|
181
|
+
"""Constructor
|
|
182
|
+
|
|
183
|
+
:param notify: event to watch for, which will indicate that this thread needs to run
|
|
184
|
+
:type notify: threading.Event
|
|
185
|
+
:param patterns: regex file patterns to match
|
|
186
|
+
:type patterns: List[str]
|
|
187
|
+
"""
|
|
188
|
+
super().__init__(group=None, target=None, name="CheckpointWorker")
|
|
189
|
+
|
|
190
|
+
self.notify = notify
|
|
191
|
+
self.patterns = patterns
|
|
192
|
+
|
|
193
|
+
def run(self):
|
|
194
|
+
"""
|
|
195
|
+
Wait for notification to start working. Once notified, create the
|
|
196
|
+
checkpoint archive file and transfer back to staging site with either
|
|
197
|
+
pegasus-transfer or condor_chirp.
|
|
198
|
+
self.log.debug("{} started".format(self.name))
|
|
199
|
+
"""
|
|
200
|
+
while True:
|
|
201
|
+
# wait till we are notified
|
|
202
|
+
self.notify.wait(timeout=None)
|
|
203
|
+
self.log.debug("got notification, starting work")
|
|
204
|
+
|
|
205
|
+
# work
|
|
206
|
+
matched_files = CheckpointWorker.get_matched_filenames(self.patterns)
|
|
207
|
+
|
|
208
|
+
CheckpointWorker.archive_and_compress(matched_files)
|
|
209
|
+
|
|
210
|
+
# TODO: add exception handling
|
|
211
|
+
pegasus_transfer(
|
|
212
|
+
max_attempts=3,
|
|
213
|
+
num_threads=8,
|
|
214
|
+
file=PEGASUS_TRANSFER_URL_FILE,
|
|
215
|
+
symlink=True,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# clear the notification, so we can wait until it is set again
|
|
219
|
+
self.notify.clear()
|
|
220
|
+
self.log.debug("work done, cleared notification")
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def get_matched_filenames(patterns: List[str]) -> Set[str]:
|
|
224
|
+
"""
|
|
225
|
+
Get all files/folders in the current working directory that match the
|
|
226
|
+
given file patterns.
|
|
227
|
+
|
|
228
|
+
:param patterns: regex patterns to match filenames with
|
|
229
|
+
:type patterns: List[str]
|
|
230
|
+
:return: all files and or folders which match one of the given patterns
|
|
231
|
+
:rtype: Set[str]
|
|
232
|
+
"""
|
|
233
|
+
patterns = [re.compile(p) for p in patterns]
|
|
234
|
+
files_in_cwd = [str(f) for f in Path(".").iterdir()]
|
|
235
|
+
matched = set()
|
|
236
|
+
|
|
237
|
+
# n^2.. maybe can do a little better by pulling out files from files_in_cwd (using a set
|
|
238
|
+
# isntead) as we make matches so we don't try to iterate over it again
|
|
239
|
+
for p in patterns:
|
|
240
|
+
for f in files_in_cwd:
|
|
241
|
+
is_match = bool(p.fullmatch(f))
|
|
242
|
+
|
|
243
|
+
if is_match:
|
|
244
|
+
matched.add(f)
|
|
245
|
+
|
|
246
|
+
CheckpointWorker.log.debug(
|
|
247
|
+
"pattern: {:<30}, file: {:>20}, match: {}".format(
|
|
248
|
+
p.pattern, f, is_match
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
CheckpointWorker.log.info(
|
|
253
|
+
f"given patterns matched the following filenames: {matched}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return matched
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def archive_and_compress(filenames: Iterable[str]):
|
|
260
|
+
"""
|
|
261
|
+
Given a list of file/folder names in CWD, archive and compress them into a single file
|
|
262
|
+
:code:`pegasus.checkpoint.tar.gz`
|
|
263
|
+
|
|
264
|
+
:param filenames: list of filenames to archive and compress
|
|
265
|
+
:type filenames: Iterable[str]
|
|
266
|
+
"""
|
|
267
|
+
start = time.perf_counter()
|
|
268
|
+
with tarfile.open(CHECKPOINT_FILENAME, "w|gz") as tar:
|
|
269
|
+
for f in filenames:
|
|
270
|
+
tar.add(name=f, recursive=True)
|
|
271
|
+
end = time.perf_counter()
|
|
272
|
+
|
|
273
|
+
checkpoint_size_in_mb = Path(CHECKPOINT_FILENAME).stat().st_size / (1 << 20)
|
|
274
|
+
|
|
275
|
+
CheckpointWorker.log.info(
|
|
276
|
+
"created {} MB checkpoint file: {}".format(
|
|
277
|
+
checkpoint_size_in_mb, CHECKPOINT_FILENAME
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
CheckpointWorker.log.info(f"archive and gzip took {end - start} seconds")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
args = parse_args()
|
|
285
|
+
|
|
286
|
+
log_level = logging.INFO
|
|
287
|
+
if args.debug:
|
|
288
|
+
log_level = logging.DEBUG
|
|
289
|
+
|
|
290
|
+
configure_logging(level=log_level, log_to_file=args.log_to_file)
|
|
291
|
+
|
|
292
|
+
write_pid()
|
|
293
|
+
|
|
294
|
+
# event to signal to worker thread to start saving checkpoints
|
|
295
|
+
notify = threading.Event()
|
|
296
|
+
|
|
297
|
+
# main thread to handle signal from user application
|
|
298
|
+
def SIGUSR1_handler(signum, frame):
|
|
299
|
+
log.info("SIGUSR1 received, setting notification")
|
|
300
|
+
notify.set()
|
|
301
|
+
|
|
302
|
+
signal.signal(signal.SIGUSR1, SIGUSR1_handler)
|
|
303
|
+
|
|
304
|
+
# create and start a periodic checkpoint notifier if user specified an interval to run on
|
|
305
|
+
periodic_notifier = None
|
|
306
|
+
if args.interval:
|
|
307
|
+
periodic_notifier = PeriodicCheckpointNotifier(
|
|
308
|
+
interval=args.interval, notify=notify
|
|
309
|
+
)
|
|
310
|
+
periodic_notifier.start()
|
|
311
|
+
|
|
312
|
+
worker = CheckpointWorker(notify=notify, patterns=args.patterns)
|
|
313
|
+
worker.start()
|
|
314
|
+
|
|
315
|
+
if periodic_notifier:
|
|
316
|
+
periodic_notifier.join()
|
|
317
|
+
worker.join()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from argparse import ArgumentParser
|
|
5
|
+
|
|
6
|
+
import globus_sdk
|
|
7
|
+
from globus_sdk.scopes import (
|
|
8
|
+
GCSCollectionScopeBuilder,
|
|
9
|
+
GCSEndpointScopeBuilder,
|
|
10
|
+
TransferScopes,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from configparser import ConfigParser
|
|
15
|
+
except Exception:
|
|
16
|
+
from ConfigParser import ConfigParser
|
|
17
|
+
|
|
18
|
+
client_id = "d7382f5a-4ea3-4b69-b094-99c392fc820d"
|
|
19
|
+
config_file = os.path.expanduser("~/.pegasus/globus.conf")
|
|
20
|
+
|
|
21
|
+
parser = ArgumentParser(
|
|
22
|
+
description="Initialize Globus OAuth Tokens - SDK Version {}".format(
|
|
23
|
+
globus_sdk.__version__
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"-p", "--permanent", action="store_true", help="request a refreshable token",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"-e",
|
|
31
|
+
"--endpoints",
|
|
32
|
+
nargs="*",
|
|
33
|
+
default=[],
|
|
34
|
+
help="list of endpoint uuids to acquire manage_collections consent",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"-c",
|
|
38
|
+
"--collections",
|
|
39
|
+
nargs="*",
|
|
40
|
+
default=[],
|
|
41
|
+
help="list of collection uuids to acquire data_access consent",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"-d",
|
|
45
|
+
"--domains",
|
|
46
|
+
nargs="*",
|
|
47
|
+
default=[],
|
|
48
|
+
help="list of domain requirements which must be satisfied by the identities under the globus user account",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
|
|
54
|
+
config = ConfigParser()
|
|
55
|
+
config.add_section("oauth")
|
|
56
|
+
config.set("oauth", "client_id", client_id)
|
|
57
|
+
|
|
58
|
+
pegasus_scopes = [TransferScopes.all]
|
|
59
|
+
for c in args.collections:
|
|
60
|
+
sb = GCSCollectionScopeBuilder(c, known_scopes=[TransferScopes.all])
|
|
61
|
+
pegasus_scopes.append(f"{TransferScopes.all}[*{sb.data_access}]")
|
|
62
|
+
pegasus_scopes.append(sb.data_access)
|
|
63
|
+
|
|
64
|
+
for e in args.endpoints:
|
|
65
|
+
sb = GCSEndpointScopeBuilder(e)
|
|
66
|
+
pegasus_scopes.append(sb.manage_collections)
|
|
67
|
+
|
|
68
|
+
required_domains = None
|
|
69
|
+
for d in args.domains:
|
|
70
|
+
if not required_domains:
|
|
71
|
+
required_domains = [d]
|
|
72
|
+
else:
|
|
73
|
+
required_domains.append(d)
|
|
74
|
+
|
|
75
|
+
client = globus_sdk.NativeAppAuthClient(client_id)
|
|
76
|
+
if args.permanent:
|
|
77
|
+
client.oauth2_start_flow(refresh_tokens=True, requested_scopes=pegasus_scopes)
|
|
78
|
+
else:
|
|
79
|
+
client.oauth2_start_flow(requested_scopes=pegasus_scopes)
|
|
80
|
+
|
|
81
|
+
authorize_url = client.oauth2_get_authorize_url(
|
|
82
|
+
session_required_single_domain=required_domains, prompt="login"
|
|
83
|
+
)
|
|
84
|
+
print(f"Please go to this URL and login: {authorize_url}")
|
|
85
|
+
|
|
86
|
+
get_input = getattr(__builtins__, "raw_input", input)
|
|
87
|
+
auth_code = get_input("Please enter the code you get after login here: ").strip()
|
|
88
|
+
token_response = client.oauth2_exchange_code_for_tokens(auth_code)
|
|
89
|
+
|
|
90
|
+
transfer_data = token_response.by_resource_server["transfer.api.globus.org"]
|
|
91
|
+
|
|
92
|
+
# get tokens as strings and their expiration timestamp
|
|
93
|
+
transfer_at = str(transfer_data["access_token"])
|
|
94
|
+
transfer_rt = str(transfer_data["refresh_token"])
|
|
95
|
+
transfer_at_exp = str(transfer_data["expires_at_seconds"])
|
|
96
|
+
|
|
97
|
+
config.set("oauth", "transfer_at", transfer_at) # transfer access token
|
|
98
|
+
config.set("oauth", "transfer_rt", transfer_rt) # transfer refresh token
|
|
99
|
+
config.set("oauth", "transfer_at_exp", transfer_at_exp) # transfer expiration time
|
|
100
|
+
|
|
101
|
+
cfg = open(config_file, "w+")
|
|
102
|
+
config.write(cfg)
|
|
103
|
+
cfg.close()
|