rda-python-miscs 3.0.5__tar.gz → 3.0.7__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.7}/PKG-INFO +35 -2
  2. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/README.md +34 -1
  3. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/pyproject.toml +2 -1
  4. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexcp.py +1 -1
  5. rda_python_miscs-3.0.7/src/rda_python_miscs/gdexdrop.py +329 -0
  6. rda_python_miscs-3.0.7/src/rda_python_miscs/gdexdrop.usg +79 -0
  7. rda_python_miscs-3.0.7/src/rda_python_miscs/gdexdrop_standalone.py +23 -0
  8. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexkill.py +1 -1
  9. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexmod.py +1 -1
  10. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7/src/rda_python_miscs.egg-info}/PKG-INFO +35 -2
  11. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs.egg-info/SOURCES.txt +3 -0
  12. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs.egg-info/entry_points.txt +1 -0
  13. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/tests/test_miscs.py +1 -0
  14. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/LICENSE +0 -0
  15. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/MANIFEST.in +0 -0
  16. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/setup.cfg +0 -0
  17. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/__init__.py +0 -0
  18. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/bash_qsub.py +0 -0
  19. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/bashqsub.py +0 -0
  20. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/bashqsub.usg +0 -0
  21. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/decsdata_restore.py +0 -0
  22. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/decsdata_restore.usg +0 -0
  23. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/decsdata_storage.py +0 -0
  24. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/decsdata_storage.usg +0 -0
  25. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdex_ls.py +0 -0
  26. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexcp.usg +0 -0
  27. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexkill.usg +0 -0
  28. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexls.py +0 -0
  29. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexls.usg +0 -0
  30. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexls_standalone.py +0 -0
  31. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexmod.usg +0 -0
  32. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexown.py +0 -0
  33. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexown.usg +0 -0
  34. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexps.py +0 -0
  35. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexps.usg +0 -0
  36. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexsub.py +0 -0
  37. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexsub.usg +0 -0
  38. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexzip.py +0 -0
  39. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/gdexzip.usg +0 -0
  40. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/pg_docs.py +0 -0
  41. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/pg_rst.py +0 -0
  42. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/pg_rst.usg +0 -0
  43. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/pg_wget.py +0 -0
  44. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/pgwget.py +0 -0
  45. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/pgwget.usg +0 -0
  46. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rda_cp.py +0 -0
  47. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rda_kill.py +0 -0
  48. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rda_mod.py +0 -0
  49. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rda_own.py +0 -0
  50. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rda_ps.py +0 -0
  51. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rda_sub.py +0 -0
  52. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rda_zip.py +0 -0
  53. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rdals.py +0 -0
  54. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rdals.usg +0 -0
  55. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rst_templates/appendix.rst.temp +0 -0
  56. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rst_templates/index.rst.temp +0 -0
  57. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/rst_templates/section.rst.temp +0 -0
  58. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/tcsh_qsub.py +0 -0
  59. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/tcshqsub.py +0 -0
  60. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs/tcshqsub.usg +0 -0
  61. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs.egg-info/dependency_links.txt +0 -0
  62. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/src/rda_python_miscs.egg-info/requires.txt +0 -0
  63. {rda_python_miscs-3.0.5 → rda_python_miscs-3.0.7}/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.7
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,38 @@ 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
+ Everything dropped, including sub-directories created for `-t`, is also set to the
176
+ GDEX group `PGLOG['GDEXGRP']`. The setuid wrapper switches only the user to
177
+ `gdexdata`, not the group, so without that step a dropped file would stay in the
178
+ calling user's group unless the directory it landed in carried the setgid bit.
179
+
180
+ Who may drop into which dataset is read from the access list
181
+ `/glade/u/home/gdexdata/config/gdexdrop.conf`. It must be owned by `gdexdata`
182
+ and must not be writable by group or others, otherwise `gdexdrop` refuses to
183
+ run. Each line is a login name, a colon, and the colon-separated dataset IDs
184
+ that login may drop into, or `all` for every dataset; `#` starts a comment:
185
+
186
+ ```
187
+ # login: dsid1[:...:dsidn]
188
+ jdoe: d123456:d654321
189
+ asmith: d111222
190
+ gdexhelp: all
191
+ ```
192
+
193
+ The root path, the access list path and the log path are compiled into the
194
+ program rather than taken from `PGLOG`, because `PGLOG['DSDHOME']`,
195
+ `PGLOG['DSSHOME']` and `PGLOG['LOGPATH']` are all settable from environment
196
+ variables of the same name, which the caller controls.
197
+
165
198
  ## Cold storage setup
166
199
 
167
200
  `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,38 @@ 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
+ Everything dropped, including sub-directories created for `-t`, is also set to the
159
+ GDEX group `PGLOG['GDEXGRP']`. The setuid wrapper switches only the user to
160
+ `gdexdata`, not the group, so without that step a dropped file would stay in the
161
+ calling user's group unless the directory it landed in carried the setgid bit.
162
+
163
+ Who may drop into which dataset is read from the access list
164
+ `/glade/u/home/gdexdata/config/gdexdrop.conf`. It must be owned by `gdexdata`
165
+ and must not be writable by group or others, otherwise `gdexdrop` refuses to
166
+ run. Each line is a login name, a colon, and the colon-separated dataset IDs
167
+ that login may drop into, or `all` for every dataset; `#` starts a comment:
168
+
169
+ ```
170
+ # login: dsid1[:...:dsidn]
171
+ jdoe: d123456:d654321
172
+ asmith: d111222
173
+ gdexhelp: all
174
+ ```
175
+
176
+ The root path, the access list path and the log path are compiled into the
177
+ program rather than taken from `PGLOG`, because `PGLOG['DSDHOME']`,
178
+ `PGLOG['DSSHOME']` and `PGLOG['LOGPATH']` are all settable from environment
179
+ variables of the same name, which the caller controls.
180
+
148
181
  ## Cold storage setup
149
182
 
150
183
  `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.7"
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,329 @@
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'. Everything dropped is then set to the
36
+ GDEX group PGLOG['GDEXGRP'], which the copy does not land in on its own.
37
+ """
38
+
39
+ def __init__(self):
40
+ """Initialize GdexDrop with default drop options and runtime state."""
41
+ super().__init__()
42
+ self.DROP = {
43
+ 'ds': None, # target dataset ID, such as d123456
44
+ 'f': [], # from file/directory names
45
+ 'i': None, # input file holding a list of from file names, one per line
46
+ 't': None, # sub-path under the dataset directory to drop the files into
47
+ 'O': 0, # 1 to override an existing target file of the same size
48
+ 'F': 0o664, # to file mode, default to 664
49
+ 'D': 0o775, # to directory mode, default to 775
50
+ }
51
+ self.DINFO = {
52
+ 'tcnt': 0, # count of files/directories copied
53
+ 'target': None, # resolved target directory
54
+ 'curdir': os.getcwd()
55
+ }
56
+
57
+ # function to read parameters
58
+ def read_parameters(self):
59
+ """Parse command-line arguments, verify read access, then switch to 'gdexdata'.
60
+
61
+ The default option is -f (source paths); positional arguments are treated
62
+ as source paths wherever they appear. -O is a boolean flag; -F and -D take
63
+ octal modes. The source access check must run before set_suid(), while the
64
+ real user ID is still the caller, so that os.access() reports what the
65
+ CALLER can read rather than what 'gdexdata' can read.
66
+ """
67
+ dohelp = 0
68
+ argv = sys.argv[1:]
69
+ self.set_help_path(__file__)
70
+ self.PGLOG['LOGPATH'] = DROPLOG
71
+ self.PGLOG['LOGFILE'] = "gdexdrop.log" # set different log file
72
+ defopt = option = 'f'
73
+ for arg in argv:
74
+ if re.match(r'-(h|-help)$', arg, re.I):
75
+ dohelp = 1
76
+ continue
77
+ ms = re.match(r'-(\w+)$', arg)
78
+ if ms:
79
+ option = ms.group(1)
80
+ if option not in self.DROP: self.pglog(arg + ": Unknown Option", self.LGEREX)
81
+ if option == 'O':
82
+ self.DROP[option] = 1
83
+ option = defopt
84
+ continue
85
+ if not option: self.pglog(arg + ": Value provided without option", self.LGEREX)
86
+ if option == 'f':
87
+ self.DROP['f'].append(arg)
88
+ else:
89
+ if option in ('F', 'D'):
90
+ self.DROP[option] = self.base2int(arg, 8)
91
+ else:
92
+ self.DROP[option] = arg
93
+ option = defopt
94
+ if self.DROP['i']: self.add_input_files(self.DROP['i'])
95
+ if dohelp or not self.DROP['ds'] or not self.DROP['f']: self.show_usage("gdexdrop")
96
+ self.check_source_access()
97
+ self.set_suid(self.PGLOG['EUID'])
98
+ self.cmdlog("gdexdrop {} ({})".format(' '.join(argv), self.DINFO['curdir']))
99
+
100
+ # read source paths from an input file and append them to the -f list
101
+ def add_input_files(self, infile):
102
+ """Append source paths read from an input file to the -f source list.
103
+
104
+ Each non-empty line is treated as one source path; leading/trailing
105
+ whitespace is stripped and lines starting with '#' are ignored. The file
106
+ must be readable by the caller, not merely by 'gdexdata'.
107
+
108
+ Args:
109
+ infile (str): Path to the input file holding one source path per line.
110
+ """
111
+ if not os.access(infile, os.R_OK):
112
+ self.pglog("{}: Input file of -i not readable by {}".format(infile, self.PGLOG['CURUID']), self.LGEREX)
113
+ fd = open(infile, 'r')
114
+ for line in fd:
115
+ line = line.strip()
116
+ if not line or line[0] == '#': continue
117
+ self.DROP['f'].append(line)
118
+ fd.close()
119
+
120
+ # make sure the caller can read every source path as themselves
121
+ def check_source_access(self):
122
+ """Verify the calling user can read each source path, exiting if not.
123
+
124
+ Must be called while the real user ID is still the caller, i.e. before
125
+ set_suid(). Without this check a caller could name a path readable only by
126
+ 'gdexdata', have it copied into a dataset directory they can read, and so
127
+ read it indirectly.
128
+ """
129
+ for file in self.DROP['f']:
130
+ path = re.sub(r'/$', '', file, 1)
131
+ if not re.match(r'^/', path): path = self.join_paths(self.DINFO['curdir'], path)
132
+ if not op.exists(path):
133
+ self.pglog("{}: {}".format(file, self.PGLOG['MISSFILE']), self.LGEREX)
134
+ if not self.caller_can_read(path):
135
+ self.pglog("{}: not readable by {}".format(file, self.PGLOG['CURUID']), self.LGEREX)
136
+
137
+ # check read access of one path, and of everything under it for a directory
138
+ def caller_can_read(self, path):
139
+ """Return 1 if the real user can read path and all of its contents.
140
+
141
+ os.access() tests the REAL user and group IDs, which is exactly what is
142
+ wanted here while running with an effective ID of 'gdexdata'.
143
+
144
+ Args:
145
+ path (str): Absolute source path to check.
146
+
147
+ Returns:
148
+ int: 1 if fully readable by the calling user, 0 otherwise.
149
+ """
150
+ if not os.access(path, os.R_OK): return 0
151
+ if op.isfile(path): return 1
152
+ if not os.access(path, os.X_OK): return 0
153
+ for root, dirs, files in os.walk(path):
154
+ for name in dirs:
155
+ if not os.access(op.join(root, name), os.R_OK|os.X_OK): return 0
156
+ for name in files:
157
+ if not os.access(op.join(root, name), os.R_OK): return 0
158
+ return 1
159
+
160
+ # function to start actions
161
+ def start_actions(self):
162
+ """Resolve and authorize the target dataset directory, then copy the sources.
163
+
164
+ Logs a summary count on completion.
165
+ """
166
+ self.DINFO['target'] = self.resolve_target()
167
+ self.validate_caller()
168
+ if not self.PGLOG['GDEXGID']:
169
+ self.pglog(self.PGLOG['GDEXGRP'] + ": Unknown Group to own the dropped files", self.LGEREX)
170
+ self.PGLOG['FILEMODE'] = self.DROP['F']
171
+ self.PGLOG['EXECMODE'] = self.DROP['D']
172
+ self.make_target_directory()
173
+ for file in self.DROP['f']:
174
+ self.DINFO['tcnt'] += self.drop_one(file)
175
+ if self.DINFO['tcnt'] > 0:
176
+ s = ('s' if self.DINFO['tcnt'] > 1 else '')
177
+ self.pglog("Total {} File{} dropped into {}".format(self.DINFO['tcnt'], s, self.DINFO['target']), self.LOGWRN)
178
+ else:
179
+ self.pglog("No File dropped into " + self.DINFO['target'], self.LOGWRN)
180
+ self.cmdlog()
181
+
182
+ # resolve the target directory and confine it to the dataset directory
183
+ def resolve_target(self):
184
+ """Return the target directory under DROPROOT for -ds, confined to the dataset.
185
+
186
+ The resolved real path of the target must equal, or lie under, the resolved
187
+ real path of the dataset directory, so that neither a '..' component in -t
188
+ nor an existing symbolic link can move the drop outside the dataset.
189
+
190
+ Returns:
191
+ str: Resolved absolute target directory.
192
+ """
193
+ dsid = self.DROP['ds']
194
+ if not re.match(r'^[a-z]\d{6}$', dsid):
195
+ self.pglog(dsid + ": Invalid Dataset ID of -ds, expecting the form d123456", self.LGEREX)
196
+ dsroot = op.realpath(op.join(DROPROOT, dsid))
197
+ if not op.isdir(dsroot):
198
+ self.pglog("{}: Dataset directory NOT exists under {}".format(dsid, DROPROOT), self.LGEREX)
199
+ target = self.confine_path(op.join(dsroot, self.DROP['t']) if self.DROP['t'] else dsroot, dsroot)
200
+ return target
201
+
202
+ # make sure a path stays inside the dataset directory
203
+ def confine_path(self, path, dsroot):
204
+ """Return the resolved path, exiting if it falls outside the dataset directory.
205
+
206
+ Args:
207
+ path (str): Candidate absolute path.
208
+ dsroot (str): Resolved absolute dataset directory.
209
+
210
+ Returns:
211
+ str: Resolved absolute path inside dsroot.
212
+ """
213
+ rpath = op.realpath(path)
214
+ if rpath != dsroot and not rpath.startswith(dsroot + '/'):
215
+ self.pglog("{}: Target path is outside of {}".format(path, dsroot), self.LGEREX)
216
+ return rpath
217
+
218
+ # check the caller is granted the target dataset in the access list
219
+ def validate_caller(self):
220
+ """Exit unless the calling user is granted the target dataset.
221
+
222
+ The access list must be owned by 'gdexdata' and must not be writable by
223
+ group or others; otherwise anyone able to edit it could grant themselves
224
+ access to any dataset. Each line is 'login: dsid1[:...:dsidn]', or
225
+ 'login: all' for every dataset; '#' starts a comment. The check is
226
+ skipped when 'gdexdata' itself runs the command.
227
+ """
228
+ logname = self.PGLOG['CURUID']
229
+ if logname == DROPUSER: return
230
+ info = self.check_local_file(DROPCONF, 6, self.LOGWRN)
231
+ if not info:
232
+ self.pglog(DROPCONF + ": gdexdrop access list NOT exists", self.LGEREX)
233
+ if info['logname'] != DROPUSER:
234
+ self.pglog("{}: access list must be owned by {}, but is owned by {}".format(DROPCONF, DROPUSER, info['logname']), self.LGEREX)
235
+ if info['mode']&0o022:
236
+ self.pglog("{}: access list must not be group or world writable, mode is {:o}".format(DROPCONF, info['mode']), self.LGEREX)
237
+ dsids = {}
238
+ fd = open(DROPCONF, 'r')
239
+ for line in fd:
240
+ line = re.sub(r'#.*$', '', line).strip()
241
+ if not line: continue
242
+ ms = re.match(r'^(\S+)\s*:\s*(.*)$', line)
243
+ if not ms: continue
244
+ if ms.group(1) != logname: continue
245
+ for dsid in ms.group(2).split(':'):
246
+ dsid = dsid.strip()
247
+ if dsid: dsids[dsid] = 1
248
+ fd.close()
249
+ if not ('all' in dsids or self.DROP['ds'] in dsids):
250
+ self.pglog("{}: NOT granted Dataset {} in {}".format(logname, self.DROP['ds'], DROPCONF), self.LGEREX)
251
+
252
+ # create the target directory, and set the group of each directory created
253
+ def make_target_directory(self):
254
+ """Create the sub-directories named by -t that do not exist yet.
255
+
256
+ They are created here rather than on the fly by local_copy_local() so that
257
+ set_drop_group() can be called on each one; a directory left in the calling
258
+ user's group could not be written into by the rest of the DECS group later.
259
+ """
260
+ newdirs = []
261
+ dir = self.DINFO['target']
262
+ while not op.isdir(dir):
263
+ newdirs.insert(0, dir)
264
+ dir = op.dirname(dir)
265
+ if not newdirs: return
266
+ self.make_local_directory(self.DINFO['target'], self.LGWNEX)
267
+ for dir in newdirs: self.set_drop_group(dir)
268
+
269
+ # set the group of a dropped path, and of everything under it for a directory
270
+ def set_drop_group(self, path):
271
+ """Set the group of path, and of all of its contents for a directory.
272
+
273
+ The copy runs with an effective user of 'gdexdata' but with the EFFECTIVE
274
+ GROUP of the caller, so a dropped file lands in the caller's group unless
275
+ the directory it lands in happens to carry the setgid bit. Symbolic links
276
+ are skipped, since os.chown() follows them out of the dataset directory.
277
+
278
+ Args:
279
+ path (str): Absolute path of a dropped file or directory.
280
+ """
281
+ if op.islink(path): return
282
+ self.change_local_group(path, None, None, None, self.LOGWRN)
283
+ if not op.isdir(path): return
284
+ for root, dirs, files in os.walk(path):
285
+ for name in dirs + files:
286
+ subpath = op.join(root, name)
287
+ if op.islink(subpath): continue
288
+ self.change_local_group(subpath, None, None, None, self.LOGWRN)
289
+
290
+ # copy one source path into the target directory
291
+ def drop_one(self, file):
292
+ """Copy one source file or directory into the target directory.
293
+
294
+ A directory is copied recursively, as the directory itself. An existing
295
+ target file of the same size is left alone unless -O is given.
296
+
297
+ Args:
298
+ file (str): Source path as given on the command line.
299
+
300
+ Returns:
301
+ int: 1 if the source was copied, 0 otherwise.
302
+ """
303
+ path = re.sub(r'/$', '', file, 1)
304
+ if not re.match(r'^/', path): path = self.join_paths(self.DINFO['curdir'], path)
305
+ finfo = self.check_local_file(path, 0, self.LOGWRN)
306
+ if not finfo:
307
+ return self.pglog("{}: {}".format(file, self.PGLOG['MISSFILE']), self.LOGERR)
308
+ tofile = self.confine_path(op.join(self.DINFO['target'], op.basename(path)), self.DINFO['target'])
309
+ if finfo['isfile'] and not self.DROP['O']:
310
+ tinfo = self.check_local_file(tofile, 0, self.LOGWRN)
311
+ if tinfo and tinfo['data_size'] == finfo['data_size']:
312
+ self.pglog(tofile + ": Target exists with same size, skip copying", self.LOGWRN)
313
+ return 0
314
+ if not self.local_copy_local(tofile, path, self.LGWNEX): return 0
315
+ self.set_drop_group(tofile)
316
+ return 1
317
+
318
+ # main function to execute this script
319
+ def main():
320
+ """Entry point: instantiate GdexDrop, parse arguments, run, and exit."""
321
+ from rda_python_setuid.setup_guide import show_setup_guide
322
+ object = GdexDrop()
323
+ show_setup_guide(object, 'rda_python_miscs', ['gdexcp', 'gdexdrop', 'gdexkill', 'gdexmod'])
324
+ object.read_parameters()
325
+ object.start_actions()
326
+ object.pgexit(0)
327
+
328
+ # call main() to start program
329
+ if __name__ == "__main__": main()
@@ -0,0 +1,79 @@
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' and belong to the GDEX group, as do any
44
+ sub-directories created for -t; without that they would stay in the calling
45
+ user's group, since only the user is switched to 'gdexdata', not the group.
46
+
47
+ Access List
48
+ -----------
49
+
50
+ The access list is /glade/u/home/gdexdata/config/gdexdrop.conf. It must be
51
+ owned by 'gdexdata' and must not be writable by group or others. Each line
52
+ is a login name, a colon, and the colon-separated dataset IDs that login may
53
+ drop into, or 'all' for every dataset. '#' starts a comment:
54
+
55
+ # login: dsid1[:...:dsidn]
56
+ jdoe: d123456:d654321
57
+ asmith: d111222
58
+ gdexhelp: all
59
+
60
+ A login absent from the list cannot drop into any dataset. 'gdexdata' itself
61
+ is always allowed and needs no entry.
62
+
63
+ Examples:
64
+
65
+ 1. Drop two files into the top of a dataset directory:
66
+
67
+ gdexdrop -ds d123456 hourly.202601.nc hourly.202602.nc
68
+
69
+ 2. Drop the same files into a sub-directory of the dataset:
70
+
71
+ gdexdrop -ds d123456 -t 2026/monthly hourly.2026*.nc
72
+
73
+ 3. Drop a whole directory tree, replacing files already there:
74
+
75
+ gdexdrop -ds d123456 -t archive -O /path/to/mydir
76
+
77
+ 4. Drop a long list of files named in a file, with group-writable modes:
78
+
79
+ 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.7
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,38 @@ 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
+ Everything dropped, including sub-directories created for `-t`, is also set to the
176
+ GDEX group `PGLOG['GDEXGRP']`. The setuid wrapper switches only the user to
177
+ `gdexdata`, not the group, so without that step a dropped file would stay in the
178
+ calling user's group unless the directory it landed in carried the setgid bit.
179
+
180
+ Who may drop into which dataset is read from the access list
181
+ `/glade/u/home/gdexdata/config/gdexdrop.conf`. It must be owned by `gdexdata`
182
+ and must not be writable by group or others, otherwise `gdexdrop` refuses to
183
+ run. Each line is a login name, a colon, and the colon-separated dataset IDs
184
+ that login may drop into, or `all` for every dataset; `#` starts a comment:
185
+
186
+ ```
187
+ # login: dsid1[:...:dsidn]
188
+ jdoe: d123456:d654321
189
+ asmith: d111222
190
+ gdexhelp: all
191
+ ```
192
+
193
+ The root path, the access list path and the log path are compiled into the
194
+ program rather than taken from `PGLOG`, because `PGLOG['DSDHOME']`,
195
+ `PGLOG['DSSHOME']` and `PGLOG['LOGPATH']` are all settable from environment
196
+ variables of the same name, which the caller controls.
197
+
165
198
  ## Cold storage setup
166
199
 
167
200
  `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