rda-python-miscs 3.0.3__tar.gz → 3.0.5__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.
- {rda_python_miscs-3.0.3/src/rda_python_miscs.egg-info → rda_python_miscs-3.0.5}/PKG-INFO +32 -3
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/README.md +31 -2
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/pyproject.toml +3 -1
- rda_python_miscs-3.0.5/src/rda_python_miscs/decsdata_restore.py +456 -0
- rda_python_miscs-3.0.5/src/rda_python_miscs/decsdata_restore.usg +101 -0
- rda_python_miscs-3.0.5/src/rda_python_miscs/decsdata_storage.py +196 -0
- rda_python_miscs-3.0.5/src/rda_python_miscs/decsdata_storage.usg +66 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/pg_rst.py +128 -25
- rda_python_miscs-3.0.5/src/rda_python_miscs/rst_templates/appendix.rst.temp +19 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5/src/rda_python_miscs.egg-info}/PKG-INFO +32 -3
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs.egg-info/SOURCES.txt +5 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs.egg-info/entry_points.txt +2 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/tests/test_miscs.py +2 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/LICENSE +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/MANIFEST.in +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/setup.cfg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/__init__.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/bash_qsub.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/bashqsub.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/bashqsub.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdex_ls.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexcp.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexcp.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexkill.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexkill.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexls.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexls.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexls_standalone.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexmod.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexmod.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexown.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexown.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexps.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexps.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexsub.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexsub.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexzip.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/gdexzip.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/pg_docs.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/pg_rst.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/pg_wget.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/pgwget.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/pgwget.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rda_cp.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rda_kill.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rda_mod.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rda_own.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rda_ps.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rda_sub.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rda_zip.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rdals.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rdals.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rst_templates/index.rst.temp +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/rst_templates/section.rst.temp +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/tcsh_qsub.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/tcshqsub.py +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs/tcshqsub.usg +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs.egg-info/dependency_links.txt +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs.egg-info/requires.txt +0 -0
- {rda_python_miscs-3.0.3 → rda_python_miscs-3.0.5}/src/rda_python_miscs.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rda_python_miscs
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.5
|
|
4
4
|
Summary: RDA Python package to hold RDA miscellaneous utility programs
|
|
5
5
|
Author-email: Zaihua Ji <zji@ucar.edu>
|
|
6
6
|
Project-URL: Homepage, https://github.com/NCAR/rda-python-miscs
|
|
@@ -45,6 +45,8 @@ The package provides two categories of programs:
|
|
|
45
45
|
| `gdexcp` | `rdacp` | `setuid_gdexcp` / `setuid_rdacp` | Copy files and directories across local, remote, Object Store, or Globus endpoints |
|
|
46
46
|
| `gdexkill` | `rdakill` | `setuid_gdexkill` / `setuid_rdakill` | Kill local processes and their children, or cancel PBS batch jobs |
|
|
47
47
|
| `gdexmod` | `rdamod` | `setuid_gdexmod` / `setuid_rdamod` | Change permission modes for files and directories owned by gdexdata |
|
|
48
|
+
| `decsdata_storage` | | `setuid_decsdata_storage` | Move decsdata datasets into the GLADE HSM cold storage to migrate them onto tape |
|
|
49
|
+
| `decsdata_restore` | | `setuid_decsdata_restore` | Recall decsdata datasets out of the GLADE HSM cold storage and copy them back |
|
|
48
50
|
|
|
49
51
|
## Environment setup
|
|
50
52
|
|
|
@@ -103,8 +105,8 @@ pip install rda_python_miscs
|
|
|
103
105
|
|
|
104
106
|
## Setuid Setup
|
|
105
107
|
|
|
106
|
-
The setuid programs (`gdexcp`, `gdexkill`, `gdexmod
|
|
107
|
-
execute as the common user `PGLOG['COMMONUSER']` (default `gdexdata`) via
|
|
108
|
+
The setuid programs (`gdexcp`, `gdexkill`, `gdexmod`, `decsdata_storage`,
|
|
109
|
+
`decsdata_restore` and the `rda*` aliases) execute as the common user `PGLOG['COMMONUSER']` (default `gdexdata`) via
|
|
108
110
|
the `rda_python_setuid` mechanism, which is pulled in automatically as a
|
|
109
111
|
dependency. After `pip install` above, choose one of the wiring options
|
|
110
112
|
below.
|
|
@@ -159,3 +161,30 @@ pywrapper-install -u|--update
|
|
|
159
161
|
The shared setuid setup guide is shown automatically if any `setuid_*`
|
|
160
162
|
connector script is invoked directly before the setuid wrapper has been
|
|
161
163
|
configured.
|
|
164
|
+
|
|
165
|
+
## Cold storage setup
|
|
166
|
+
|
|
167
|
+
`decsdata_storage` and `decsdata_restore` drive the GLADE HSM through the
|
|
168
|
+
`glade_hsm` script, which is not part of this package. They look it up at
|
|
169
|
+
`~benkirk/glade_hsm` unless the environment variable `GLADE_HSM` names another
|
|
170
|
+
copy of it:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
export GLADE_HSM=/path/to/glade_hsm
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Both programs work on the decsdata directory `PGLOG['DECSHOME']` (default
|
|
177
|
+
`/glade/campaign/collections/gdex/decsdata`, also reachable as
|
|
178
|
+
`/gdex/decsdata`), which option `-w` overrides. Only members of the DECS group
|
|
179
|
+
may run them, and `decsdata_restore` reads Table `sfile` in RDADB to size a
|
|
180
|
+
restore, so a working RDADB login is required as well.
|
|
181
|
+
|
|
182
|
+
Because the decsdata quota is limited, check the space left with `gladequota`
|
|
183
|
+
before a large restore:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pgstart_gdexdata gladequota # or plain 'gladequota' when logged in as gdexdata
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`decsdata_restore -x` runs the same check itself and refuses to request a
|
|
190
|
+
recall unless twice the size of the data is still available.
|
|
@@ -28,6 +28,8 @@ The package provides two categories of programs:
|
|
|
28
28
|
| `gdexcp` | `rdacp` | `setuid_gdexcp` / `setuid_rdacp` | Copy files and directories across local, remote, Object Store, or Globus endpoints |
|
|
29
29
|
| `gdexkill` | `rdakill` | `setuid_gdexkill` / `setuid_rdakill` | Kill local processes and their children, or cancel PBS batch jobs |
|
|
30
30
|
| `gdexmod` | `rdamod` | `setuid_gdexmod` / `setuid_rdamod` | Change permission modes for files and directories owned by gdexdata |
|
|
31
|
+
| `decsdata_storage` | | `setuid_decsdata_storage` | Move decsdata datasets into the GLADE HSM cold storage to migrate them onto tape |
|
|
32
|
+
| `decsdata_restore` | | `setuid_decsdata_restore` | Recall decsdata datasets out of the GLADE HSM cold storage and copy them back |
|
|
31
33
|
|
|
32
34
|
## Environment setup
|
|
33
35
|
|
|
@@ -86,8 +88,8 @@ pip install rda_python_miscs
|
|
|
86
88
|
|
|
87
89
|
## Setuid Setup
|
|
88
90
|
|
|
89
|
-
The setuid programs (`gdexcp`, `gdexkill`, `gdexmod
|
|
90
|
-
execute as the common user `PGLOG['COMMONUSER']` (default `gdexdata`) via
|
|
91
|
+
The setuid programs (`gdexcp`, `gdexkill`, `gdexmod`, `decsdata_storage`,
|
|
92
|
+
`decsdata_restore` and the `rda*` aliases) execute as the common user `PGLOG['COMMONUSER']` (default `gdexdata`) via
|
|
91
93
|
the `rda_python_setuid` mechanism, which is pulled in automatically as a
|
|
92
94
|
dependency. After `pip install` above, choose one of the wiring options
|
|
93
95
|
below.
|
|
@@ -142,3 +144,30 @@ pywrapper-install -u|--update
|
|
|
142
144
|
The shared setuid setup guide is shown automatically if any `setuid_*`
|
|
143
145
|
connector script is invoked directly before the setuid wrapper has been
|
|
144
146
|
configured.
|
|
147
|
+
|
|
148
|
+
## Cold storage setup
|
|
149
|
+
|
|
150
|
+
`decsdata_storage` and `decsdata_restore` drive the GLADE HSM through the
|
|
151
|
+
`glade_hsm` script, which is not part of this package. They look it up at
|
|
152
|
+
`~benkirk/glade_hsm` unless the environment variable `GLADE_HSM` names another
|
|
153
|
+
copy of it:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
export GLADE_HSM=/path/to/glade_hsm
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Both programs work on the decsdata directory `PGLOG['DECSHOME']` (default
|
|
160
|
+
`/glade/campaign/collections/gdex/decsdata`, also reachable as
|
|
161
|
+
`/gdex/decsdata`), which option `-w` overrides. Only members of the DECS group
|
|
162
|
+
may run them, and `decsdata_restore` reads Table `sfile` in RDADB to size a
|
|
163
|
+
restore, so a working RDADB login is required as well.
|
|
164
|
+
|
|
165
|
+
Because the decsdata quota is limited, check the space left with `gladequota`
|
|
166
|
+
before a large restore:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pgstart_gdexdata gladequota # or plain 'gladequota' when logged in as gdexdata
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`decsdata_restore -x` runs the same check itself and refuses to request a
|
|
173
|
+
recall unless twice the size of the data is still available.
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "rda_python_miscs"
|
|
9
|
-
version = "3.0.
|
|
9
|
+
version = "3.0.5"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name="Zaihua Ji", email="zji@ucar.edu" },
|
|
12
12
|
]
|
|
@@ -52,3 +52,5 @@ pythonpath = [
|
|
|
52
52
|
"setuid_gdexmod" = "rda_python_miscs.gdexmod:main"
|
|
53
53
|
"setuid_rdamod" = "rda_python_miscs.gdexmod:main"
|
|
54
54
|
"pgrst" = "rda_python_miscs.pg_rst:main"
|
|
55
|
+
"setuid_decsdata_storage" = "rda_python_miscs.decsdata_storage:main"
|
|
56
|
+
"setuid_decsdata_restore" = "rda_python_miscs.decsdata_restore:main"
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
##################################################################################
|
|
3
|
+
# Title: decsdata_restore
|
|
4
|
+
# Author: Zaihua Ji, zji@ucar.edu
|
|
5
|
+
# Date: 2026-08-31
|
|
6
|
+
# Purpose: restore decsdata datasets, or parts of them, out of the GLADE HSM
|
|
7
|
+
# cold storage; the opposite of decsdata_storage
|
|
8
|
+
# Github: https://github.com/NCAR/rda-python-miscs.git
|
|
9
|
+
##################################################################################
|
|
10
|
+
import re
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from os import path as op
|
|
14
|
+
from rda_python_common.pg_file import PgFile
|
|
15
|
+
|
|
16
|
+
class DecsRestore(PgFile):
|
|
17
|
+
"""Restore decsdata datasets out of the GLADE HSM cold storage.
|
|
18
|
+
|
|
19
|
+
'glade_hsm recall' only submits a request that the HSM batch processes
|
|
20
|
+
fulfill later on, so restoring is done in three steps:
|
|
21
|
+
|
|
22
|
+
-x submit the recall requests for the given datasets/paths
|
|
23
|
+
-s check the recall status, repeat until nothing is left on tape
|
|
24
|
+
-r copy the recalled data back under the decsdata directory
|
|
25
|
+
|
|
26
|
+
Recalled files stay readable inside cold storage for 7 days only, after
|
|
27
|
+
which they are migrated onto tape again, so step -r must be done within
|
|
28
|
+
that window.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
"""Initialize DecsRestore with default option values and runtime state."""
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.HSM = os.environ.get('GLADE_HSM', op.expanduser('~benkirk/glade_hsm'))
|
|
35
|
+
self.VALOPTS = 'Dwl' # single-value options
|
|
36
|
+
self.MULOPTS = 'd' # multi-value options
|
|
37
|
+
self.MODOPTS = 'hf' # mode options
|
|
38
|
+
self.ACTOPTS = 'xsr' # action options, one and only one is required
|
|
39
|
+
self.OPTS = {
|
|
40
|
+
'D': None, # cold storage date, in YYYYMMDD or YYYY-MM-DD
|
|
41
|
+
'w': None, # decsdata directory, defaults to PGLOG['DECSHOME']
|
|
42
|
+
'l': None, # dataset list file
|
|
43
|
+
'd': [], # dataset IDs, a sub-path may be appended to each one
|
|
44
|
+
't': None, # target directory of -r, defaults to the decsdata directory
|
|
45
|
+
'h': 0, # 1 to show help message
|
|
46
|
+
'f': 0, # 1 to copy back while files are still on tape
|
|
47
|
+
}
|
|
48
|
+
self.ACTION = None # one of the ACTOPTS letters
|
|
49
|
+
self.SIZEUNITS = { # units reported by 'gladequota', in bytes
|
|
50
|
+
'B': 1, 'KIB': 1024, 'MIB': 1024**2,
|
|
51
|
+
'GIB': 1024**3, 'TIB': 1024**4, 'PIB': 1024**5,
|
|
52
|
+
}
|
|
53
|
+
self.RINFO = {
|
|
54
|
+
'decsdir': None, # decsdata directory the data is restored into
|
|
55
|
+
'roots': [], # cold storage directories to look the data up in
|
|
56
|
+
'acnt': 0, # number of dataset paths acted on successfully
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# function to read parameters
|
|
60
|
+
def read_parameters(self):
|
|
61
|
+
"""Parse the command line into the OPTS option values and the action.
|
|
62
|
+
|
|
63
|
+
Single-value options -D, -w and -l take one value each, multi-value
|
|
64
|
+
option -d gathers every following dataset path, mode options -h and -f
|
|
65
|
+
are simple flags, and action options -x, -s and -r are mutually
|
|
66
|
+
exclusive; -r optionally takes a target directory. Exits with usage if
|
|
67
|
+
-h is given or no action is specified.
|
|
68
|
+
"""
|
|
69
|
+
self.set_suid(self.PGLOG['EUID'])
|
|
70
|
+
self.set_help_path(__file__)
|
|
71
|
+
self.PGLOG['LOGFILE'] = "decsdata_restore.log" # set different log file
|
|
72
|
+
argv = sys.argv[1:]
|
|
73
|
+
self.cmdlog("decsdata_restore {}".format(' '.join(argv)))
|
|
74
|
+
option = None
|
|
75
|
+
for arg in argv:
|
|
76
|
+
ms = re.match(r'^-(\w+)$', arg)
|
|
77
|
+
if ms:
|
|
78
|
+
option = ms.group(1)
|
|
79
|
+
if option in self.ACTOPTS:
|
|
80
|
+
self.set_action(option)
|
|
81
|
+
if option != 'r': option = None # -r may be followed by a target directory
|
|
82
|
+
elif option in self.MODOPTS:
|
|
83
|
+
self.OPTS[option] = 1
|
|
84
|
+
option = None
|
|
85
|
+
elif option not in self.VALOPTS and option not in self.MULOPTS:
|
|
86
|
+
self.pglog(arg + ": Unknown Option", self.LGEREX)
|
|
87
|
+
continue
|
|
88
|
+
if not option: self.pglog(arg + ": Value provided without option", self.LGEREX)
|
|
89
|
+
if option in self.MULOPTS:
|
|
90
|
+
self.OPTS[option].append(arg) # gather all values until the next option
|
|
91
|
+
else:
|
|
92
|
+
if option == 'r': option = 't' # the value of -r is the target directory
|
|
93
|
+
self.OPTS[option] = arg
|
|
94
|
+
option = None
|
|
95
|
+
if self.OPTS['h'] or not self.ACTION: self.show_usage("decsdata_restore")
|
|
96
|
+
|
|
97
|
+
# remember the action option and reject a second one
|
|
98
|
+
def set_action(self, option):
|
|
99
|
+
"""Record the single action option to perform.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
option (str): One of the ACTOPTS letters.
|
|
103
|
+
"""
|
|
104
|
+
if self.ACTION and self.ACTION != option:
|
|
105
|
+
self.pglog("-{}: Cannot combine with Action -{}".format(option, self.ACTION), self.LGEREX)
|
|
106
|
+
self.ACTION = option
|
|
107
|
+
|
|
108
|
+
# function to start actions
|
|
109
|
+
def start_actions(self):
|
|
110
|
+
"""Validate the caller, resolve the cold storage paths, and act on each dataset path."""
|
|
111
|
+
self.dssdb_dbname()
|
|
112
|
+
self.validate_decs_group('decsdata_restore', self.PGLOG['CURUID'], 1)
|
|
113
|
+
self.set_restore_paths()
|
|
114
|
+
specs = self.get_dataset_list()
|
|
115
|
+
if not specs: self.pglog("No dataset found to restore", self.LGWNEX)
|
|
116
|
+
if self.ACTION == 'x': self.check_restore_space(specs)
|
|
117
|
+
for spec in specs:
|
|
118
|
+
self.restore_one_path(spec)
|
|
119
|
+
acts = {'x': 'requested Recall', 's': 'checked Status', 'r': 'copied back'}
|
|
120
|
+
s = ('s' if self.RINFO['acnt'] > 1 else '')
|
|
121
|
+
self.pglog("{} of {} Dataset Path{} {}".format(self.RINFO['acnt'], len(specs),
|
|
122
|
+
s, acts[self.ACTION]), self.LOGWRN)
|
|
123
|
+
self.cmdlog()
|
|
124
|
+
|
|
125
|
+
# resolve the decsdata directory and the cold storage directories
|
|
126
|
+
def set_restore_paths(self):
|
|
127
|
+
"""Fill RINFO with the decsdata directory and the cold storage directories to search.
|
|
128
|
+
|
|
129
|
+
For a given -D date only '<decsdata>/cold_storage_<date>/COLD_STORAGE' is
|
|
130
|
+
searched. Without -D both '<decsdata>/COLD_STORAGE' and every
|
|
131
|
+
'<decsdata>/cold_storage_<YYYYMMDD>/COLD_STORAGE' are searched, the most
|
|
132
|
+
recent dated one first.
|
|
133
|
+
"""
|
|
134
|
+
decsdir = self.OPTS['w'] if self.OPTS['w'] else self.PGLOG['DECSHOME']
|
|
135
|
+
if not self.check_local_file(decsdir, 0, self.LOGWRN):
|
|
136
|
+
self.pglog(decsdir + ": decsdata directory NOT exists", self.LGEREX)
|
|
137
|
+
self.RINFO['decsdir'] = decsdir
|
|
138
|
+
if not self.OPTS['t']: self.OPTS['t'] = decsdir
|
|
139
|
+
roots = []
|
|
140
|
+
if self.OPTS['D']:
|
|
141
|
+
date = re.sub('-', '', self.OPTS['D'])
|
|
142
|
+
if not re.match(r'^\d{8}$', date):
|
|
143
|
+
self.pglog(date + ": Invalid cold storage date, YYYYMMDD expected", self.LGEREX)
|
|
144
|
+
roots.append(self.join_paths(decsdir, "cold_storage_{}/COLD_STORAGE".format(date)))
|
|
145
|
+
else:
|
|
146
|
+
roots.append(self.join_paths(decsdir, "COLD_STORAGE"))
|
|
147
|
+
files = self.local_glob(self.join_paths(decsdir, "cold_storage_[0-9]*/COLD_STORAGE"), 0, self.LOGWRN)
|
|
148
|
+
for file in sorted(files, reverse = True):
|
|
149
|
+
if not files[file]['isfile']: roots.append(file)
|
|
150
|
+
for root in roots:
|
|
151
|
+
if self.check_local_file(root, 0, 0): self.RINFO['roots'].append(root)
|
|
152
|
+
if not self.RINFO['roots']:
|
|
153
|
+
self.pglog("{}: No cold storage directory found in {}".format(', '.join(roots), decsdir), self.LGEREX)
|
|
154
|
+
self.pglog("Cold storage searched: {}".format(', '.join(self.RINFO['roots'])), self.LOGWRN)
|
|
155
|
+
|
|
156
|
+
# gather the dataset paths to restore
|
|
157
|
+
def get_dataset_list(self):
|
|
158
|
+
"""Return the list of dataset paths to restore.
|
|
159
|
+
|
|
160
|
+
Uses the -d values if given. Otherwise reads the -l list file, creating
|
|
161
|
+
it first from every dNNNNNN directory in the cold storage directories if
|
|
162
|
+
it does not exist yet.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
list[str]: Dataset IDs, each optionally followed by a sub-path.
|
|
166
|
+
"""
|
|
167
|
+
if self.OPTS['d']:
|
|
168
|
+
self.pglog("Restore {} given Dataset Path(s)".format(len(self.OPTS['d'])), self.LOGWRN)
|
|
169
|
+
return self.OPTS['d']
|
|
170
|
+
lstfile = self.OPTS['l'] if self.OPTS['l'] else "dsids_{}.lst".format(re.sub('-', '', self.curdate()))
|
|
171
|
+
if not op.isfile(lstfile):
|
|
172
|
+
dsids = self.get_coldstorage_datasets()
|
|
173
|
+
with open(lstfile, 'w') as OUT:
|
|
174
|
+
for dsid in dsids: OUT.write(dsid + "\n")
|
|
175
|
+
self.pglog("{}: Generated with {} Dataset(s)".format(lstfile, len(dsids)), self.LOGWRN)
|
|
176
|
+
specs = []
|
|
177
|
+
with open(lstfile, 'r') as IN:
|
|
178
|
+
for line in IN:
|
|
179
|
+
line = line.strip()
|
|
180
|
+
if line: specs.append(line)
|
|
181
|
+
self.pglog("{}: Read {} Dataset Path(s) to restore".format(lstfile, len(specs)), self.LOGWRN)
|
|
182
|
+
return specs
|
|
183
|
+
|
|
184
|
+
# find all dNNNNNN dataset directories in the cold storage directories
|
|
185
|
+
def get_coldstorage_datasets(self):
|
|
186
|
+
"""Return the sorted dataset IDs of every dNNNNNN directory in the cold storage directories.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
list[str]: Unique dataset IDs; plain files matching the pattern are skipped.
|
|
190
|
+
"""
|
|
191
|
+
dsids = []
|
|
192
|
+
for root in self.RINFO['roots']:
|
|
193
|
+
files = self.local_glob(self.join_paths(root, "d" + "[0-9]"*6), 0, self.LOGWRN)
|
|
194
|
+
for file in files:
|
|
195
|
+
if files[file]['isfile']: continue
|
|
196
|
+
dsid = op.basename(file)
|
|
197
|
+
if dsid not in dsids: dsids.append(dsid)
|
|
198
|
+
return sorted(dsids)
|
|
199
|
+
|
|
200
|
+
# locate a dataset path in the cold storage directories, the first match wins
|
|
201
|
+
def find_cold_path(self, spec):
|
|
202
|
+
"""Look a dataset path up in each cold storage directory.
|
|
203
|
+
|
|
204
|
+
Warns and names the ignored ones if the path is found in more than one
|
|
205
|
+
cold storage directory.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
spec (str): Dataset ID, optionally followed by a sub-path.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
tuple: (path, info dict) of the first match, or (None, None).
|
|
212
|
+
"""
|
|
213
|
+
hits = {}
|
|
214
|
+
for root in self.RINFO['roots']:
|
|
215
|
+
path = self.join_paths(root, spec)
|
|
216
|
+
info = self.check_local_file(path, 0, 0)
|
|
217
|
+
if info: hits[path] = info
|
|
218
|
+
if not hits: return (None, None)
|
|
219
|
+
paths = list(hits)
|
|
220
|
+
if len(paths) > 1:
|
|
221
|
+
self.pglog("{}: Found in {} cold storage directories, use {}".format(spec, len(paths), paths[0]), self.LOGWRN)
|
|
222
|
+
self.pglog("{}: Ignored {}".format(spec, ', '.join(paths[1:])), self.LOGWRN)
|
|
223
|
+
return (paths[0], hits[paths[0]])
|
|
224
|
+
|
|
225
|
+
# count the files of a cold storage path still on tape
|
|
226
|
+
def hsm_offline_count(self, path, isfile):
|
|
227
|
+
"""Return the number of files under a cold storage path that are still on tape.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
path (str): Cold storage path of a file or directory.
|
|
231
|
+
isfile (int): 1 if path is a regular file, 0 for a directory.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
int | None: Count of offline files, or None if it cannot be determined.
|
|
235
|
+
"""
|
|
236
|
+
out = self.pgsystem("{} status {}".format(self.HSM, path), self.LOGWRN, 51)
|
|
237
|
+
if not out: return None
|
|
238
|
+
if isfile: return (1 if re.search(r'migrated', out) else 0)
|
|
239
|
+
cnts = re.findall(r'Offline:\s*([\d,]+)', out)
|
|
240
|
+
if not cnts: return None
|
|
241
|
+
return int(re.sub(',', '', cnts[-1]))
|
|
242
|
+
|
|
243
|
+
# build the sfile condition of one dataset path
|
|
244
|
+
def sfile_condition(self, spec):
|
|
245
|
+
"""Turn a dataset path into a condition on table dssdb.sfile.
|
|
246
|
+
|
|
247
|
+
A saved file lives in '<decsdata>/<dsid>/<type>/<sfile>', so the first
|
|
248
|
+
component of the path is the dataset ID, the second one the saved file
|
|
249
|
+
type, and the rest the leading part of the sfile field.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
spec (str): Dataset ID, optionally followed by a sub-path.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
str: The WHERE condition of the saved files under the path.
|
|
256
|
+
"""
|
|
257
|
+
paths = spec.strip('/').split('/')
|
|
258
|
+
if not re.match(r'^[a-z]\d{6}$', paths[0]):
|
|
259
|
+
self.pglog(spec + ": Invalid dataset path, dNNNNNN expected", self.LGEREX)
|
|
260
|
+
cnd = "dsid = '{}'".format(paths[0])
|
|
261
|
+
if len(paths) > 1:
|
|
262
|
+
if not re.match(r'^\w$', paths[1]):
|
|
263
|
+
self.pglog(spec + ": Invalid saved file type, one word character expected", self.LGEREX)
|
|
264
|
+
cnd += " AND type = '{}'".format(paths[1])
|
|
265
|
+
if len(paths) > 2:
|
|
266
|
+
sfile = '/'.join(paths[2:])
|
|
267
|
+
if re.search(r"['\\]", sfile):
|
|
268
|
+
self.pglog(spec + ": Invalid saved file path", self.LGEREX)
|
|
269
|
+
# the path is either a saved file itself or the directory holding them
|
|
270
|
+
cnd += " AND (sfile = '{}' OR sfile LIKE '{}/%')".format(sfile, re.sub(r'([%_])', r'\\\1', sfile))
|
|
271
|
+
return cnd
|
|
272
|
+
|
|
273
|
+
# get the archived size of one dataset path
|
|
274
|
+
def restore_data_size(self, spec):
|
|
275
|
+
"""Return the total size of the saved files under one dataset path.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
spec (str): Dataset ID, optionally followed by a sub-path.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
int: Number of bytes recorded in dssdb.sfile; 0 if nothing is found.
|
|
282
|
+
"""
|
|
283
|
+
pgrec = self.pgget('sfile', "sum(data_size) tsize, count(sid) fcnt",
|
|
284
|
+
self.sfile_condition(spec), self.LOGWRN)
|
|
285
|
+
if not pgrec or not pgrec['tsize']:
|
|
286
|
+
self.pglog(spec + ": No saved file found in RDADB", self.LOGWRN)
|
|
287
|
+
return 0
|
|
288
|
+
self.pglog("{}: {} in {} saved file(s)".format(spec,
|
|
289
|
+
self.format_float_value(pgrec['tsize']), pgrec['fcnt']), self.LOGWRN)
|
|
290
|
+
return int(pgrec['tsize'])
|
|
291
|
+
|
|
292
|
+
# get the GLADE space left for the decsdata directory
|
|
293
|
+
def decsdata_free_size(self):
|
|
294
|
+
"""Return the GLADE space left on the quota holding the decsdata directory.
|
|
295
|
+
|
|
296
|
+
Parses the 'Used' and 'Quota' columns of 'gladequota' and picks the
|
|
297
|
+
longest reported path the decsdata directory falls under.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
int | None: Number of free bytes, or None if it cannot be determined.
|
|
301
|
+
"""
|
|
302
|
+
cmd = self.get_local_command("gladequota", self.PGLOG['COMMONUSER'])
|
|
303
|
+
out = self.pgsystem(cmd, self.LOGWRN, 21) # 1+4+16, log the command and return stdout
|
|
304
|
+
if not out: return None
|
|
305
|
+
target = op.realpath(self.RINFO['decsdir'])
|
|
306
|
+
(fsize, fpath) = (None, None)
|
|
307
|
+
for line in out.split('\n'):
|
|
308
|
+
ms = re.match(r'^(/\S+)\s+([\d.]+)\s*(\w+)\s+([\d.]+)\s*(\w+)', line)
|
|
309
|
+
if not ms: continue # skips the header and the 'n/a' quota lines
|
|
310
|
+
path = ms.group(1)
|
|
311
|
+
if not (target == path or target.startswith(path + '/')): continue
|
|
312
|
+
if fpath and len(fpath) >= len(path): continue # keeps the closest path only
|
|
313
|
+
used = self.quota_size(ms.group(2), ms.group(3))
|
|
314
|
+
quota = self.quota_size(ms.group(4), ms.group(5))
|
|
315
|
+
if used is None or quota is None: continue
|
|
316
|
+
(fsize, fpath) = (max(quota - used, 0), path)
|
|
317
|
+
return fsize
|
|
318
|
+
|
|
319
|
+
# convert one 'gladequota' size into bytes
|
|
320
|
+
def quota_size(self, value, unit):
|
|
321
|
+
"""Convert one size reported by 'gladequota' into bytes.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
value (str): The numeric part of the size.
|
|
325
|
+
unit (str): The unit of the size, such as 'TiB'.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
int | None: Number of bytes, or None for an unknown unit.
|
|
329
|
+
"""
|
|
330
|
+
unit = unit.upper()
|
|
331
|
+
if unit not in self.SIZEUNITS: return None
|
|
332
|
+
return int(float(value)*self.SIZEUNITS[unit])
|
|
333
|
+
|
|
334
|
+
# make sure the decsdata directory has room for the whole restore
|
|
335
|
+
def check_restore_space(self, specs):
|
|
336
|
+
"""Stop the recall if the decsdata directory cannot hold the whole restore.
|
|
337
|
+
|
|
338
|
+
The size to restore is added up from table dssdb.sfile and compared to
|
|
339
|
+
the GLADE space left for the decsdata directory. Twice the size is
|
|
340
|
+
required, since the recall brings the data back on disk inside the cold
|
|
341
|
+
storage first and Action -r copies it back afterwards, so both copies
|
|
342
|
+
live under the decsdata quota at the same time. The check is skipped,
|
|
343
|
+
with a warning, if either size cannot be determined.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
specs (list[str]): Dataset paths to recall.
|
|
347
|
+
"""
|
|
348
|
+
tsize = 0
|
|
349
|
+
for spec in specs:
|
|
350
|
+
tsize += self.restore_data_size(spec)
|
|
351
|
+
if not tsize:
|
|
352
|
+
self.pglog("Unknown size to restore, Skip checking the decsdata space", self.LOGWRN)
|
|
353
|
+
return
|
|
354
|
+
fsize = self.decsdata_free_size()
|
|
355
|
+
if fsize is None:
|
|
356
|
+
self.pglog("{}: Cannot get the space left, Skip checking the decsdata space".format(self.RINFO['decsdir']), self.LOGWRN)
|
|
357
|
+
return
|
|
358
|
+
nsize = 2*tsize # room for the recalled copy and for the copy of Action -r
|
|
359
|
+
msg = "{}: Restore {}, needs {} of the {} left".format(self.RINFO['decsdir'],
|
|
360
|
+
self.format_float_value(tsize), self.format_float_value(nsize),
|
|
361
|
+
self.format_float_value(fsize))
|
|
362
|
+
if nsize > fsize:
|
|
363
|
+
self.pglog(msg + ", NOT enough space", self.LGEREX)
|
|
364
|
+
self.pglog(msg, self.LOGWRN)
|
|
365
|
+
|
|
366
|
+
# act on one dataset path in cold storage
|
|
367
|
+
def restore_one_path(self, spec):
|
|
368
|
+
"""Perform the requested action on one dataset path in cold storage.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
spec (str): Dataset ID, optionally followed by a sub-path.
|
|
372
|
+
"""
|
|
373
|
+
(path, info) = self.find_cold_path(spec)
|
|
374
|
+
if not path:
|
|
375
|
+
self.pglog(spec + ": NOT found in cold storage", self.LOGERR)
|
|
376
|
+
return
|
|
377
|
+
if self.ACTION == 'x':
|
|
378
|
+
self.recall_cold_path(spec, path)
|
|
379
|
+
elif self.ACTION == 's':
|
|
380
|
+
self.status_cold_path(spec, path, info['isfile'])
|
|
381
|
+
else:
|
|
382
|
+
self.copy_cold_path(spec, path, info['isfile'])
|
|
383
|
+
|
|
384
|
+
# submit the recall request of one cold storage path
|
|
385
|
+
def recall_cold_path(self, spec, path):
|
|
386
|
+
"""Submit the HSM recall request for one cold storage path.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
spec (str): Dataset ID, optionally followed by a sub-path.
|
|
390
|
+
path (str): Cold storage path of the data.
|
|
391
|
+
"""
|
|
392
|
+
if self.pgsystem("{} recall -f {}".format(self.HSM, path), self.LOGWRN, 7):
|
|
393
|
+
self.RINFO['acnt'] += 1
|
|
394
|
+
self.pglog("{}: Recall requested, check the progress via -s".format(spec), self.LOGWRN)
|
|
395
|
+
else:
|
|
396
|
+
self.pglog("{}: Error request Recall of {}".format(spec, path), self.LOGERR)
|
|
397
|
+
|
|
398
|
+
# report the recall status of one cold storage path
|
|
399
|
+
def status_cold_path(self, spec, path, isfile):
|
|
400
|
+
"""Report the HSM and recall status of one cold storage path.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
spec (str): Dataset ID, optionally followed by a sub-path.
|
|
404
|
+
path (str): Cold storage path of the data.
|
|
405
|
+
isfile (int): 1 if path is a regular file, 0 for a directory.
|
|
406
|
+
"""
|
|
407
|
+
offline = self.hsm_offline_count(path, isfile)
|
|
408
|
+
if offline is None:
|
|
409
|
+
self.pglog("{}: Cannot get the offline file count of {}".format(spec, path), self.LOGERR)
|
|
410
|
+
return
|
|
411
|
+
self.RINFO['acnt'] += 1
|
|
412
|
+
if offline > 0:
|
|
413
|
+
s = ('s' if offline > 1 else '')
|
|
414
|
+
self.pglog("{}: Recall PENDING, {} File{} still on tape".format(spec, offline, s), self.LOGWRN)
|
|
415
|
+
else:
|
|
416
|
+
self.pglog("{}: Recall COMPLETE, ready to copy back via -r".format(spec), self.LOGWRN)
|
|
417
|
+
|
|
418
|
+
# copy one recalled cold storage path back into the decsdata directory
|
|
419
|
+
def copy_cold_path(self, spec, path, isfile):
|
|
420
|
+
"""Copy one recalled cold storage path back to its target directory.
|
|
421
|
+
|
|
422
|
+
Nothing is copied while files are still on tape unless -f is given.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
spec (str): Dataset ID, optionally followed by a sub-path.
|
|
426
|
+
path (str): Cold storage path of the data.
|
|
427
|
+
isfile (int): 1 if path is a regular file, 0 for a directory.
|
|
428
|
+
"""
|
|
429
|
+
offline = self.hsm_offline_count(path, isfile)
|
|
430
|
+
if offline:
|
|
431
|
+
s = ('s' if offline > 1 else '')
|
|
432
|
+
if not self.OPTS['f']:
|
|
433
|
+
self.pglog("{}: {} File{} still on tape, add Mode -f to copy anyway".format(spec, offline, s), self.LOGERR)
|
|
434
|
+
return
|
|
435
|
+
self.pglog("{}: {} File{} still on tape, copy it anyway".format(spec, offline, s), self.LOGWRN)
|
|
436
|
+
tofile = self.join_paths(self.OPTS['t'], spec)
|
|
437
|
+
# a directory is copied as '<path>/.' to merge into an existing target
|
|
438
|
+
fromfile = path if isfile else path + "/."
|
|
439
|
+
if self.local_copy_local(tofile, fromfile, self.LOGWRN):
|
|
440
|
+
self.RINFO['acnt'] += 1
|
|
441
|
+
self.pglog("{}: Copied back to {}".format(spec, tofile), self.LOGWRN)
|
|
442
|
+
else:
|
|
443
|
+
self.pglog("{}: Error copy {} back to {}".format(spec, path, tofile), self.LOGERR)
|
|
444
|
+
|
|
445
|
+
# main function to execute this script
|
|
446
|
+
def main():
|
|
447
|
+
"""Entry point: instantiate DecsRestore, parse arguments, run, and exit."""
|
|
448
|
+
from rda_python_setuid.setup_guide import show_setup_guide
|
|
449
|
+
object = DecsRestore()
|
|
450
|
+
show_setup_guide(object, 'rda_python_miscs', ['decsdata_storage', 'decsdata_restore'])
|
|
451
|
+
object.read_parameters()
|
|
452
|
+
object.start_actions()
|
|
453
|
+
object.pgexit(0)
|
|
454
|
+
|
|
455
|
+
# call main() to start program
|
|
456
|
+
if __name__ == "__main__": main()
|