rda-python-dsquasar 2.0.12__tar.gz → 3.0.3__tar.gz

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.
Files changed (23) hide show
  1. rda_python_dsquasar-3.0.3/MANIFEST.in +1 -0
  2. rda_python_dsquasar-3.0.3/PKG-INFO +87 -0
  3. rda_python_dsquasar-3.0.3/README.md +70 -0
  4. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/pyproject.toml +1 -1
  5. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar/ds_quasar.py +2 -2
  6. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar/dsquasar.py +184 -118
  7. rda_python_dsquasar-3.0.3/src/rda_python_dsquasar/dsquasar.usg +128 -0
  8. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar/dstacc.py +2 -2
  9. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar/taccrec.py +1 -1
  10. rda_python_dsquasar-3.0.3/src/rda_python_dsquasar.egg-info/PKG-INFO +87 -0
  11. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar.egg-info/SOURCES.txt +2 -0
  12. rda_python_dsquasar-2.0.12/PKG-INFO +0 -18
  13. rda_python_dsquasar-2.0.12/README.md +0 -1
  14. rda_python_dsquasar-2.0.12/src/rda_python_dsquasar.egg-info/PKG-INFO +0 -18
  15. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/LICENSE +0 -0
  16. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/setup.cfg +0 -0
  17. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar/__init__.py +0 -0
  18. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar/tacctar.py +0 -0
  19. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar.egg-info/dependency_links.txt +0 -0
  20. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar.egg-info/entry_points.txt +0 -0
  21. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar.egg-info/requires.txt +0 -0
  22. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/src/rda_python_dsquasar.egg-info/top_level.txt +0 -0
  23. {rda_python_dsquasar-2.0.12 → rda_python_dsquasar-3.0.3}/test/test_dsquasar.py +0 -0
@@ -0,0 +1 @@
1
+ include src/rda_python_dsquasar/*.usg
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: rda_python_dsquasar
3
+ Version: 3.0.3
4
+ Summary: RDA Python package to backup and recover RDA data archives to and from GLOBUS Quasar backup server
5
+ Author-email: Zaihua Ji <zji@ucar.edu>
6
+ Project-URL: Homepage, https://github.com/NCAR/rda-python-dsquasar
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: rda_python_common
15
+ Requires-Dist: rda_python_dsarch
16
+ Dynamic: license-file
17
+
18
+ # rda_python_dsquasar
19
+
20
+ RDA Python package to back up and recover RDA data archives to and from the
21
+ GLOBUS Quasar backup server.
22
+
23
+ ## Programs
24
+
25
+ The package installs the following command-line utilities, all running as the
26
+ current user:
27
+
28
+ - `dsquasar` — back up and restore RDA dataset archives via Quasar
29
+ - `tacctar` — create tar bundles for Quasar archival
30
+ - `taccrec` — recover content from Quasar tar bundles
31
+
32
+ Run any program with `--help` (or `-h`) for full usage details.
33
+
34
+ ## Environment setup
35
+
36
+ Create a Python environment first; package installs in the next section run
37
+ inside whichever environment you activate here.
38
+
39
+ ### Option A — Python venv (DECS machines)
40
+
41
+ ```bash
42
+ python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
43
+ source $ENVHOME/bin/activate
44
+ ```
45
+
46
+ ### Option B — Conda (DAV/Casper)
47
+
48
+ ```bash
49
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
50
+ conda activate $ENVHOME
51
+ ```
52
+
53
+ ## Installing rda-python-dsquasar
54
+
55
+ Pick whichever install mode fits your workflow. All variants pull in the
56
+ transitive dependencies (`rda_python_common`, `rda_python_dsarch`)
57
+ automatically.
58
+
59
+ For local development, clone this repo alongside your project and install it
60
+ in editable mode so that changes are picked up without re-installing:
61
+
62
+ ```bash
63
+ git clone https://github.com/NCAR/rda-python-dsquasar.git
64
+ cd rda-python-dsquasar
65
+ pip install -e .
66
+ ```
67
+
68
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
69
+ `-b/--branch` to `git clone`:
70
+
71
+ ```bash
72
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-dsquasar.git
73
+ cd rda-python-dsquasar
74
+ pip install -e .
75
+ ```
76
+
77
+ For a regular (non-editable) install from a checkout:
78
+
79
+ ```bash
80
+ pip install /path/to/rda-python-dsquasar
81
+ ```
82
+
83
+ For a production install on a system that uses the published distribution:
84
+
85
+ ```bash
86
+ pip install rda_python_dsquasar
87
+ ```
@@ -0,0 +1,70 @@
1
+ # rda_python_dsquasar
2
+
3
+ RDA Python package to back up and recover RDA data archives to and from the
4
+ GLOBUS Quasar backup server.
5
+
6
+ ## Programs
7
+
8
+ The package installs the following command-line utilities, all running as the
9
+ current user:
10
+
11
+ - `dsquasar` — back up and restore RDA dataset archives via Quasar
12
+ - `tacctar` — create tar bundles for Quasar archival
13
+ - `taccrec` — recover content from Quasar tar bundles
14
+
15
+ Run any program with `--help` (or `-h`) for full usage details.
16
+
17
+ ## Environment setup
18
+
19
+ Create a Python environment first; package installs in the next section run
20
+ inside whichever environment you activate here.
21
+
22
+ ### Option A — Python venv (DECS machines)
23
+
24
+ ```bash
25
+ python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
26
+ source $ENVHOME/bin/activate
27
+ ```
28
+
29
+ ### Option B — Conda (DAV/Casper)
30
+
31
+ ```bash
32
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
33
+ conda activate $ENVHOME
34
+ ```
35
+
36
+ ## Installing rda-python-dsquasar
37
+
38
+ Pick whichever install mode fits your workflow. All variants pull in the
39
+ transitive dependencies (`rda_python_common`, `rda_python_dsarch`)
40
+ automatically.
41
+
42
+ For local development, clone this repo alongside your project and install it
43
+ in editable mode so that changes are picked up without re-installing:
44
+
45
+ ```bash
46
+ git clone https://github.com/NCAR/rda-python-dsquasar.git
47
+ cd rda-python-dsquasar
48
+ pip install -e .
49
+ ```
50
+
51
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
52
+ `-b/--branch` to `git clone`:
53
+
54
+ ```bash
55
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-dsquasar.git
56
+ cd rda-python-dsquasar
57
+ pip install -e .
58
+ ```
59
+
60
+ For a regular (non-editable) install from a checkout:
61
+
62
+ ```bash
63
+ pip install /path/to/rda-python-dsquasar
64
+ ```
65
+
66
+ For a production install on a system that uses the published distribution:
67
+
68
+ ```bash
69
+ pip install rda_python_dsquasar
70
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rda_python_dsquasar"
7
- version = "2.0.12"
7
+ version = "3.0.3"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -91,7 +91,7 @@ TFCOUNT = 100 # if file count is greater, use MINSIZE for tar file
91
91
  SUBLMTS = 2000 # file count limit for a sub-group
92
92
 
93
93
  PGBACK = {
94
- 'workdir' : "{}/{}/quasar_backup".format(PgLOG.PGLOG['GDEXWORK'], PgLOG.PGLOG['GDEXUSER']),
94
+ 'workdir' : "{}/{}/quasar_backup".format(PgLOG.PGLOG['GDEXWORK'], PgLOG.PGLOG['COMMONUSER']),
95
95
  'mproc' : 1,
96
96
  'action' : CTACTS,
97
97
  'chgdays' : 0,
@@ -1092,7 +1092,7 @@ def evaluate_file_stat(dsid, cate, pgrec):
1092
1092
  chome = PgLOG.PGLOG['DECSHOME'] if cate == 's' else PgLOG.PGLOG['DSDHOME']
1093
1093
 
1094
1094
  if pgrec['locflag'] == 'O':
1095
- bucket = 'gdex-decsdata' if cate == 's' else PgLOG.PGLOC['OBJCTBKT']
1095
+ bucket = 'gdex-decsdata' if cate == 's' else PgLOG.PGLOG['OBJCTBKT']
1096
1096
  ofile = PgLOG.join_paths(dsid, fname)
1097
1097
  info = PgFile.check_object_file(ofile, bucket, 1, ERRACT)
1098
1098
  if not info: return False
@@ -12,7 +12,7 @@
12
12
  # to gather Web and Saved bfiles, tar them into larger (> 2GB) bfiles, and
13
13
  # back them up onto Quasar Backup Servers, at the Globus end points of
14
14
  # NCAR GDEX Quasar and/or NCAR GDEX Quasar Drdata, for Backup and/or
15
- # desaster recovery copies, respectively. The file tarring and backing up
15
+ # disaster recovery copies, respectively. The file tarring and backing up
16
16
  # processes are completed in utility program dsarch.
17
17
  #
18
18
  # Github : https://github.com/NCAR/rda-python-dsquasar.git
@@ -84,8 +84,12 @@ class DsQuasar(PgCMD, PgSplit):
84
84
  self.ONESIZE = 20*self.PGLOG['ONEGBS'] # 20GB, minimal file size to tar a single file
85
85
  self.TFCOUNT = 100 # if file count is greater, use self.MINSIZE for tar file
86
86
  self.SUBLMTS = 2000 # file count limit for a sub-group
87
+ self.MPLIMIT = 500 # tar file count per batch process for multi-processing
88
+ self.MPMAX = 8 # maximum number of batch processes for multi-processing
89
+ self.MAXRUNTIME = 23*3600 # 23 hours; stop before the 24-hour PBS walltime
90
+ self.ONEHOUR = 3600 # seconds; time headroom needed to finish before walltime
87
91
  self.PGBACK = {
88
- 'workdir' : "{}/{}/quasar_backup".format(self.PGLOG['GDEXWORK'], self.PGLOG['GDEXUSER']),
92
+ 'workdir' : "{}/{}/quasar_backup".format(self.PGLOG['GDEXWORK'], self.PGLOG['COMMONUSER']),
89
93
  'mproc' : 1,
90
94
  'action' : self.CTACTS,
91
95
  'chgdays' : 0,
@@ -97,6 +101,8 @@ class DsQuasar(PgCMD, PgSplit):
97
101
  'maxcnt' : 10,
98
102
  'dolock' : 1,
99
103
  'doemail' : 0,
104
+ 'starttime' : 0, # wall-clock start of the run, for the PBS walltime guard
105
+ 'tardone' : 0, # tar files dispatched so far, for the finish-rate estimate
100
106
  'cmd' : None
101
107
  }
102
108
  self.dsids = []
@@ -105,12 +111,14 @@ class DsQuasar(PgCMD, PgSplit):
105
111
  self.dstart = 0
106
112
 
107
113
  # read in command line parameters
108
- def read_parameters(self):
114
+ def read_parameters(self):
109
115
  self.PGLOG['LOGFILE'] = "dsquasar.log" # set different log file
116
+ self.set_help_path(__file__)
110
117
  self.dssdb_dbname()
111
118
  option = None
112
119
  argv = sys.argv[1:]
113
120
  for arg in argv:
121
+ if re.match(r'^-(h|-help)$', arg, re.I): self.show_usage('dsquasar')
114
122
  ms = re.match(r'^-(a|b|c|d|e|E|l|m|n|t|u|A|B|D)$', arg)
115
123
  if ms:
116
124
  arg = ms.group(1)
@@ -149,32 +157,13 @@ class DsQuasar(PgCMD, PgSplit):
149
157
  self.add_to_dsids(arg)
150
158
  else:
151
159
  self.pglog(arg + ": Value without leading option", self.LGWNEX)
152
- if not (self.sopts['a'] or self.dsids):
153
- print("dsquasar [-b] [(-a|-t DatasetIDs)] [-c ChangeDays] [-(e|E)] [-(B|D)] [-A ActionBits] \\")
154
- print(" [-l (Y|N)] [-m ProcessCount] [-n] [-u] [-d [HostName] [TryCount]")
155
- print(" Option -a - gather files of all datasets for Quasar Backup, or")
156
- print(" Option -t - specify dataset IDs to backup files, wildcard % is allowed")
157
- print(" Option -b - turn the background mode on; no screen display")
158
- print(" Option -c - change days to work with option -A 1 to rebackup changed files")
159
- print(" Option -d - turn the delay mode on for running as a batch job")
160
- print(" Option -e - send an email to specialist for statistics")
161
- print(" Option -E - send an email to specialist for detail statistics")
162
- print(" Option -B - Quasar Backup only for the provided datasets, or")
163
- print(" Option -D - Quasar Backup&Drdata; default to both B & D")
164
- print(" Option -A - Action bits, 1 - create dsarch input files; 2 - build tarfiles;")
165
- print(" 4 - Transfer Quasar backup files; 8 - Check Backup Files;")
166
- print(" 16 - Dump Backup Statistics; default to 3 for 1+2 actions,")
167
- print(" and the valid values are 1,2,3,4,6,7,8,16")
168
- print(" Option -l - flag to check&lock dataset before backup processes, defaults to Y")
169
- print(" Option -m - number of concurrent processes, defaults to 1")
170
- print(" Option -n - gather and show the available file counts with no backup actions")
171
- print(" Option -u - to clean up backup locks for provided dataset IDs")
172
- sys.exit(0)
160
+ if not (self.sopts['a'] or self.dsids): self.show_usage('dsquasar')
173
161
  self.PGBACK['cmd'] = "dsquasar {}".format(' '.join(argv))
174
162
 
175
163
  # do quasar backup process
176
164
  def start_actions(self):
177
165
  self.cmdlog(self.PGBACK['cmd'])
166
+ self.PGBACK['starttime'] = tm()
178
167
  if self.sopts['u']:
179
168
  if self.sopts['a']: self.pglog("-u: Dataset IDs must be provided to Unlock datasets", self.LOGWRN)
180
169
  self.unlock_datasets()
@@ -331,6 +320,37 @@ class DsQuasar(PgCMD, PgSplit):
331
320
  self.backup_dataset_tarfiles(dsfiles, 'B')
332
321
  self.backup_dataset_tarfiles(dsfiles, 'D')
333
322
 
323
+ # size up the number of batch processes from the number of tar files that are the
324
+ # unit of parallel work: new infiles CINACT will create this run (estimated from the
325
+ # total bytes of GDEX files ready to back up, ~one tar per TARSIZE) plus existing
326
+ # status 'N' infile records to build (TARACT) and status 'T' tar records to transfer
327
+ # (BCKACT). scales one process per MPLIMIT tar files, capped at MPMAX. create-infile
328
+ # only (-A 1) is not a batch action (see NBACTS) so it never reaches here and stays
329
+ # single process.
330
+ def batch_process_count(self, acts):
331
+ tcnt = 0
332
+ if acts&self.CINACT:
333
+ sizes = [0]
334
+ if self.gather_dataset_files(None, False, sizes):
335
+ tcnt += max(1, (sizes[0] + self.TARSIZE - 1)//self.TARSIZE)
336
+ if acts&self.TARACT: tcnt += self.batch_tar_count('N')
337
+ if acts&self.BCKACT: tcnt += self.batch_tar_count('T')
338
+ if tcnt <= self.MPLIMIT: return 1
339
+ mproc = (tcnt + self.MPLIMIT - 1)//self.MPLIMIT
340
+ if mproc > self.MPMAX: mproc = self.MPMAX
341
+ return mproc
342
+
343
+ # count the bfile tar records of a given status honoring backflag/dataset scope
344
+ def batch_tar_count(self, status):
345
+ bcnd = "status = '{}'".format(status)
346
+ if self.PGBACK['backflag']: bcnd += " AND type = '{}'".format(self.PGBACK['backflag'])
347
+ if self.dsids:
348
+ tcnt = 0
349
+ for dsid in self.dsids:
350
+ tcnt += self.pgget('bfile', '', "dsid = '{}' AND {}".format(dsid, bcnd), self.LGWNEX)
351
+ return tcnt
352
+ return self.pgget('bfile', '', bcnd, self.LGWNEX)
353
+
334
354
  # start none daemon and intialize dscheck counts
335
355
  def start_dsquasar_none_daemon(self, act, fcnt = 0):
336
356
  acts = self.PGBACK['action']
@@ -338,11 +358,39 @@ class DsQuasar(PgCMD, PgSplit):
338
358
  dsid = self.dsids[0] if len(self.dsids) == 1 else ''
339
359
  if not re.match(r'^[a-z]\d{6}$', dsid): dsid = ''
340
360
  cact = ('A' if acts < 10 else '') + str(acts)
341
- if act == self.STTACT or acts&self.NBACTS != acts:
342
- self.set_one_boption('qoptions', '-l walltime=24:00:00', 1)
361
+ # only the initial delayed submit (not yet under PBS) sets qoptions for the batch
362
+ # job the dscheck daemon will start; size up multi-processing from the detail file
363
+ # count (unless an explicit -m N was given) and reserve matching PBS cpus. the
364
+ # invocation argv is left unchanged so a repeat submit with the same argv still
365
+ # blocks as a duplicate. a command-line or the PBS batch run does not set qoptions.
366
+ if self.PGLOG['CURBID'] < 1 and (act == self.STTACT or acts&self.NBACTS != acts):
367
+ # check for a duplicate command first (same lookup key init_dscheck builds);
368
+ # only size up multi-processing - which checks the backup files - when no
369
+ # matching dscheck record is on file yet, so a duplicate submit blocks without
370
+ # checking the backup files.
371
+ dargv = self.argv_to_string(sys.argv[1:], 0, "Process in Delayed Mode")
372
+ dargx = None
373
+ if len(dargv) > 100:
374
+ dargx = dargv[100:]
375
+ dargv = dargv[0:100]
376
+ if not self.get_dscheck("dsquasar", dargv, self.PGBACK['workdir'], self.PGLOG['CURUID'], dargx, self.LOGWRN):
377
+ qoptions = '-l walltime=24:00:00'
378
+ if acts&self.NBACTS != acts:
379
+ if self.PGBACK['mproc'] < 2: self.PGBACK['mproc'] = self.batch_process_count(acts)
380
+ if self.PGBACK['mproc'] > 1:
381
+ qoptions += ",select=1:ncpus={0}:mem={0}gb".format(self.PGBACK['mproc'])
382
+ self.set_one_boption('qoptions', qoptions, 1)
343
383
  self.init_dscheck(0, '', "dsquasar", dsid, cact, self.PGBACK['workdir'],
344
384
  self.PGLOG['CURUID'], self.bopts, self.LOGWRN)
345
- if self.PGBACK['mproc'] > 1: self.PGBACK['mproc'] = 1
385
+ # on the PBS batch run, take the process count from the reserved ncpus in qoptions
386
+ # instead of recomputing it
387
+ if self.PGLOG['DSCHECK'] and self.PGBACK['mproc'] < 2 and acts&self.NBACTS != acts:
388
+ pgrec = self.pgget('dscheck', 'qoptions',
389
+ "cindex = {}".format(self.PGLOG['DSCHECK']['cindex']), self.LGEREX)
390
+ if pgrec and pgrec['qoptions']:
391
+ ms = re.search(r'ncpus=(\d+)', pgrec['qoptions'])
392
+ if ms: self.PGBACK['mproc'] = int(ms.group(1))
393
+ if self.PGBACK['mproc'] > 1 and acts&self.NBACTS == acts: self.PGBACK['mproc'] = 1
346
394
  elif self.PGBACK['mproc'] > 1 and acts&self.NBACTS == acts:
347
395
  self.PGBACK['mproc'] = 1
348
396
  self.start_none_daemon('dsquasar', '', self.PGLOG['CURUID'], self.PGBACK['mproc'], 60, 1)
@@ -409,7 +457,7 @@ class DsQuasar(PgCMD, PgSplit):
409
457
  dmsg = list(bfiles)[0] if dcnt == 1 else "{} datasets".format(dcnt)
410
458
  self.pglog("{}: {} for {} ...".format(amsg, bmsg, dmsg), self.WARNLG)
411
459
  qinfo = {'backflag' : backflag, 'bid' : self.current_bid(), 'dsids' : [], 'fcnt' : 0,
412
- 'size' : 0, 'infiles' : [], 'instr' : '', 'qdsids' : [],
460
+ 'size' : 0, 'infiles' : [], 'instr' : '', 'qdsids' : [], 'abids' : [],
413
461
  'dslocks' : [], 'qfcnt' : 0, 'qsize' : 0, 'qcnt' : 0}
414
462
  for dsid in bfiles:
415
463
  if len(qinfo['dsids']) == self.DSCNT: self.process_one_backup_file(qinfo, True, False)
@@ -427,6 +475,9 @@ class DsQuasar(PgCMD, PgSplit):
427
475
  self.lock_dataset(dsid, 0, self.LGEREX)
428
476
  qinfo['dslocks'].remove(dsid)
429
477
  qinfo['dsids'] = []
478
+ if self.PGBACK['mproc'] > 1:
479
+ self.check_child(None, 0, self.LOGWRN, 1) # wait all child processes done
480
+ self.confirm_quasar_counts(qinfo, qinfo['abids'], "status = 'T'") # recount confirmed backups from RDADB
430
481
  if qinfo['dslocks']:
431
482
  for dsid in qinfo['dslocks']:
432
483
  self.lock_dataset(dsid, 0, self.LGEREX) # unlock the locked datasets
@@ -475,7 +526,7 @@ class DsQuasar(PgCMD, PgSplit):
475
526
  bmsg = self.BACKMSG[backflag]
476
527
  s = 's' if bcnt > 1 else ''
477
528
  self.pglog("{}: {} {} file{}...".format(amsg, bcnt, bmsg, s), self.WARNLG)
478
- qinfo = {'backflag' : backflag, 'bid' : 0, 'dsids' : [], 'fcnt' : 0, 'size' : 0,
529
+ qinfo = {'backflag' : backflag, 'bid' : 0, 'dsids' : [], 'fcnt' : 0, 'size' : 0, 'abids' : [],
479
530
  'infiles' : [], 'instr' : '', 'qdsids' : [], 'qfcnt' : 0, 'qsize' : 0, 'qcnt' : 0}
480
531
  for dsid in bfiles:
481
532
  if self.PGBACK['dolock'] and self.lock_dataset(dsid, 1, self.LOGERR) < 1: continue
@@ -485,6 +536,9 @@ class DsQuasar(PgCMD, PgSplit):
485
536
  for bkey in binfo: qinfo[bkey] = binfo[bkey]
486
537
  self.process_one_backup_file(qinfo, False)
487
538
  if self.PGBACK['dolock']: self.lock_dataset(dsid, 0, self.LOGERR)
539
+ if self.PGBACK['mproc'] > 1:
540
+ self.check_child(None, 0, self.LOGWRN, 1) # wait all child processes done
541
+ self.confirm_quasar_counts(qinfo, qinfo['abids'], "status = 'T'") # recount confirmed backups from RDADB
488
542
  qcnt = qinfo['qcnt']
489
543
  if qcnt > 0:
490
544
  self.PGBACK['bckcnt'] += qcnt
@@ -522,6 +576,9 @@ class DsQuasar(PgCMD, PgSplit):
522
576
  if qinfo['size'] < self.BCKSIZE: continue
523
577
  self.transfer_quasar_tarfiles(qinfo)
524
578
  if qinfo['size'] > 0: self.transfer_quasar_tarfiles(qinfo)
579
+ if self.PGBACK['mproc'] > 1:
580
+ self.check_child(None, 0, self.LOGWRN, 1) # wait all child processes done
581
+ self.confirm_quasar_counts(qinfo, list(bfiles), "status = 'A'") # recount confirmed backups from RDADB
525
582
  qcnt = qinfo['qcnt']
526
583
  if qcnt > 0:
527
584
  self.PGBACK['bckcnt'] += qcnt
@@ -562,11 +619,67 @@ class DsQuasar(PgCMD, PgSplit):
562
619
  if self.PGBACK['pstep']: self.record_dscheck_status("F")
563
620
  self.pgexit(0)
564
621
 
622
+ # guard the -A 3 (Create Input&Tar, build status 'N' infiles) and -A 4 (Transfer,
623
+ # send status 'T' tars) PBS batch jobs against the 24-hour walltime: once past
624
+ # MAXRUNTIME, if the tar files still to process cannot finish within the last hour at
625
+ # the rate achieved so far, send a progress email and stop cleanly so the report is
626
+ # not lost to a PBS timeout; the remaining records are left for the next scheduled run
627
+ # to resume. a no-op for command-line runs, other actions, or before the cutoff.
628
+ def check_batch_deadline(self, qinfo):
629
+ act = self.PGBACK['action']
630
+ if self.PGLOG['CURBID'] < 1 or act not in (self.CTACTS, self.BCKACT): return
631
+ elapsed = tm() - self.PGBACK['starttime']
632
+ if elapsed < self.MAXRUNTIME: return
633
+ status, work = ('N', 'build') if act == self.CTACTS else ('T', 'transfer')
634
+ remaining = self.batch_tar_count(status)
635
+ if remaining < 1: return
636
+ done = self.PGBACK['tardone']
637
+ need = remaining*elapsed/done if done > 0 else elapsed
638
+ if need <= self.ONEHOUR: return # enough time left to finish before the walltime
639
+ if self.PGBACK['mproc'] > 1: self.check_child(None, 0, self.LOGWRN, 1) # wait all children
640
+ if qinfo and qinfo.get('dslocks'):
641
+ for dsid in qinfo['dslocks']: self.lock_dataset(dsid, 0, self.LGEREX)
642
+ etime = self.seconds_to_string_time(int(elapsed))
643
+ amsg = self.ACTMSG[act]
644
+ msg = "{}: Stopped after {} with {} tar file(s) still to {} - not enough time to finish before the 24-hour PBS walltime; the next scheduled run will resume".format(amsg, etime, remaining, work)
645
+ self.pglog(self.INDENT + msg, self.LOGACT)
646
+ if self.PGBACK['doemail']:
647
+ bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
648
+ self.set_email("{}: {} - stopped early before the PBS walltime with {} {} tar file(s) remaining!".format(self.PGBACK['cmd'], amsg, remaining, bmsg), self.EMLTOP)
649
+ title = "dsquasar: {} stopped early ({} remaining)".format(amsg, remaining)
650
+ if self.PGBACK['errcnt']: title += " Error({})".format(self.PGBACK['errcnt'])
651
+ if self.PGLOG['DSCHECK']:
652
+ self.build_customized_email("dscheck", "einfo", "cindex = {}".format(self.PGLOG['DSCHECK']['cindex']), title, self.LOGWRN)
653
+ else:
654
+ self.pglog(title, self.LOGWRN|self.SNDEML)
655
+ if self.PGBACK['pstep']: self.record_dscheck_status("D")
656
+ self.pgexit(0)
657
+
658
+ # recompute the confirmed backup counts from RDADB after all child processes
659
+ # finished, so a multi-process summary reflects succeeded (not just started) work
660
+ def confirm_quasar_counts(self, qinfo, bids, dcnd):
661
+ qinfo['qcnt'] = qinfo['qfcnt'] = qinfo['qsize'] = 0
662
+ qinfo['qdsids'] = []
663
+ if not bids: return
664
+ cnd = "{} AND bid IN ({})".format(dcnd, ','.join(str(bid) for bid in bids))
665
+ pgrecs = self.pgmget('bfile', "dsid, dsids, data_size size, (scount + wcount) fcnt", cnd, self.LGEREX)
666
+ bcnt = len(pgrecs['dsid']) if pgrecs else 0
667
+ for i in range(bcnt):
668
+ qinfo['qcnt'] += 1
669
+ qinfo['qfcnt'] += pgrecs['fcnt'][i]
670
+ qinfo['qsize'] += pgrecs['size'][i]
671
+ dsid = pgrecs['dsid'][i]
672
+ if dsid not in qinfo['qdsids']: qinfo['qdsids'].append(dsid)
673
+ if pgrecs['dsids'][i]:
674
+ for did in pgrecs['dsids'][i].split(','):
675
+ if did and did not in qinfo['qdsids']: qinfo['qdsids'].append(did)
676
+
565
677
  # backup one Quasar Backup or Backup&Drdata from one or multiple inputs and,
566
678
  # reset the quasar backup dict
567
679
  def process_one_backup_file(self, qinfo, addback, keepid = False):
568
680
  ccnt = self.check_child(None, 0, self.LOGWRN, -1) if self.PGBACK['mproc'] > 1 else 0
569
681
  if self.PGSIG['QUIT']: self.quit_dsquasar(qinfo)
682
+ self.check_batch_deadline(qinfo)
570
683
  dsids = qinfo['dsids']
571
684
  dcnt = len(dsids)
572
685
  if dcnt == 0: return
@@ -580,6 +693,7 @@ class DsQuasar(PgCMD, PgSplit):
580
693
  else:
581
694
  bid = qinfo['bid']
582
695
  qfile = qinfo['bfile']
696
+ if 'abids' in qinfo: qinfo['abids'].append(bid) # track dispatched bids for confirmed recount
583
697
  stat = 1
584
698
  if self.PGBACK['action'] > self.CINACT:
585
699
  s = 's' if fcnt > 1 else ''
@@ -601,7 +715,7 @@ class DsQuasar(PgCMD, PgSplit):
601
715
  for infile in qinfo['infiles']: self.delete_local_file(infile)
602
716
  elif re.search(r'file backed up to', self.PGLOG['SYSERR']):
603
717
  if self.pgdel('bfile', f"bid = {bid}"):
604
- self.pglog(f"{dsid}-{qfile}: backup tarfile deleted for duplicattion", self.DTLACT)
718
+ self.pglog(f"{dsid}-{qfile}: backup tarfile deleted for duplication", self.DTLACT)
605
719
  for infile in qinfo['infiles']: self.delete_local_file(infile)
606
720
  sys.exit(0) # stop child process
607
721
  else:
@@ -614,7 +728,7 @@ class DsQuasar(PgCMD, PgSplit):
614
728
  for infile in qinfo['infiles']: self.delete_local_file(infile)
615
729
  elif re.search(r'file backed up to', self.PGLOG['SYSERR']):
616
730
  if self.pgdel('bfile', f"bid = {bid}"):
617
- self.pglog(f"{dsid}-{qfile}: backup tarfile deleted for duplicattion", self.DTLACT)
731
+ self.pglog(f"{dsid}-{qfile}: backup tarfile deleted for duplication", self.DTLACT)
618
732
  for infile in qinfo['infiles']: self.delete_local_file(infile)
619
733
  else:
620
734
  self.PGBACK['errcnt'] += 1
@@ -623,12 +737,13 @@ class DsQuasar(PgCMD, PgSplit):
623
737
  if stat:
624
738
  # reset qinfo after quasar backup
625
739
  qinfo['qcnt'] += 1
740
+ self.PGBACK['tardone'] += 1 # cumulative dispatched tars for the walltime finish-rate
626
741
  qinfo['qfcnt'] += fcnt
627
742
  qinfo['qsize'] += fsize
628
743
  for dsid in dsids:
629
744
  if dsid not in qinfo['qdsids']: qinfo['qdsids'].append(dsid)
630
745
  if addback:
631
- lastdsid = dsids.pop() if keepid else None
746
+ lastdsid = dsids.pop() if keepid else None
632
747
  for dsid in dsids: # unlock all datasets but the last one
633
748
  if dsid in qinfo['dslocks']:
634
749
  self.lock_dataset(dsid, 0, self.LGEREX)
@@ -645,6 +760,7 @@ class DsQuasar(PgCMD, PgSplit):
645
760
  def transfer_quasar_tarfiles(self, qinfo):
646
761
  ccnt = self.check_child(None, 0, self.LOGWRN, -1) if self.PGBACK['mproc'] > 1 else 0
647
762
  if self.PGSIG['QUIT']: self.quit_dsquasar(qinfo)
763
+ self.check_batch_deadline(qinfo)
648
764
  # prepare for backup one tar file
649
765
  dsids = qinfo['dsids']
650
766
  bids = qinfo['bids']
@@ -675,10 +791,10 @@ class DsQuasar(PgCMD, PgSplit):
675
791
  dstat = bstat = -1
676
792
  if ccnt == 0 or self.PGSIG['PPID'] > 1:
677
793
  if bflg == 'D':
678
- dstat = self.quasar_multiple_trasnfer(tofiles, fromfiles, 'gdex-quasar-drdata', 'gdex-glade', self.ERRACT)
794
+ dstat = self.quasar_multiple_transfer(tofiles, fromfiles, 'gdex-quasar-drdata', 'gdex-glade', self.ERRACT)
679
795
  if not dstat: self.pglog("Error Quaser Drdata for " + fmsg, self.ERRACT|self.LOGERR)
680
- bstat = self.quasar_multiple_trasnfer(tofiles, fromfiles, 'gdex-quasar', 'gdex-glade', self.ERRACT)
681
- if not dstat: self.pglog("Error Quaser Backup for " + fmsg, self.ERRACT|self.LOGERR)
796
+ bstat = self.quasar_multiple_transfer(tofiles, fromfiles, 'gdex-quasar', 'gdex-glade', self.ERRACT)
797
+ if not bstat: self.pglog("Error Quaser Backup for " + fmsg, self.ERRACT|self.LOGERR)
682
798
  if dstat == self.FINISH: dstat = self.check_globus_finished(tofiles[0], 'gdex-quasar-drdata', self.ERRACT|self.NOWAIT)
683
799
  if bstat == self.FINISH: bstat = self.check_globus_finished(tofiles[0], 'gdex-quasar', self.ERRACT|self.NOWAIT)
684
800
  if dstat and bstat:
@@ -698,6 +814,7 @@ class DsQuasar(PgCMD, PgSplit):
698
814
  if dstat and bstat:
699
815
  # reset qinfo after quasar backup
700
816
  qinfo['qcnt'] += bcnt
817
+ self.PGBACK['tardone'] += bcnt # cumulative transferred tars for the walltime finish-rate
701
818
  qinfo['qfcnt'] += fcnt
702
819
  qinfo['qsize'] += fsize
703
820
  for dsid in dsids:
@@ -777,7 +894,7 @@ class DsQuasar(PgCMD, PgSplit):
777
894
  return {'wcount' : 0, 'wrecs' : [], 'scount' : 0, 'srecs' : []}
778
895
 
779
896
  # get all files need to be backed up for a given dataset ID
780
- def get_dataset_files(self, dsid, dsfiles, backflag, logact = 0):
897
+ def get_dataset_files(self, dsid, dsfiles, backflag, logact = 0, sizes = None):
781
898
  dcnd = "dsid = '{}'".format(dsid)
782
899
  fopt = self.get_backup_options(dsid, dcnd)
783
900
  if not fopt:
@@ -817,6 +934,7 @@ class DsQuasar(PgCMD, PgSplit):
817
934
  self.pglog("{}-G{}: Found {} Files ({})".format(dsid, gidx, rcnt, rmsg), self.LOGWRN)
818
935
 
819
936
  fcnt = cnts['B'][0] + cnts['D'][0]
937
+ if sizes is not None: sizes[0] += cnts['B'][1] + cnts['D'][1]
820
938
  if dsfiles:
821
939
  cmsg = 'Changed ' if self.PGBACK['chgdays'] else ''
822
940
  for bflg in dsfiles:
@@ -836,7 +954,6 @@ class DsQuasar(PgCMD, PgSplit):
836
954
 
837
955
  # get web/saved files to backup for a given group condition and file option
838
956
  def get_group_files(self, dsid, gcnd, dcnd, bfiles, fopt, infiles, cnts):
839
- rcnt = 0
840
957
  if self.PGBACK['chgdays'] > 0:
841
958
  return self.get_group_changed_files(dsid, gcnd, dcnd, bfiles, fopt, infiles, cnts)
842
959
  else:
@@ -920,7 +1037,7 @@ class DsQuasar(PgCMD, PgSplit):
920
1037
  fname = pgrec[tname]
921
1038
  chome = self.PGLOG['DECSHOME'] if cate == 's' else self.PGLOG['DSDHOME']
922
1039
  if pgrec['locflag'] == 'O':
923
- bucket = 'gdex-decsdata' if cate == 's' else self.PGLOC['OBJCTBKT']
1040
+ bucket = 'gdex-decsdata' if cate == 's' else self.PGLOG['OBJCTBKT']
924
1041
  ofile = self.join_paths(dsid, fname)
925
1042
  info = self.check_object_file(ofile, bucket, 1, self.ERRACT)
926
1043
  if not info: return False
@@ -982,28 +1099,27 @@ class DsQuasar(PgCMD, PgSplit):
982
1099
  def get_backup_options(self, dsid, dcnd):
983
1100
  fopt = 0
984
1101
  bcnd = "= 0" if self.PGBACK['chgdays'] < 1 else "> 0"
985
- # if self.pgget('sfile', 'sid', "{} AND bid {}".format(dcnd, bcnd), self.LGWNEX): fopt |= self.SOPT
986
1102
  if self.pgget_wfile(dsid, 'wid', "bid " + bcnd, self.LGWNEX): fopt |= self.WOPT
987
1103
  return fopt
988
1104
 
989
1105
  # gather all available dataset ids to backup data files
990
- def gather_dataset_files(self, dsfiles, unlock = True):
1106
+ def gather_dataset_files(self, dsfiles, unlock = True, sizes = None):
991
1107
  fcnt = 0
1108
+ # warn per named dataset with no files; stay silent when scanning all datasets
1109
+ logact = self.LOGWRN if self.dsids else 0
992
1110
  if self.dsids:
1111
+ pgrecs = []
993
1112
  for dsid in self.dsids:
994
- dcnd = "dsid = '{}'".format(dsid)
995
- pgrec = self.pgget("dataset", "dsid, backflag, pid", dcnd, self.LGWNEX)
996
- if pgrec:
997
- if unlock and pgrec['pid'] and self.lock_dataset(dsid, 0, self.LOGACT) < 1: continue
998
- fcnt += self.get_dataset_files(dsid, dsfiles, pgrec['backflag'], self.LOGWRN)
1113
+ pgrec = self.pgget("dataset", "dsid, backflag, pid", "dsid = '{}'".format(dsid), self.LGWNEX)
1114
+ if pgrec: pgrecs.append(pgrec)
999
1115
  else:
1000
- dcnd = "ORDER BY dsid"
1001
- pgrecs = self.pgmget("dataset", "dsid, backflag, pid", dcnd, self.LGWNEX)
1002
- dcnt = len(pgrecs['dsid']) if pgrecs else 0
1003
- for i in range(dcnt):
1004
- dsid = pgrecs['dsid'][i]
1005
- if unlock and pgrecs['pid'][i] and self.lock_dataset(dsid, 0, self.LOGACT) < 1: continue
1006
- fcnt += self.get_dataset_files(dsid, dsfiles, pgrecs['backflag'][i])
1116
+ mrecs = self.pgmget("dataset", "dsid, backflag, pid", "ORDER BY dsid", self.LGWNEX)
1117
+ dcnt = len(mrecs['dsid']) if mrecs else 0
1118
+ pgrecs = [self.onerecord(mrecs, i) for i in range(dcnt)]
1119
+ for pgrec in pgrecs:
1120
+ dsid = pgrec['dsid']
1121
+ if unlock and pgrec['pid'] and self.lock_dataset(dsid, 0, self.LOGACT) < 1: continue
1122
+ fcnt += self.get_dataset_files(dsid, dsfiles, pgrec['backflag'], logact, sizes)
1007
1123
  if dsfiles:
1008
1124
  s = 's' if fcnt > 1 else ''
1009
1125
  bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
@@ -1013,28 +1129,21 @@ class DsQuasar(PgCMD, PgSplit):
1013
1129
 
1014
1130
  # gather all available backup file records with status N to backup
1015
1131
  def gather_dataset_infiles(self, dsfiles):
1016
- icnt = fcnt = cnt = 0
1132
+ icnt = fcnt = 0
1017
1133
  bcnd = "status = 'N'"
1018
- ocnd = " ORDER BY dsid"
1019
1134
  flds = "bid, dsid, bfile, type, data_size size, (scount + wcount) fcnt, dsids, note"
1020
1135
  if self.PGBACK['backflag']: bcnd += " AND type = '{}'".format(self.PGBACK['backflag'])
1021
1136
  if self.dsids:
1022
- for dsid in self.dsids:
1023
- dcnd = "dsid = '{}' AND {}".format(dsid, bcnd)
1024
- pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1025
- bcnt = len(pgrecs['bid']) if pgrecs else 0
1026
- for i in range(bcnt):
1027
- cnt = self.get_backup_infile(dsfiles, self.onerecord(pgrecs, i))
1028
- if cnt:
1029
- icnt +=1
1030
- fcnt += cnt
1137
+ cnds = ["dsid = '{}' AND {}".format(dsid, bcnd) for dsid in self.dsids]
1031
1138
  else:
1032
- pgrecs = self.pgmget("bfile", flds, bcnd+ocnd, self.LGWNEX)
1139
+ cnds = [bcnd + " ORDER BY dsid"]
1140
+ for dcnd in cnds:
1141
+ pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1033
1142
  bcnt = len(pgrecs['bid']) if pgrecs else 0
1034
1143
  for i in range(bcnt):
1035
1144
  cnt = self.get_backup_infile(dsfiles, self.onerecord(pgrecs, i))
1036
1145
  if cnt:
1037
- icnt +=1
1146
+ icnt += 1
1038
1147
  fcnt += cnt
1039
1148
  if dsfiles:
1040
1149
  s = 's' if icnt > 1 else ''
@@ -1045,28 +1154,21 @@ class DsQuasar(PgCMD, PgSplit):
1045
1154
  # gather all available backup file records with status T to backup
1046
1155
  #
1047
1156
  def gather_dataset_tarfiles(self, dsfiles):
1048
- tcnt = fcnt = cnt = 0
1157
+ tcnt = fcnt = 0
1049
1158
  bcnd = "status = 'T'"
1050
- ocnd = " ORDER BY dsid"
1051
1159
  flds = "bid, dsid, bfile, type, data_size size, (scount + wcount) fcnt, note"
1052
1160
  if self.PGBACK['backflag']: bcnd += " AND type = '{}'".format(self.PGBACK['backflag'])
1053
1161
  if self.dsids:
1054
- for dsid in self.dsids:
1055
- dcnd = "dsid = '{}' AND {}".format(dsid, bcnd)
1056
- pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1057
- bcnt = len(pgrecs['bid']) if pgrecs else 0
1058
- for i in range(bcnt):
1059
- cnt = self.get_backup_tarfile(dsfiles, self.onerecord(pgrecs, i))
1060
- if cnt:
1061
- tcnt +=1
1062
- fcnt += cnt
1162
+ cnds = ["dsid = '{}' AND {}".format(dsid, bcnd) for dsid in self.dsids]
1063
1163
  else:
1064
- pgrecs = self.pgmget("bfile", flds, bcnd+ocnd, self.LGWNEX)
1164
+ cnds = [bcnd + " ORDER BY dsid"]
1165
+ for dcnd in cnds:
1166
+ pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1065
1167
  bcnt = len(pgrecs['bid']) if pgrecs else 0
1066
1168
  for i in range(bcnt):
1067
1169
  cnt = self.get_backup_tarfile(dsfiles, self.onerecord(pgrecs, i))
1068
1170
  if cnt:
1069
- tcnt +=1
1171
+ tcnt += 1
1070
1172
  fcnt += cnt
1071
1173
  s = 's' if tcnt > 1 else ''
1072
1174
  bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
@@ -1092,7 +1194,6 @@ class DsQuasar(PgCMD, PgSplit):
1092
1194
  if self.PGBACK['action']&self.CINACT: ret = fcnt
1093
1195
  if not dsfiles: return ret
1094
1196
  dsids = [dsid]
1095
- # if not (pgrec['note'] or self.rebuild_file_note(pgrec, dsids)): return 0
1096
1197
  if not pgrec['note']:
1097
1198
  return self.pglog(bfile + ": Miss note for N backup file", self.ERRACT)
1098
1199
  infiles = []
@@ -1147,7 +1248,6 @@ class DsQuasar(PgCMD, PgSplit):
1147
1248
  if self.PGBACK['action']&self.CINACT: ret = fcnt
1148
1249
  if not dsfiles: return ret
1149
1250
  dsids = [dsid]
1150
- # if not (pgrec['note'] or self.rebuild_file_note(pgrec, dsids)): return 0
1151
1251
  if not pgrec['note']:
1152
1252
  return self.pglog(bfile + ": Miss note for T backup file", self.ERRACT)
1153
1253
  ftype = None
@@ -1347,6 +1447,9 @@ class DsQuasar(PgCMD, PgSplit):
1347
1447
  binfo = bfiles[bid]
1348
1448
  for bkey in binfo: qinfo[bkey] = binfo[bkey]
1349
1449
  self.process_one_quasar_pathfile(qinfo)
1450
+ if self.PGBACK['mproc'] > 1:
1451
+ self.check_child(None, 0, self.LOGWRN, 1) # wait all child processes done
1452
+ self.confirm_quasar_counts(qinfo, list(bfiles), "bfile LIKE 'G%/%.tar'") # recount confirmed renames from RDADB
1350
1453
  qcnt = qinfo['qcnt']
1351
1454
  if qcnt > 0:
1352
1455
  s = 's' if qcnt > 1 else ''
@@ -1671,43 +1774,6 @@ class DsQuasar(PgCMD, PgSplit):
1671
1774
  dsfiles[pgrec['type']][pgrec['bid']] = {'note' : pgrec['note'], 'fcnt' : pgrec['fcnt'], 'size' : pgrec['size'],
1672
1775
  'date' : pgrec['date_modified'], 'time' : pgrec['time_modified']}
1673
1776
 
1674
- # rebuild missed file note
1675
- def rebuild_file_note(self, pgrec, dsids):
1676
- bid = pgrec['bid']
1677
- fields = 'file, dsid, type, data_size, checksum'
1678
- bfiles = {dsids[0] : {'S' : [], 'W' : []}}
1679
- pgrecs = self.pgmget('sfile', 's' + fields, "bid = {} ORDER BY dsid, sfile".format(bid), self.LGEREX)
1680
- cnt = len(pgrecs['sfile']) if pgrecs else 0
1681
- for i in range(cnt):
1682
- dsid = pgrecs['dsid'][i]
1683
- if dsid not in dsids:
1684
- dsids.append(dsid)
1685
- bfiles[dsid] = {'S' : [], 'W' : []}
1686
- bfiles[dsid]['S'].append([pgrecs['sfile'][i], pgrecs['type'][i], pgrecs['data_size'][i], pgrecs['checksum'][i]])
1687
- pgrecs = self.pgmget('wfile', 'w' + fields, "bid = {} ORDER BY dsid, wfile".format(bid), self.LGEREX)
1688
- cnt = len(pgrecs['wfile']) if pgrecs else 0
1689
- for i in range(cnt):
1690
- dsid = pgrecs['dsid'][i]
1691
- if dsid not in dsids:
1692
- dsids.append(dsid)
1693
- bfiles[dsid] = {'S' : [], 'W' : []}
1694
- bfiles[dsid]['W'].append([pgrecs['wfile'][i], pgrecs['type'][i], pgrecs['data_size'][i], pgrecs['checksum'][i]])
1695
- # create note
1696
- note = ''
1697
- for dsid in bfiles:
1698
- for ftype in bfiles[dsid]:
1699
- files = bfiles[dsid][ftype]
1700
- if not files: continue
1701
- note += "<{}_{}_{}.txt>\n".format(dsid, ftype, bid)
1702
- note += "DS<=>{}\n{}F<:>{}T<:>SZ<:>MC<:>\n".format(dsid, ftype, ftype)
1703
- for flist in files:
1704
- note += "{}<:>{}<:>{}<:>{}<:>\n".format(flist[0], flist[1], flist[2], flist[3])
1705
- if note:
1706
- self.pgexec("UPDATE bfile SET note = '{}' WHERE bid = {}".format(note, bid), self.LGEREX)
1707
- pgrec['note'] = note
1708
- return 1
1709
- return 0
1710
-
1711
1777
  # dump statistics for GDEX files available to create input files
1712
1778
  def dump_dataset_files(self, dsfiles, backflag):
1713
1779
  bfiles = dsfiles[backflag]
@@ -1922,7 +1988,7 @@ class DsQuasar(PgCMD, PgSplit):
1922
1988
  pcnt += 1
1923
1989
  return pcnt
1924
1990
 
1925
- # main function to excecute this script
1991
+ # main function to execute this script
1926
1992
  def main():
1927
1993
  object = DsQuasar()
1928
1994
  object.read_parameters()
@@ -0,0 +1,128 @@
1
+
2
+ Name: dsquasar - back up RDA/GDEX data archives onto the Quasar backup servers
3
+
4
+ Check the Backup flags set in dataset.backflag and/or dsgroup.backflag of RDADB
5
+ to gather Web and Saved files, tar them into larger (>= 2GB) backup files, and
6
+ back them up onto the NCAR GDEX Quasar Globus end points, for a Backup copy
7
+ (gdex-quasar) and/or a disaster-recovery Drdata copy (gdex-quasar-drdata). The
8
+ file tarring and transferring is performed through the utility program dsarch.
9
+
10
+ Usage: dsquasar [-b] [(-a | -t DatasetIDs)] [-c ChangeDays] [-(e|E)] \
11
+ [-(B|D)] [-A ActionBits] [-l (Y|N)] [-m ProcessCount] \
12
+ [-n] [-u] [-d [HostName] [TryCount]] [-h]
13
+
14
+ Either -a or -t must be given (usage is displayed otherwise). dsquasar runs
15
+ from the Quasar backup work directory; it changes there automatically.
16
+
17
+
18
+ DATASET SELECTION
19
+
20
+ -a Gather files of all datasets that have a Backup flag set.
21
+
22
+ -t DatasetIDs
23
+ One or more dataset IDs whose files are backed up. The SQL wildcard
24
+ '%' is allowed and is expanded against the dataset table. Ignored,
25
+ with a warning, when -a is also given.
26
+
27
+
28
+ BACKUP SCOPE
29
+
30
+ -B Quasar Backup only (endpoint gdex-quasar) for the given datasets.
31
+
32
+ -D Quasar Backup & Drdata (gdex-quasar and gdex-quasar-drdata).
33
+
34
+ -B and -D are mutually exclusive; the default (neither given) backs up
35
+ to both the Backup and the Drdata endpoints.
36
+
37
+ -c ChangeDays
38
+ Used with -A 1 to re-back up files that changed within the given
39
+ number of days. Must be greater than 0.
40
+
41
+
42
+ ACTIONS
43
+
44
+ -A ActionBits
45
+ Bit flags selecting the actions to perform; the valid values are
46
+ 1, 2, 3, 4, 6, 7, 8, 16 and it defaults to 3 (actions 1 + 2):
47
+
48
+ 1 - create the dsarch input Save/Web file lists and add the
49
+ backup file (bfile) placeholder records (status 'N').
50
+ 2 - build tar files (each >= 5GB where possible) from the Saved/
51
+ Web files listed in the input files (status 'N' -> 'T').
52
+ 4 - transfer the built tar files (batched to >= 90GB per Globus
53
+ transfer) to the Quasar backup end points (status 'T' -> 'A').
54
+ 8 - check file sizes between RDADB and the Quasar servers, and
55
+ reset status to 'N' when they differ.
56
+ 16 - dump backup statistics for files backed up and files that are
57
+ ready to be backed up.
58
+
59
+ Combined values: 3 = 1+2, 6 = 2+4, 7 = 1+2+4.
60
+
61
+
62
+ RUN CONTROL
63
+
64
+ -b Turn on background mode; no screen display.
65
+
66
+ -d [HostName] [TryCount]
67
+ Turn on delay mode to run as a batch job. A dscheck record is created
68
+ and the dscheck daemon later submits the job to PBS. On the initial
69
+ submit dsquasar sizes multi-processing from the detail file count and
70
+ reserves matching PBS cpus (walltime + select=1:ncpus=N:mem=Ngb) in the
71
+ dscheck qoptions; the invocation arguments are left unchanged so a
72
+ repeat submit with the same arguments still blocks as a duplicate.
73
+
74
+ -l (Y|N)
75
+ Whether to check and lock a dataset before backing it up. Defaults
76
+ to Y.
77
+
78
+ -m ProcessCount
79
+ Number of concurrent child processes. Defaults to 1. In delay mode
80
+ the process count travels through the reserved PBS cpus rather than
81
+ the command line.
82
+
83
+ -n Gather and show the available file counts only; no backup actions are
84
+ performed.
85
+
86
+ -u Clean up (unlock) backup locks for the given dataset IDs. Dataset IDs
87
+ must be provided (not valid with -a).
88
+
89
+
90
+ EMAIL
91
+
92
+ -e Send an email to the specialist with backup statistics.
93
+
94
+ -E Send an email to the specialist with detailed backup statistics.
95
+
96
+
97
+ MISCELLANEOUS
98
+
99
+ -h Display this help document.
100
+
101
+
102
+ EXAMPLES
103
+
104
+ 1. Back up (create input files and tar files for) all flagged datasets:
105
+
106
+ dsquasar -a -A 3
107
+
108
+ 2. Transfer built tar files of one dataset to both Quasar end points:
109
+
110
+ dsquasar -t d123456 -A 4
111
+
112
+ 3. Show the counts of files ready to back up for a dataset, without acting:
113
+
114
+ dsquasar -t d123456 -n
115
+
116
+ 4. Dump backup statistics for all datasets by email:
117
+
118
+ dsquasar -a -A 16 -e
119
+
120
+ 5. Queue a delayed PBS batch job to create input files and tar files for all
121
+ datasets in the background (as run from cron); multi-processing is sized
122
+ automatically from the file count:
123
+
124
+ dsquasar -a -A 3 -e -b -d PBS
125
+
126
+ 6. Unlock backup locks left on datasets:
127
+
128
+ dsquasar -t d123456 -u
@@ -81,7 +81,7 @@ class DsTACC(PgCMD, PgSplit):
81
81
  self.ONESIZE = 20*self.PGLOG['ONEGBS'] # 20GB, minimal file size to tar a single file
82
82
  self.TFCOUNT = 1000000 # if file count is greater, use self.MINSIZE for tar file
83
83
  self.PGBACK = {
84
- 'workdir' : "{}/{}/tacc_backup".format(self.PGLOG['GDEXWORK'], self.PGLOG['GDEXUSER']),
84
+ 'workdir' : "{}/{}/tacc_backup".format(self.PGLOG['GDEXWORK'], self.PGLOG['COMMONUSER']),
85
85
  'mproc' : 1,
86
86
  'action' : self.CTACTS,
87
87
  'chgdays' : 0,
@@ -857,7 +857,7 @@ class DsTACC(PgCMD, PgSplit):
857
857
  fname = pgrec[tname]
858
858
  chome = self.PGLOG['DECSHOME'] if cate == 's' else self.PGLOG['DSDHOME']
859
859
  if pgrec['locflag'] == 'O':
860
- bucket = 'gdex-decsdata' if cate == 's' else self.PGLOC['OBJCTBKT']
860
+ bucket = 'gdex-decsdata' if cate == 's' else self.PGLOG['OBJCTBKT']
861
861
  ofile = self.join_paths(dsid, fname)
862
862
  info = self.check_object_file(ofile, bucket, 1, self.ERRACT)
863
863
  if not info: return False
@@ -19,7 +19,7 @@ def get_tar_summary_and_details(member_list_path):
19
19
  with open(member_list_path, 'r') as f:
20
20
  for idx, line in enumerate(f):
21
21
  line = line.strip()
22
- if not line or line.startswith('-rw') is False:
22
+ if not line:
23
23
  continue
24
24
  parts = line.split()
25
25
  if len(parts) < 6:
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: rda_python_dsquasar
3
+ Version: 3.0.3
4
+ Summary: RDA Python package to backup and recover RDA data archives to and from GLOBUS Quasar backup server
5
+ Author-email: Zaihua Ji <zji@ucar.edu>
6
+ Project-URL: Homepage, https://github.com/NCAR/rda-python-dsquasar
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: rda_python_common
15
+ Requires-Dist: rda_python_dsarch
16
+ Dynamic: license-file
17
+
18
+ # rda_python_dsquasar
19
+
20
+ RDA Python package to back up and recover RDA data archives to and from the
21
+ GLOBUS Quasar backup server.
22
+
23
+ ## Programs
24
+
25
+ The package installs the following command-line utilities, all running as the
26
+ current user:
27
+
28
+ - `dsquasar` — back up and restore RDA dataset archives via Quasar
29
+ - `tacctar` — create tar bundles for Quasar archival
30
+ - `taccrec` — recover content from Quasar tar bundles
31
+
32
+ Run any program with `--help` (or `-h`) for full usage details.
33
+
34
+ ## Environment setup
35
+
36
+ Create a Python environment first; package installs in the next section run
37
+ inside whichever environment you activate here.
38
+
39
+ ### Option A — Python venv (DECS machines)
40
+
41
+ ```bash
42
+ python3 -m venv $ENVHOME # e.g. /glade/u/home/gdexdata/gdexmsenv
43
+ source $ENVHOME/bin/activate
44
+ ```
45
+
46
+ ### Option B — Conda (DAV/Casper)
47
+
48
+ ```bash
49
+ conda create --prefix $ENVHOME python=3.12 # e.g. /glade/work/gdexdata/conda-envs/pg-gdex
50
+ conda activate $ENVHOME
51
+ ```
52
+
53
+ ## Installing rda-python-dsquasar
54
+
55
+ Pick whichever install mode fits your workflow. All variants pull in the
56
+ transitive dependencies (`rda_python_common`, `rda_python_dsarch`)
57
+ automatically.
58
+
59
+ For local development, clone this repo alongside your project and install it
60
+ in editable mode so that changes are picked up without re-installing:
61
+
62
+ ```bash
63
+ git clone https://github.com/NCAR/rda-python-dsquasar.git
64
+ cd rda-python-dsquasar
65
+ pip install -e .
66
+ ```
67
+
68
+ To test a specific branch (e.g. an in-progress feature or fix branch), pass
69
+ `-b/--branch` to `git clone`:
70
+
71
+ ```bash
72
+ git clone -b <branch-name> https://github.com/NCAR/rda-python-dsquasar.git
73
+ cd rda-python-dsquasar
74
+ pip install -e .
75
+ ```
76
+
77
+ For a regular (non-editable) install from a checkout:
78
+
79
+ ```bash
80
+ pip install /path/to/rda-python-dsquasar
81
+ ```
82
+
83
+ For a production install on a system that uses the published distribution:
84
+
85
+ ```bash
86
+ pip install rda_python_dsquasar
87
+ ```
@@ -1,9 +1,11 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
3
4
  pyproject.toml
4
5
  src/rda_python_dsquasar/__init__.py
5
6
  src/rda_python_dsquasar/ds_quasar.py
6
7
  src/rda_python_dsquasar/dsquasar.py
8
+ src/rda_python_dsquasar/dsquasar.usg
7
9
  src/rda_python_dsquasar/dstacc.py
8
10
  src/rda_python_dsquasar/taccrec.py
9
11
  src/rda_python_dsquasar/tacctar.py
@@ -1,18 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: rda_python_dsquasar
3
- Version: 2.0.12
4
- Summary: RDA Python package to backup and recover RDA data archives to and from GLOBUS Quasar backup server
5
- Author-email: Zaihua Ji <zji@ucar.edu>
6
- Project-URL: Homepage, https://github.com/NCAR/rda-python-dsquasar
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Development Status :: 5 - Production/Stable
11
- Requires-Python: >=3.7
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
- Requires-Dist: rda_python_common
15
- Requires-Dist: rda_python_dsarch
16
- Dynamic: license-file
17
-
18
- RDA python package to backup RDA dataset data onto Globus Quasar Server.
@@ -1 +0,0 @@
1
- RDA python package to backup RDA dataset data onto Globus Quasar Server.
@@ -1,18 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: rda_python_dsquasar
3
- Version: 2.0.12
4
- Summary: RDA Python package to backup and recover RDA data archives to and from GLOBUS Quasar backup server
5
- Author-email: Zaihua Ji <zji@ucar.edu>
6
- Project-URL: Homepage, https://github.com/NCAR/rda-python-dsquasar
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Development Status :: 5 - Production/Stable
11
- Requires-Python: >=3.7
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
- Requires-Dist: rda_python_common
15
- Requires-Dist: rda_python_dsarch
16
- Dynamic: license-file
17
-
18
- RDA python package to backup RDA dataset data onto Globus Quasar Server.