rda-python-miscs 3.0.5__tar.gz → 3.0.6__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 (63) hide show
  1. {rda_python_miscs-3.0.5/src/rda_python_miscs.egg-info → rda_python_miscs-3.0.6}/PKG-INFO +30 -2
  2. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/README.md +29 -1
  3. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/pyproject.toml +2 -1
  4. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexcp.py +1 -1
  5. rda_python_miscs-3.0.6/src/rda_python_miscs/gdexdrop.py +285 -0
  6. rda_python_miscs-3.0.6/src/rda_python_miscs/gdexdrop.usg +77 -0
  7. rda_python_miscs-3.0.6/src/rda_python_miscs/gdexdrop_standalone.py +23 -0
  8. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexkill.py +1 -1
  9. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexmod.py +1 -1
  10. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6/src/rda_python_miscs.egg-info}/PKG-INFO +30 -2
  11. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs.egg-info/SOURCES.txt +3 -0
  12. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs.egg-info/entry_points.txt +1 -0
  13. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/tests/test_miscs.py +1 -0
  14. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/LICENSE +0 -0
  15. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/MANIFEST.in +0 -0
  16. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/setup.cfg +0 -0
  17. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/__init__.py +0 -0
  18. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/bash_qsub.py +0 -0
  19. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/bashqsub.py +0 -0
  20. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/bashqsub.usg +0 -0
  21. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/decsdata_restore.py +0 -0
  22. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/decsdata_restore.usg +0 -0
  23. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/decsdata_storage.py +0 -0
  24. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/decsdata_storage.usg +0 -0
  25. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdex_ls.py +0 -0
  26. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexcp.usg +0 -0
  27. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexkill.usg +0 -0
  28. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexls.py +0 -0
  29. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexls.usg +0 -0
  30. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexls_standalone.py +0 -0
  31. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexmod.usg +0 -0
  32. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexown.py +0 -0
  33. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexown.usg +0 -0
  34. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexps.py +0 -0
  35. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexps.usg +0 -0
  36. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexsub.py +0 -0
  37. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexsub.usg +0 -0
  38. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexzip.py +0 -0
  39. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/gdexzip.usg +0 -0
  40. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/pg_docs.py +0 -0
  41. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/pg_rst.py +0 -0
  42. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/pg_rst.usg +0 -0
  43. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/pg_wget.py +0 -0
  44. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/pgwget.py +0 -0
  45. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/pgwget.usg +0 -0
  46. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rda_cp.py +0 -0
  47. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rda_kill.py +0 -0
  48. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rda_mod.py +0 -0
  49. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rda_own.py +0 -0
  50. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rda_ps.py +0 -0
  51. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rda_sub.py +0 -0
  52. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rda_zip.py +0 -0
  53. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rdals.py +0 -0
  54. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rdals.usg +0 -0
  55. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rst_templates/appendix.rst.temp +0 -0
  56. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rst_templates/index.rst.temp +0 -0
  57. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/rst_templates/section.rst.temp +0 -0
  58. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/tcsh_qsub.py +0 -0
  59. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/tcshqsub.py +0 -0
  60. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs/tcshqsub.usg +0 -0
  61. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs.egg-info/dependency_links.txt +0 -0
  62. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/src/rda_python_miscs.egg-info/requires.txt +0 -0
  63. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.6}/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.5
3
+ Version: 3.0.6
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
@@ -43,6 +43,7 @@ The package provides two categories of programs:
43
43
  | Command | Alias | Connector script | Description |
44
44
  |---------|-------|-----------------|-------------|
45
45
  | `gdexcp` | `rdacp` | `setuid_gdexcp` / `setuid_rdacp` | Copy files and directories across local, remote, Object Store, or Globus endpoints |
46
+ | `gdexdrop` | | `setuid_gdexdrop` | Copy local files into a GDEX dataset directory as gdexdata, for data providers outside the DECS group |
46
47
  | `gdexkill` | `rdakill` | `setuid_gdexkill` / `setuid_rdakill` | Kill local processes and their children, or cancel PBS batch jobs |
47
48
  | `gdexmod` | `rdamod` | `setuid_gdexmod` / `setuid_rdamod` | Change permission modes for files and directories owned by gdexdata |
48
49
  | `decsdata_storage` | | `setuid_decsdata_storage` | Move decsdata datasets into the GLADE HSM cold storage to migrate them onto tape |
@@ -105,7 +106,7 @@ pip install rda_python_miscs
105
106
 
106
107
  ## Setuid Setup
107
108
 
108
- The setuid programs (`gdexcp`, `gdexkill`, `gdexmod`, `decsdata_storage`,
109
+ The setuid programs (`gdexcp`, `gdexdrop`, `gdexkill`, `gdexmod`, `decsdata_storage`,
109
110
  `decsdata_restore` and the `rda*` aliases) execute as the common user `PGLOG['COMMONUSER']` (default `gdexdata`) via
110
111
  the `rda_python_setuid` mechanism, which is pulled in automatically as a
111
112
  dependency. After `pip install` above, choose one of the wiring options
@@ -162,6 +163,33 @@ The shared setuid setup guide is shown automatically if any `setuid_*`
162
163
  connector script is invoked directly before the setuid wrapper has been
163
164
  configured.
164
165
 
166
+ ## gdexdrop access list
167
+
168
+ `gdexdrop` lets data providers who are **not** in the DECS group copy files into
169
+ a dataset directory so that the result is owned by `gdexdata`. It is a
170
+ deliberately narrow alternative to `gdexcp`: the destination is always
171
+ `/glade/campaign/collections/gdex/data/<dsid>`, a path that escapes the dataset
172
+ directory is rejected, and every source must be readable by the calling user
173
+ rather than only by `gdexdata`.
174
+
175
+ Who may drop into which dataset is read from the access list
176
+ `/glade/u/home/gdexdata/config/gdexdrop.conf`. It must be owned by `gdexdata`
177
+ and must not be writable by group or others, otherwise `gdexdrop` refuses to
178
+ run. Each line is a login name, a colon, and the colon-separated dataset IDs
179
+ that login may drop into, or `all` for every dataset; `#` starts a comment:
180
+
181
+ ```
182
+ # login: dsid1[:...:dsidn]
183
+ jdoe: d123456:d654321
184
+ asmith: d111222
185
+ gdexhelp: all
186
+ ```
187
+
188
+ The root path, the access list path and the log path are compiled into the
189
+ program rather than taken from `PGLOG`, because `PGLOG['DSDHOME']`,
190
+ `PGLOG['DSSHOME']` and `PGLOG['LOGPATH']` are all settable from environment
191
+ variables of the same name, which the caller controls.
192
+
165
193
  ## Cold storage setup
166
194
 
167
195
  `decsdata_storage` and `decsdata_restore` drive the GLADE HSM through the
@@ -26,6 +26,7 @@ The package provides two categories of programs:
26
26
  | Command | Alias | Connector script | Description |
27
27
  |---------|-------|-----------------|-------------|
28
28
  | `gdexcp` | `rdacp` | `setuid_gdexcp` / `setuid_rdacp` | Copy files and directories across local, remote, Object Store, or Globus endpoints |
29
+ | `gdexdrop` | | `setuid_gdexdrop` | Copy local files into a GDEX dataset directory as gdexdata, for data providers outside the DECS group |
29
30
  | `gdexkill` | `rdakill` | `setuid_gdexkill` / `setuid_rdakill` | Kill local processes and their children, or cancel PBS batch jobs |
30
31
  | `gdexmod` | `rdamod` | `setuid_gdexmod` / `setuid_rdamod` | Change permission modes for files and directories owned by gdexdata |
31
32
  | `decsdata_storage` | | `setuid_decsdata_storage` | Move decsdata datasets into the GLADE HSM cold storage to migrate them onto tape |
@@ -88,7 +89,7 @@ pip install rda_python_miscs
88
89
 
89
90
  ## Setuid Setup
90
91
 
91
- The setuid programs (`gdexcp`, `gdexkill`, `gdexmod`, `decsdata_storage`,
92
+ The setuid programs (`gdexcp`, `gdexdrop`, `gdexkill`, `gdexmod`, `decsdata_storage`,
92
93
  `decsdata_restore` and the `rda*` aliases) execute as the common user `PGLOG['COMMONUSER']` (default `gdexdata`) via
93
94
  the `rda_python_setuid` mechanism, which is pulled in automatically as a
94
95
  dependency. After `pip install` above, choose one of the wiring options
@@ -145,6 +146,33 @@ The shared setuid setup guide is shown automatically if any `setuid_*`
145
146
  connector script is invoked directly before the setuid wrapper has been
146
147
  configured.
147
148
 
149
+ ## gdexdrop access list
150
+
151
+ `gdexdrop` lets data providers who are **not** in the DECS group copy files into
152
+ a dataset directory so that the result is owned by `gdexdata`. It is a
153
+ deliberately narrow alternative to `gdexcp`: the destination is always
154
+ `/glade/campaign/collections/gdex/data/<dsid>`, a path that escapes the dataset
155
+ directory is rejected, and every source must be readable by the calling user
156
+ rather than only by `gdexdata`.
157
+
158
+ Who may drop into which dataset is read from the access list
159
+ `/glade/u/home/gdexdata/config/gdexdrop.conf`. It must be owned by `gdexdata`
160
+ and must not be writable by group or others, otherwise `gdexdrop` refuses to
161
+ run. Each line is a login name, a colon, and the colon-separated dataset IDs
162
+ that login may drop into, or `all` for every dataset; `#` starts a comment:
163
+
164
+ ```
165
+ # login: dsid1[:...:dsidn]
166
+ jdoe: d123456:d654321
167
+ asmith: d111222
168
+ gdexhelp: all
169
+ ```
170
+
171
+ The root path, the access list path and the log path are compiled into the
172
+ program rather than taken from `PGLOG`, because `PGLOG['DSDHOME']`,
173
+ `PGLOG['DSSHOME']` and `PGLOG['LOGPATH']` are all settable from environment
174
+ variables of the same name, which the caller controls.
175
+
148
176
  ## Cold storage setup
149
177
 
150
178
  `decsdata_storage` and `decsdata_restore` drive the GLADE HSM through the
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "rda_python_miscs"
9
- version = "3.0.5"
9
+ version = "3.0.6"
10
10
  authors = [
11
11
  { name="Zaihua Ji", email="zji@ucar.edu" },
12
12
  ]
@@ -47,6 +47,7 @@ pythonpath = [
47
47
  "rdaown" = "rda_python_miscs.gdexown:main"
48
48
  "setuid_gdexcp" = "rda_python_miscs.gdexcp:main"
49
49
  "setuid_rdacp" = "rda_python_miscs.gdexcp:main"
50
+ "setuid_gdexdrop" = "rda_python_miscs.gdexdrop:main"
50
51
  "setuid_gdexkill" = "rda_python_miscs.gdexkill:main"
51
52
  "setuid_rdakill" = "rda_python_miscs.gdexkill:main"
52
53
  "setuid_gdexmod" = "rda_python_miscs.gdexmod:main"
@@ -378,7 +378,7 @@ def main():
378
378
  """Entry point: instantiate GdexCp, parse arguments, run, and exit."""
379
379
  from rda_python_setuid.setup_guide import show_setup_guide
380
380
  object = GdexCp()
381
- show_setup_guide(object, 'rda_python_miscs', ['gdexcp', 'gdexkill', 'gdexmod'])
381
+ show_setup_guide(object, 'rda_python_miscs', ['gdexcp', 'gdexdrop', 'gdexkill', 'gdexmod'])
382
382
  object.read_parameters()
383
383
  object.start_actions()
384
384
  object.pgexit(0)
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env python3
2
+ ##################################################################################
3
+ # Title: gdexdrop
4
+ # Author: Zaihua Ji, zji@ucar.edu
5
+ # Date: 2026-09-23
6
+ # Purpose: copy local files into a GDEX dataset directory as 'gdexdata', for
7
+ # data providers who are not in the DECS group. Unlike gdexcp, the
8
+ # destination is confined to <DROPROOT>/<dsid> and the caller must be
9
+ # listed in the gdexdrop access list.
10
+ # Github: https://github.com/NCAR/rda-python-miscs.git
11
+ ##################################################################################
12
+ import re
13
+ import os
14
+ import sys
15
+ from os import path as op
16
+ from rda_python_common.pg_file import PgFile
17
+
18
+ # These are hardcoded on purpose. gdexdrop runs setuid to 'gdexdata' for callers
19
+ # who are NOT in the DECS group, so nothing deciding WHERE it writes or WHO may
20
+ # write there can come from the caller. PgLOG.SETPGLOG() fills PGLOG['DSDHOME'],
21
+ # PGLOG['DSSHOME'] and PGLOG['LOGPATH'] from environment variables of the same
22
+ # names, which the caller controls, so those values are deliberately not used.
23
+ DROPROOT = "/glade/campaign/collections/gdex/data" # dataset data root path
24
+ DROPCONF = "/glade/u/home/gdexdata/config/gdexdrop.conf" # access list
25
+ DROPLOG = "/glade/u/home/gdexdata/dssdb/log" # log path
26
+ DROPUSER = "gdexdata" # owner of dropped files
27
+
28
+ class GdexDrop(PgFile):
29
+ """Copy local files and directories into a GDEX dataset directory as 'gdexdata'.
30
+
31
+ The destination is always <DROPROOT>/<dsid>, optionally with a sub-path given
32
+ by -t, and is verified to stay inside that dataset directory. The caller must
33
+ be granted the dataset in the gdexdrop access list, and must be able to read
34
+ every source path as themselves; both checks exist because the copy itself
35
+ runs with the privileges of 'gdexdata'.
36
+ """
37
+
38
+ def __init__(self):
39
+ """Initialize GdexDrop with default drop options and runtime state."""
40
+ super().__init__()
41
+ self.DROP = {
42
+ 'ds': None, # target dataset ID, such as d123456
43
+ 'f': [], # from file/directory names
44
+ 'i': None, # input file holding a list of from file names, one per line
45
+ 't': None, # sub-path under the dataset directory to drop the files into
46
+ 'O': 0, # 1 to override an existing target file of the same size
47
+ 'F': 0o664, # to file mode, default to 664
48
+ 'D': 0o775, # to directory mode, default to 775
49
+ }
50
+ self.DINFO = {
51
+ 'tcnt': 0, # count of files/directories copied
52
+ 'target': None, # resolved target directory
53
+ 'curdir': os.getcwd()
54
+ }
55
+
56
+ # function to read parameters
57
+ def read_parameters(self):
58
+ """Parse command-line arguments, verify read access, then switch to 'gdexdata'.
59
+
60
+ The default option is -f (source paths); positional arguments are treated
61
+ as source paths wherever they appear. -O is a boolean flag; -F and -D take
62
+ octal modes. The source access check must run before set_suid(), while the
63
+ real user ID is still the caller, so that os.access() reports what the
64
+ CALLER can read rather than what 'gdexdata' can read.
65
+ """
66
+ dohelp = 0
67
+ argv = sys.argv[1:]
68
+ self.set_help_path(__file__)
69
+ self.PGLOG['LOGPATH'] = DROPLOG
70
+ self.PGLOG['LOGFILE'] = "gdexdrop.log" # set different log file
71
+ defopt = option = 'f'
72
+ for arg in argv:
73
+ if re.match(r'-(h|-help)$', arg, re.I):
74
+ dohelp = 1
75
+ continue
76
+ ms = re.match(r'-(\w+)$', arg)
77
+ if ms:
78
+ option = ms.group(1)
79
+ if option not in self.DROP: self.pglog(arg + ": Unknown Option", self.LGEREX)
80
+ if option == 'O':
81
+ self.DROP[option] = 1
82
+ option = defopt
83
+ continue
84
+ if not option: self.pglog(arg + ": Value provided without option", self.LGEREX)
85
+ if option == 'f':
86
+ self.DROP['f'].append(arg)
87
+ else:
88
+ if option in ('F', 'D'):
89
+ self.DROP[option] = self.base2int(arg, 8)
90
+ else:
91
+ self.DROP[option] = arg
92
+ option = defopt
93
+ if self.DROP['i']: self.add_input_files(self.DROP['i'])
94
+ if dohelp or not self.DROP['ds'] or not self.DROP['f']: self.show_usage("gdexdrop")
95
+ self.check_source_access()
96
+ self.set_suid(self.PGLOG['EUID'])
97
+ self.cmdlog("gdexdrop {} ({})".format(' '.join(argv), self.DINFO['curdir']))
98
+
99
+ # read source paths from an input file and append them to the -f list
100
+ def add_input_files(self, infile):
101
+ """Append source paths read from an input file to the -f source list.
102
+
103
+ Each non-empty line is treated as one source path; leading/trailing
104
+ whitespace is stripped and lines starting with '#' are ignored. The file
105
+ must be readable by the caller, not merely by 'gdexdata'.
106
+
107
+ Args:
108
+ infile (str): Path to the input file holding one source path per line.
109
+ """
110
+ if not os.access(infile, os.R_OK):
111
+ self.pglog("{}: Input file of -i not readable by {}".format(infile, self.PGLOG['CURUID']), self.LGEREX)
112
+ fd = open(infile, 'r')
113
+ for line in fd:
114
+ line = line.strip()
115
+ if not line or line[0] == '#': continue
116
+ self.DROP['f'].append(line)
117
+ fd.close()
118
+
119
+ # make sure the caller can read every source path as themselves
120
+ def check_source_access(self):
121
+ """Verify the calling user can read each source path, exiting if not.
122
+
123
+ Must be called while the real user ID is still the caller, i.e. before
124
+ set_suid(). Without this check a caller could name a path readable only by
125
+ 'gdexdata', have it copied into a dataset directory they can read, and so
126
+ read it indirectly.
127
+ """
128
+ for file in self.DROP['f']:
129
+ path = re.sub(r'/$', '', file, 1)
130
+ if not re.match(r'^/', path): path = self.join_paths(self.DINFO['curdir'], path)
131
+ if not op.exists(path):
132
+ self.pglog("{}: {}".format(file, self.PGLOG['MISSFILE']), self.LGEREX)
133
+ if not self.caller_can_read(path):
134
+ self.pglog("{}: not readable by {}".format(file, self.PGLOG['CURUID']), self.LGEREX)
135
+
136
+ # check read access of one path, and of everything under it for a directory
137
+ def caller_can_read(self, path):
138
+ """Return 1 if the real user can read path and all of its contents.
139
+
140
+ os.access() tests the REAL user and group IDs, which is exactly what is
141
+ wanted here while running with an effective ID of 'gdexdata'.
142
+
143
+ Args:
144
+ path (str): Absolute source path to check.
145
+
146
+ Returns:
147
+ int: 1 if fully readable by the calling user, 0 otherwise.
148
+ """
149
+ if not os.access(path, os.R_OK): return 0
150
+ if op.isfile(path): return 1
151
+ if not os.access(path, os.X_OK): return 0
152
+ for root, dirs, files in os.walk(path):
153
+ for name in dirs:
154
+ if not os.access(op.join(root, name), os.R_OK|os.X_OK): return 0
155
+ for name in files:
156
+ if not os.access(op.join(root, name), os.R_OK): return 0
157
+ return 1
158
+
159
+ # function to start actions
160
+ def start_actions(self):
161
+ """Resolve and authorize the target dataset directory, then copy the sources.
162
+
163
+ Logs a summary count on completion.
164
+ """
165
+ self.DINFO['target'] = self.resolve_target()
166
+ self.validate_caller()
167
+ self.PGLOG['FILEMODE'] = self.DROP['F']
168
+ self.PGLOG['EXECMODE'] = self.DROP['D']
169
+ for file in self.DROP['f']:
170
+ self.DINFO['tcnt'] += self.drop_one(file)
171
+ if self.DINFO['tcnt'] > 0:
172
+ s = ('s' if self.DINFO['tcnt'] > 1 else '')
173
+ self.pglog("Total {} File{} dropped into {}".format(self.DINFO['tcnt'], s, self.DINFO['target']), self.LOGWRN)
174
+ else:
175
+ self.pglog("No File dropped into " + self.DINFO['target'], self.LOGWRN)
176
+ self.cmdlog()
177
+
178
+ # resolve the target directory and confine it to the dataset directory
179
+ def resolve_target(self):
180
+ """Return the target directory under DROPROOT for -ds, confined to the dataset.
181
+
182
+ The resolved real path of the target must equal, or lie under, the resolved
183
+ real path of the dataset directory, so that neither a '..' component in -t
184
+ nor an existing symbolic link can move the drop outside the dataset.
185
+
186
+ Returns:
187
+ str: Resolved absolute target directory.
188
+ """
189
+ dsid = self.DROP['ds']
190
+ if not re.match(r'^[a-z]\d{6}$', dsid):
191
+ self.pglog(dsid + ": Invalid Dataset ID of -ds, expecting the form d123456", self.LGEREX)
192
+ dsroot = op.realpath(op.join(DROPROOT, dsid))
193
+ if not op.isdir(dsroot):
194
+ self.pglog("{}: Dataset directory NOT exists under {}".format(dsid, DROPROOT), self.LGEREX)
195
+ target = self.confine_path(op.join(dsroot, self.DROP['t']) if self.DROP['t'] else dsroot, dsroot)
196
+ return target
197
+
198
+ # make sure a path stays inside the dataset directory
199
+ def confine_path(self, path, dsroot):
200
+ """Return the resolved path, exiting if it falls outside the dataset directory.
201
+
202
+ Args:
203
+ path (str): Candidate absolute path.
204
+ dsroot (str): Resolved absolute dataset directory.
205
+
206
+ Returns:
207
+ str: Resolved absolute path inside dsroot.
208
+ """
209
+ rpath = op.realpath(path)
210
+ if rpath != dsroot and not rpath.startswith(dsroot + '/'):
211
+ self.pglog("{}: Target path is outside of {}".format(path, dsroot), self.LGEREX)
212
+ return rpath
213
+
214
+ # check the caller is granted the target dataset in the access list
215
+ def validate_caller(self):
216
+ """Exit unless the calling user is granted the target dataset.
217
+
218
+ The access list must be owned by 'gdexdata' and must not be writable by
219
+ group or others; otherwise anyone able to edit it could grant themselves
220
+ access to any dataset. Each line is 'login: dsid1[:...:dsidn]', or
221
+ 'login: all' for every dataset; '#' starts a comment. The check is
222
+ skipped when 'gdexdata' itself runs the command.
223
+ """
224
+ logname = self.PGLOG['CURUID']
225
+ if logname == DROPUSER: return
226
+ info = self.check_local_file(DROPCONF, 6, self.LOGWRN)
227
+ if not info:
228
+ self.pglog(DROPCONF + ": gdexdrop access list NOT exists", self.LGEREX)
229
+ if info['logname'] != DROPUSER:
230
+ self.pglog("{}: access list must be owned by {}, but is owned by {}".format(DROPCONF, DROPUSER, info['logname']), self.LGEREX)
231
+ if info['mode']&0o022:
232
+ self.pglog("{}: access list must not be group or world writable, mode is {:o}".format(DROPCONF, info['mode']), self.LGEREX)
233
+ dsids = {}
234
+ fd = open(DROPCONF, 'r')
235
+ for line in fd:
236
+ line = re.sub(r'#.*$', '', line).strip()
237
+ if not line: continue
238
+ ms = re.match(r'^(\S+)\s*:\s*(.*)$', line)
239
+ if not ms: continue
240
+ if ms.group(1) != logname: continue
241
+ for dsid in ms.group(2).split(':'):
242
+ dsid = dsid.strip()
243
+ if dsid: dsids[dsid] = 1
244
+ fd.close()
245
+ if not ('all' in dsids or self.DROP['ds'] in dsids):
246
+ self.pglog("{}: NOT granted Dataset {} in {}".format(logname, self.DROP['ds'], DROPCONF), self.LGEREX)
247
+
248
+ # copy one source path into the target directory
249
+ def drop_one(self, file):
250
+ """Copy one source file or directory into the target directory.
251
+
252
+ A directory is copied recursively, as the directory itself. An existing
253
+ target file of the same size is left alone unless -O is given.
254
+
255
+ Args:
256
+ file (str): Source path as given on the command line.
257
+
258
+ Returns:
259
+ int: 1 if the source was copied, 0 otherwise.
260
+ """
261
+ path = re.sub(r'/$', '', file, 1)
262
+ if not re.match(r'^/', path): path = self.join_paths(self.DINFO['curdir'], path)
263
+ finfo = self.check_local_file(path, 0, self.LOGWRN)
264
+ if not finfo:
265
+ return self.pglog("{}: {}".format(file, self.PGLOG['MISSFILE']), self.LOGERR)
266
+ tofile = self.confine_path(op.join(self.DINFO['target'], op.basename(path)), self.DINFO['target'])
267
+ if finfo['isfile'] and not self.DROP['O']:
268
+ tinfo = self.check_local_file(tofile, 0, self.LOGWRN)
269
+ if tinfo and tinfo['data_size'] == finfo['data_size']:
270
+ self.pglog(tofile + ": Target exists with same size, skip copying", self.LOGWRN)
271
+ return 0
272
+ return (1 if self.local_copy_local(tofile, path, self.LGWNEX) else 0)
273
+
274
+ # main function to execute this script
275
+ def main():
276
+ """Entry point: instantiate GdexDrop, parse arguments, run, and exit."""
277
+ from rda_python_setuid.setup_guide import show_setup_guide
278
+ object = GdexDrop()
279
+ show_setup_guide(object, 'rda_python_miscs', ['gdexcp', 'gdexdrop', 'gdexkill', 'gdexmod'])
280
+ object.read_parameters()
281
+ object.start_actions()
282
+ object.pgexit(0)
283
+
284
+ # call main() to start program
285
+ if __name__ == "__main__": main()
@@ -0,0 +1,77 @@
1
+
2
+ Copy local files and directories into a GDEX dataset directory so that they end
3
+ up owned by 'gdexdata'. It is a restricted alternative to gdexcp, intended for
4
+ data providers who are not in the DECS group: the destination is always the
5
+ given dataset's directory, and the calling user must be granted that dataset in
6
+ the gdexdrop access list.
7
+
8
+ Usage: gdexdrop -ds DatasetID [-t TargetPath] [-i InputFile] [-O] [-h] \
9
+ [-D DirectoryMode] [-F FileMode] File/Directory List
10
+
11
+ - Option -ds DatasetID is mandatory. It names the dataset to drop the
12
+ files into, in the form d123456. The files are copied under
13
+ /glade/campaign/collections/gdex/data/DatasetID, which must already
14
+ exist;
15
+
16
+ - Option -t TargetPath, sub-path under the dataset directory to drop the
17
+ files into. Missing sub-directories are created. The path must stay
18
+ inside the dataset directory; '..' components and symbolic links
19
+ leading out of it are rejected. Defaults to the dataset directory
20
+ itself;
21
+
22
+ - Option -f File/Directory List is the default option, so the source paths
23
+ may be given without it and in any position. Relative paths are
24
+ resolved against the current directory. A directory is copied
25
+ recursively, as the directory itself;
26
+
27
+ - Option -i InputFile, read additional source paths from a file, one per
28
+ line. Blank lines and lines starting with '#' are ignored;
29
+
30
+ - Option -O, override an existing target file of the same size. Without
31
+ it such a file is left alone and the copy is skipped;
32
+
33
+ - Option -D DirectoryMode, permission mode for created directories in
34
+ octal notation. Defaults to 775;
35
+
36
+ - Option -F FileMode, permission mode for copied files in octal notation.
37
+ Defaults to 664;
38
+
39
+ - Option -h, display this help document.
40
+
41
+ Every source path must be readable by the calling user, not merely by
42
+ 'gdexdata'; a path the caller cannot read themselves is refused. Copied files
43
+ and directories are owned by 'gdexdata'.
44
+
45
+ Access List
46
+ -----------
47
+
48
+ The access list is /glade/u/home/gdexdata/config/gdexdrop.conf. It must be
49
+ owned by 'gdexdata' and must not be writable by group or others. Each line
50
+ is a login name, a colon, and the colon-separated dataset IDs that login may
51
+ drop into, or 'all' for every dataset. '#' starts a comment:
52
+
53
+ # login: dsid1[:...:dsidn]
54
+ jdoe: d123456:d654321
55
+ asmith: d111222
56
+ gdexhelp: all
57
+
58
+ A login absent from the list cannot drop into any dataset. 'gdexdata' itself
59
+ is always allowed and needs no entry.
60
+
61
+ Examples:
62
+
63
+ 1. Drop two files into the top of a dataset directory:
64
+
65
+ gdexdrop -ds d123456 hourly.202601.nc hourly.202602.nc
66
+
67
+ 2. Drop the same files into a sub-directory of the dataset:
68
+
69
+ gdexdrop -ds d123456 -t 2026/monthly hourly.2026*.nc
70
+
71
+ 3. Drop a whole directory tree, replacing files already there:
72
+
73
+ gdexdrop -ds d123456 -t archive -O /path/to/mydir
74
+
75
+ 4. Drop a long list of files named in a file, with group-writable modes:
76
+
77
+ gdexdrop -ds d123456 -i filelist.txt -F 664 -D 775
@@ -0,0 +1,23 @@
1
+ #!/glade/work/zji/conda-envs/pg-gdex/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ # 2026-09-23, zji@ucar.edu, created for a standalone version of gdexdrop
4
+ #
5
+ # Copy this file to a common area, such as /glade/u/home/gdexdata/bin, so that
6
+ # users without the conda environment or a venv activated can run gdexdrop.
7
+ # The shebang and pgpath below name the environment gdexdrop is installed in.
8
+ #
9
+ # Run directly, it executes as the CALLING user, so the dropped files are owned
10
+ # by the caller. To have them owned by 'gdexdata' for a caller outside the DECS
11
+ # group, compile a 4755 cmwrapper binary that execs this script:
12
+ #
13
+ # pywrapper-install -m gdexdrop -t <common area>/gdexdrop_standalone.py \
14
+ # -d <common area>
15
+ import re
16
+ import sys
17
+ pgpath = '/glade/work/zji/conda-envs/pg-gdex/lib/python3.12/site-packages'
18
+ if pgpath not in sys.path: sys.path.insert(0, pgpath)
19
+
20
+ from rda_python_miscs.gdexdrop import main
21
+ if __name__ == '__main__':
22
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
23
+ sys.exit(main())
@@ -260,7 +260,7 @@ def main():
260
260
  """Entry point: instantiate GdexKill, parse arguments, run, and exit."""
261
261
  from rda_python_setuid.setup_guide import show_setup_guide
262
262
  object = GdexKill()
263
- show_setup_guide(object, 'rda_python_miscs', ['gdexcp', 'gdexkill', 'gdexmod'])
263
+ show_setup_guide(object, 'rda_python_miscs', ['gdexcp', 'gdexdrop', 'gdexkill', 'gdexmod'])
264
264
  object.read_parameters()
265
265
  object.start_actions()
266
266
  object.pgexit(0)
@@ -196,7 +196,7 @@ def main():
196
196
  """Entry point: instantiate GdexMod, parse arguments, run, and exit."""
197
197
  from rda_python_setuid.setup_guide import show_setup_guide
198
198
  object = GdexMod()
199
- show_setup_guide(object, 'rda_python_miscs', ['gdexcp', 'gdexkill', 'gdexmod'])
199
+ show_setup_guide(object, 'rda_python_miscs', ['gdexcp', 'gdexdrop', 'gdexkill', 'gdexmod'])
200
200
  object.read_parameters()
201
201
  object.start_actions()
202
202
  object.pgexit(0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_miscs
3
- Version: 3.0.5
3
+ Version: 3.0.6
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
@@ -43,6 +43,7 @@ The package provides two categories of programs:
43
43
  | Command | Alias | Connector script | Description |
44
44
  |---------|-------|-----------------|-------------|
45
45
  | `gdexcp` | `rdacp` | `setuid_gdexcp` / `setuid_rdacp` | Copy files and directories across local, remote, Object Store, or Globus endpoints |
46
+ | `gdexdrop` | | `setuid_gdexdrop` | Copy local files into a GDEX dataset directory as gdexdata, for data providers outside the DECS group |
46
47
  | `gdexkill` | `rdakill` | `setuid_gdexkill` / `setuid_rdakill` | Kill local processes and their children, or cancel PBS batch jobs |
47
48
  | `gdexmod` | `rdamod` | `setuid_gdexmod` / `setuid_rdamod` | Change permission modes for files and directories owned by gdexdata |
48
49
  | `decsdata_storage` | | `setuid_decsdata_storage` | Move decsdata datasets into the GLADE HSM cold storage to migrate them onto tape |
@@ -105,7 +106,7 @@ pip install rda_python_miscs
105
106
 
106
107
  ## Setuid Setup
107
108
 
108
- The setuid programs (`gdexcp`, `gdexkill`, `gdexmod`, `decsdata_storage`,
109
+ The setuid programs (`gdexcp`, `gdexdrop`, `gdexkill`, `gdexmod`, `decsdata_storage`,
109
110
  `decsdata_restore` and the `rda*` aliases) execute as the common user `PGLOG['COMMONUSER']` (default `gdexdata`) via
110
111
  the `rda_python_setuid` mechanism, which is pulled in automatically as a
111
112
  dependency. After `pip install` above, choose one of the wiring options
@@ -162,6 +163,33 @@ The shared setuid setup guide is shown automatically if any `setuid_*`
162
163
  connector script is invoked directly before the setuid wrapper has been
163
164
  configured.
164
165
 
166
+ ## gdexdrop access list
167
+
168
+ `gdexdrop` lets data providers who are **not** in the DECS group copy files into
169
+ a dataset directory so that the result is owned by `gdexdata`. It is a
170
+ deliberately narrow alternative to `gdexcp`: the destination is always
171
+ `/glade/campaign/collections/gdex/data/<dsid>`, a path that escapes the dataset
172
+ directory is rejected, and every source must be readable by the calling user
173
+ rather than only by `gdexdata`.
174
+
175
+ Who may drop into which dataset is read from the access list
176
+ `/glade/u/home/gdexdata/config/gdexdrop.conf`. It must be owned by `gdexdata`
177
+ and must not be writable by group or others, otherwise `gdexdrop` refuses to
178
+ run. Each line is a login name, a colon, and the colon-separated dataset IDs
179
+ that login may drop into, or `all` for every dataset; `#` starts a comment:
180
+
181
+ ```
182
+ # login: dsid1[:...:dsidn]
183
+ jdoe: d123456:d654321
184
+ asmith: d111222
185
+ gdexhelp: all
186
+ ```
187
+
188
+ The root path, the access list path and the log path are compiled into the
189
+ program rather than taken from `PGLOG`, because `PGLOG['DSDHOME']`,
190
+ `PGLOG['DSSHOME']` and `PGLOG['LOGPATH']` are all settable from environment
191
+ variables of the same name, which the caller controls.
192
+
165
193
  ## Cold storage setup
166
194
 
167
195
  `decsdata_storage` and `decsdata_restore` drive the GLADE HSM through the
@@ -13,6 +13,9 @@ src/rda_python_miscs/decsdata_storage.usg
13
13
  src/rda_python_miscs/gdex_ls.py
14
14
  src/rda_python_miscs/gdexcp.py
15
15
  src/rda_python_miscs/gdexcp.usg
16
+ src/rda_python_miscs/gdexdrop.py
17
+ src/rda_python_miscs/gdexdrop.usg
18
+ src/rda_python_miscs/gdexdrop_standalone.py
16
19
  src/rda_python_miscs/gdexkill.py
17
20
  src/rda_python_miscs/gdexkill.usg
18
21
  src/rda_python_miscs/gdexls.py
@@ -14,6 +14,7 @@ rdazip = rda_python_miscs.gdexzip:main
14
14
  setuid_decsdata_restore = rda_python_miscs.decsdata_restore:main
15
15
  setuid_decsdata_storage = rda_python_miscs.decsdata_storage:main
16
16
  setuid_gdexcp = rda_python_miscs.gdexcp:main
17
+ setuid_gdexdrop = rda_python_miscs.gdexdrop:main
17
18
  setuid_gdexkill = rda_python_miscs.gdexkill:main
18
19
  setuid_gdexmod = rda_python_miscs.gdexmod:main
19
20
  setuid_rdacp = rda_python_miscs.gdexcp:main
@@ -8,6 +8,7 @@ def test_miscs():
8
8
  import rda_python_miscs.gdexls
9
9
  import rda_python_miscs.pgwget
10
10
  import rda_python_miscs.gdexcp
11
+ import rda_python_miscs.gdexdrop
11
12
  import rda_python_miscs.gdexkill
12
13
  import rda_python_miscs.gdexmod
13
14
  import rda_python_miscs.gdexown