Skip to content

Commit 921ed27

Browse files
authored
Merge pull request #6562 from grondo/relative-uris
python: support relative and absolute path-like targets in jobid URI resolver
2 parents 01cad95 + f1c3ded commit 921ed27

File tree

5 files changed

+129
-40
lines changed

5 files changed

+129
-40
lines changed

doc/man1/flux-uri.rst

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,21 @@ URI SCHEMES
8989

9090
The following URI schemes are included by default:
9191

92-
jobid:ID[/ID...]
92+
jobid:PATH
9393
This scheme attempts to get the URI for a Flux instance running as a
9494
job in the current enclosing instance. This is the assumed scheme if no
9595
``scheme:`` is provided in *TARGET* passed to :program:`flux uri`, so the
96-
``jobid:`` prefix is optional. A hierarchy of Flux jobids is supported,
97-
so ``f1234/f3456`` will resolve the URI for job ``f3456`` running in
98-
job ``f1234`` in the current instance. This scheme will raise an error
99-
if the target job is not running.
96+
``jobid:`` prefix is optional. *PATH* is a hierarchical path expression
97+
that may contain an optional leading slash (``/``) (which references
98+
the top-level, root instance explicitly), followed by zero or more job
99+
IDs separated by slashes. The special IDs ``.`` and ``..`` indicate
100+
the current instance (within the hierarchy) and its parent, respectively.
101+
This allows resolution of a single job running in the current instance
102+
via ``f1234``, explicitly within the root instance via ``/f2345``, or
103+
a job running within another job via ``f3456/f789``. Completely relative
104+
paths can also be used such as ``..`` to get the URI of the current
105+
parent, or ``../..`` to get the URI of the parent's parent. Finally,
106+
a single slash (``/``) may be used to get the root instance URI.
100107

101108
The ``jobid`` scheme supports the optional query parameter ``?wait``, which
102109
causes the resolver to wait until a URI has been posted to the job eventlog
@@ -150,6 +157,13 @@ Get the URI of a nested job:
150157
the last component of the jobid "path" or hierarchy. This will resolve
151158
each URI in turn as a local URI.
152159

160+
Get the URI of the root instance from within a job running at any depth:
161+
162+
::
163+
164+
$ flux uri /
165+
local:///run/flux/local
166+
153167
Get the URI of a local flux-broker
154168

155169
::

src/bindings/python/flux/uri/resolvers/jobid.py

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,13 @@
99
###############################################################
1010

1111
import os
12-
from pathlib import PurePath
12+
from pathlib import PurePosixPath
1313

1414
import flux
1515
from flux.job import JobID, job_list_id
1616
from flux.uri import JobURI, URIResolverPlugin, URIResolverURI
1717

1818

19-
def filter_slash(iterable):
20-
return list(filter(lambda x: "/" not in x, iterable))
21-
22-
2319
def wait_for_uri(flux_handle, jobid):
2420
"""Wait for memo event containing job uri, O/w finish event"""
2521
for event in flux.job.event_watch(flux_handle, jobid):
@@ -30,41 +26,78 @@ def wait_for_uri(flux_handle, jobid):
3026
return None
3127

3228

29+
def resolve_parent(handle):
30+
"""Return parent-uri if instance-level > 0, else local-uri"""
31+
if int(handle.attr_get("instance-level")) > 0:
32+
return handle.attr_get("parent-uri")
33+
return handle.attr_get("local-uri")
34+
35+
36+
def resolve_root(flux_handle):
37+
"""Return the URI of the top-level, or root, instance."""
38+
handle = flux_handle
39+
while int(handle.attr_get("instance-level")) > 0:
40+
handle = flux.Flux(resolve_parent(handle))
41+
return handle.attr_get("local-uri")
42+
43+
44+
def resolve_jobid(flux_handle, arg, wait):
45+
try:
46+
jobid = JobID(arg)
47+
except OSError as exc:
48+
raise ValueError(f"{arg} is not a valid jobid")
49+
50+
try:
51+
if wait:
52+
uri = wait_for_uri(flux_handle, jobid)
53+
else:
54+
# Fetch the jobinfo object for this job
55+
job = job_list_id(
56+
flux_handle, jobid, attrs=["state", "annotations"]
57+
).get_jobinfo()
58+
if job.state != "RUN":
59+
raise ValueError(f"jobid {arg} is not running")
60+
uri = job.user.uri
61+
except FileNotFoundError as exc:
62+
raise ValueError(f"jobid {arg} not found") from exc
63+
64+
if uri is None or str(uri) == "":
65+
raise ValueError(f"URI not found for job {arg}")
66+
return uri
67+
68+
3369
class URIResolver(URIResolverPlugin):
3470
"""A URI resolver that attempts to fetch the remote_uri for a job"""
3571

3672
def describe(self):
3773
return "Get URI for a given Flux JOBID"
3874

39-
def _do_resolve(self, uri, flux_handle, force_local=False, wait=False):
75+
def _do_resolve(
76+
self, uri, flux_handle, force_local=False, wait=False, hostname=None
77+
):
4078
#
41-
# Convert a possible hierarchy of jobids to a list, dropping any
42-
# extraneous '/' (e.g. //id0/id1 -> [ "id0", "id1" ]
43-
jobids = filter_slash(PurePath(uri.path).parts)
79+
# Convert a possible hierarchy of jobids to a list
80+
jobids = list(PurePosixPath(uri.path).parts)
81+
82+
# If path is empty, return current enclosing URI
83+
if not jobids:
84+
return flux_handle.attr_get("local-uri")
4485

45-
# Pop the first jobid off the list, this id should be local:
86+
# Pop the first jobid off the list, if a jobid it should be local,
87+
# otherwise "/" for the root URI or ".." for parent URI:
4688
arg = jobids.pop(0)
47-
try:
48-
jobid = JobID(arg)
49-
except OSError as exc:
50-
raise ValueError(f"{arg} is not a valid jobid")
51-
52-
try:
53-
if wait:
54-
uri = wait_for_uri(flux_handle, jobid)
55-
else:
56-
# Fetch the jobinfo object for this job
57-
job = job_list_id(
58-
flux_handle, jobid, attrs=["state", "annotations"]
59-
).get_jobinfo()
60-
if job.state != "RUN":
61-
raise ValueError(f"jobid {arg} is not running")
62-
uri = job.user.uri
63-
except FileNotFoundError as exc:
64-
raise ValueError(f"jobid {arg} not found") from exc
65-
66-
if uri is None or str(uri) == "":
67-
raise ValueError(f"URI not found for job {arg}")
89+
if arg == "/":
90+
uri = resolve_root(flux_handle)
91+
elif arg == "..":
92+
uri = resolve_parent(flux_handle)
93+
# Relative paths always use a local:// uri. But, if a jobid was
94+
# resolved earlier in the path, then use the hostname associated
95+
# with that job.
96+
if hostname:
97+
uri = JobURI(uri, remote_hostname=hostname).remote
98+
else:
99+
uri = resolve_jobid(flux_handle, arg, wait)
100+
hostname = JobURI(uri).netloc
68101

69102
# If there are more jobids in the hierarchy to resolve, resolve
70103
# them recursively
@@ -74,7 +107,10 @@ def _do_resolve(self, uri, flux_handle, force_local=False, wait=False):
74107
if force_local:
75108
uri = JobURI(uri).local
76109
return self._do_resolve(
77-
resolver_uri, flux.Flux(uri), force_local=force_local
110+
resolver_uri,
111+
flux.Flux(uri),
112+
force_local=force_local,
113+
hostname=hostname,
78114
)
79115
return uri
80116

src/bindings/python/flux/uri/uri.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,26 +57,34 @@ class JobURI(URI):
5757
remote: If local URI, returns a remote URI substituting current hostname.
5858
If a remote URI, returns the URI.
5959
local: If a remote URI, convert to a local URI. Otherwise return the URI.
60+
61+
Args:
62+
uri (str): The URI string with which to initialize the JobURI instance.
63+
remote_hostname (str): If ``uri`` is a local URI, use the provided
64+
hostname instead of the current hostname when rendering the remote
65+
URI.
6066
"""
6167

6268
force_local = os.environ.get("FLUX_URI_RESOLVE_LOCAL", False)
6369

64-
def __init__(self, uri):
70+
def __init__(self, uri, remote_hostname=None):
6571
super().__init__(uri)
6672
if self.scheme == "":
6773
raise ValueError(f"JobURI '{uri}' does not have a valid scheme")
6874
self.path = re.sub("/+", "/", self.path)
6975
self.remote_uri = None
7076
self.local_uri = None
77+
self.remote_hostname = remote_hostname
7178

7279
@property
7380
def remote(self):
7481
if not self.remote_uri:
7582
if self.scheme == "ssh":
7683
self.remote_uri = self.uri
7784
elif self.scheme == "local":
78-
hostname = platform.uname()[1]
79-
self.remote_uri = f"ssh://{hostname}{self.path}"
85+
if not self.remote_hostname:
86+
self.remote_hostname = platform.uname()[1]
87+
self.remote_uri = f"ssh://{self.remote_hostname}{self.path}"
8088
else:
8189
raise ValueError(
8290
f"Cannot convert JobURI with scheme {self.scheme} to remote"

t/python/t0025-uri.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ def test_parse_local(self):
4545
self.assertEqual(uri.fragment, "")
4646
self.assertEqual(uri.params, "")
4747

48+
def test_parse_local_with_remote_hostname(self):
49+
hostname = "fakehost"
50+
uri = JobURI("local:///tmp/foo", remote_hostname=hostname)
51+
self.assertEqual(uri.uri, "local:///tmp/foo")
52+
self.assertEqual(str(uri), "local:///tmp/foo")
53+
self.assertEqual(uri.remote, f"ssh://{hostname}/tmp/foo")
54+
self.assertEqual(uri.local, "local:///tmp/foo")
55+
self.assertEqual(uri.scheme, "local")
56+
self.assertEqual(uri.netloc, "")
57+
self.assertEqual(uri.path, "/tmp/foo")
58+
self.assertEqual(uri.query, "")
59+
self.assertEqual(uri.fragment, "")
60+
self.assertEqual(uri.params, "")
61+
4862
def test_parse_errors(self):
4963
with self.assertRaises(ValueError):
5064
JobURI("foo:///tmp/bar").remote

t/t2802-uri-cmd.t

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,23 @@ test_expect_success 'flux uri resolves hierarchical jobids with ?local' '
108108
test_debug "echo ${jobid}/${jobid2}?local is ${uri}"
109109
110110
'
111+
test_expect_success 'flux uri works with relative paths' '
112+
root_uri=$(FLUX_SSH=$testssh flux uri --local .) &&
113+
job1_uri=$(FLUX_SSH=$testssh flux uri --local ${jobid}) &&
114+
job2_uri=$(FLUX_SSH=$testssh flux uri --local ${jobid}/${jobid2}) &&
115+
uri=$(FLUX_SSH=$testssh flux proxy $job2_uri flux uri /) &&
116+
test_debug "echo flux uri / got ${uri} expected ${root_uri}" &&
117+
test "$uri" = "$root_uri" &&
118+
uri=$(FLUX_SSH=$testssh flux proxy $job2_uri flux uri ../..) &&
119+
test_debug "echo flux uri ../.. got ${uri} expected ${root_uri}" &&
120+
test "$uri" = "$root_uri" &&
121+
uri=$(FLUX_SSH=$testssh flux proxy $job2_uri flux uri ..) &&
122+
test_debug "echo flux uri .. got ${uri} expected ${job1_uri}" &&
123+
test "$uri" = "$job1_uri" &&
124+
uri=$(FLUX_SSH=$testssh flux proxy $job2_uri flux uri .) &&
125+
test_debug "echo flux uri . got ${uri} expected ${job2_uri}" &&
126+
test "$uri" = "$job2_uri"
127+
'
111128
test_expect_success 'flux uri --wait can resolve URI for pending job' '
112129
uri=$(flux uri --wait $(flux batch -n1 --wrap hostname)) &&
113130
flux job wait-event -vt 30 $(flux job last) clean &&

0 commit comments

Comments
 (0)