Skip to content

Commit 01cad95

Browse files
authored
Merge pull request #6573 from grondo/uri-pathlike
libflux: support ancestor paths as alternative to URI in `flux_open(3)` and `flux_open_ex(3)`
2 parents 16f7233 + e97e094 commit 01cad95

File tree

4 files changed

+269
-15
lines changed

4 files changed

+269
-15
lines changed

doc/man3/flux_open.rst

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,18 @@ to communicate with the Flux message broker. :func:`flux_open_ex` takes an
3333
optional pointer to a :type:`flux_error_t` structure which, when non-NULL, will
3434
be used to store any errors which may have otherwise gone to :var:`stderr`.
3535

36-
The :var:`uri` scheme (before "://") specifies the "connector" that will be used
37-
to establish the connection. The :var:`uri` path (after "://") is parsed by the
38-
connector. If :var:`uri` is NULL, the value of :envvar:`FLUX_URI` is used. If
39-
:envvar:`FLUX_URI` is not set, a compiled-in default URI is used.
36+
When set, the :var:`uri` argument may be a valid native URI, e.g. as
37+
returned from the :man1:`flux-uri` command or found in the :var:`local-uri`
38+
and :var:`parent-uri` broker attributes, or a path-like string referencing
39+
a possible instance ancestor like "/" for the top-level or root instance,
40+
or ".." for the parent instance. See `ANCESTOR PATHS`_ below.
41+
42+
If :var:`uri` is NULL, the value of :envvar:`FLUX_URI` is used.
43+
If :envvar:`FLUX_URI` is not set, a compiled-in default URI is used.
44+
45+
When :var:`uri` contains a native URI, the scheme (before "://") specifies
46+
the "connector" that will be used to establish the connection. The :var:`uri`
47+
path (after "://") is parsed by the connector.
4048

4149
*flags* is the logical "or" of zero or more of the following flags:
4250

@@ -89,10 +97,24 @@ original handle.
8997
the Flux message broker.
9098

9199

100+
ANCESTOR PATHS
101+
==============
102+
103+
As an alternative to a URI, the :func:`flux_open` and :func:`flux_open_ex`
104+
functions also take a path-like string indicating that a handle to an ancestor
105+
instance should be opened. This string follows the following rules:
106+
107+
- One or more ".." separated by "/" refer to a parent instance
108+
(possibly multiple levels up the hierarchy)
109+
- "." indicates the current instance
110+
- A single slash ("/") indicates the root instance
111+
- ".." at the root is not an error, it just returns a handle to the root
112+
instance
113+
92114
RETURN VALUE
93115
============
94116

95-
:func:`flux_open` and :func:`flux_clone` return a :type:`flux_t`` handle on
117+
:func:`flux_open` and :func:`flux_clone` return a :type:`flux_t` handle on
96118
success. On error, NULL is returned, with :var:`errno` set.
97119

98120

src/bindings/python/flux/core/handle.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ class Flux(Wrapper):
3232
Example:
3333
>>> flux.Flux()
3434
<flux.core.Flux object at 0x...>
35+
36+
Args:
37+
uri (str): A fully-qualified native path as returned by
38+
:man1:`flux-uri`, or a path-like string that references any
39+
ancestor in the current hierarchy, e.g. ``/`` refers to
40+
the root instance, ``..`` refers to the parent, ``../..``
41+
the parent's parent and so on. See :man3:`flux_open` for more
42+
details.
43+
flags (int): flags as described in :man3:`flux_open`.
3544
"""
3645

3746
# Thread local storage to hold a reactor_running boolean, indicating

src/common/libflux/handle.c

Lines changed: 189 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,184 @@ static char *strtrim (char *s, const char *trim)
171171
return *s ? s : NULL;
172172
}
173173

174+
/* Return local URI from either FLUX_URI environment variable, or
175+
* if that is not set, the builtin config. Caller must free result.
176+
*/
177+
static char *get_local_uri (void)
178+
{
179+
const char *uri;
180+
char *result = NULL;
181+
182+
if ((uri = getenv ("FLUX_URI")))
183+
return strdup (uri);
184+
if (asprintf (&result,
185+
"local://%s/local",
186+
flux_conf_builtin_get ("rundir", FLUX_CONF_INSTALLED)) < 0)
187+
result = NULL;
188+
return result;
189+
}
190+
191+
/* Return the current instance level as an integer using the
192+
* instance-level broker attribute.
193+
*/
194+
static int get_instance_level (flux_t *h)
195+
{
196+
long l;
197+
char *endptr;
198+
const char *level;
199+
200+
if (!(level = flux_attr_get (h, "instance-level")))
201+
return -1;
202+
203+
errno = 0;
204+
l = strtol (level, &endptr, 10);
205+
if (errno != 0 || *endptr != '\0') {
206+
errno = EINVAL;
207+
return -1;
208+
}
209+
if (l < 0 || l > INT_MAX) {
210+
errno = ERANGE;
211+
return -1;
212+
}
213+
return (int) l;
214+
}
215+
216+
/* Resolve the URI of an ancestor `n` levels up the hierarchy.
217+
* If n is 0, then the current default uri (FLUX_URI or builtin) is returned.
218+
* If n >= instance-level, then the URI of the root instance is returned.
219+
*/
220+
static char *resolve_ancestor_uri (flux_t *h, int n, flux_error_t *errp)
221+
{
222+
int level;
223+
int depth = 0;
224+
const char *uri = NULL;
225+
char *result = NULL;
226+
227+
if ((level = get_instance_level (h)) < 0) {
228+
errprintf (errp,
229+
"Failed to get instance-level attribute: %s",
230+
strerror (errno));
231+
return NULL;
232+
}
233+
234+
/* If n is 0 (resolve current uri) or level is 0 (we're already
235+
* in the root instance), return copy of local URI immediately:
236+
*/
237+
if (n == 0 || level == 0) {
238+
if (!(result = get_local_uri ())) {
239+
errprintf (errp, "Failed to get local URI");
240+
return NULL;
241+
}
242+
return result;
243+
}
244+
245+
/* Resolve to depth 0 unless n is > 0 (but ensure depth not < 0)
246+
*/
247+
if (n > 0 && (depth = level - n) < 0)
248+
depth = 0;
249+
250+
/* Take a reference on h since it will be closed below
251+
*/
252+
flux_incref (h);
253+
254+
while (!result) {
255+
flux_t *parent_h;
256+
257+
if (!(uri = flux_attr_get (h, "parent-uri"))
258+
|| !(parent_h = flux_open_ex (uri, 0, errp)))
259+
goto out;
260+
261+
if (--level == depth) {
262+
if (!(result = strdup (uri))) {
263+
errprintf (errp, "Out of memory");
264+
goto out;
265+
}
266+
}
267+
flux_close (h);
268+
h = parent_h;
269+
}
270+
out:
271+
flux_close (h);
272+
return result;
273+
}
274+
275+
/* Count the number of parent elements ".." separated by one or more "/"
276+
* in `path`. It is an error if anything but ".." or "." appears in each
277+
* element ("." is ignored and indicates "current instance").
278+
*
279+
* `path` should have already been checked for a leading slash (an error).
280+
*/
281+
static int count_parents (const char *path, flux_error_t *errp)
282+
{
283+
char *copy;
284+
char *str;
285+
char *s;
286+
char *sp = NULL;
287+
int n = 0;
288+
int rc = -1;
289+
290+
if (!(copy = strdup (path))) {
291+
errprintf (errp, "Out of memory");
292+
return -1;
293+
}
294+
str = copy;
295+
while ((s = strtok_r (str, "/", &sp))) {
296+
if (streq (s, ".."))
297+
n++;
298+
else if (!streq (s, ".")) {
299+
errprintf (errp, "%s: invalid URI path element '%s'", path, s);
300+
errno = EINVAL;
301+
goto out;
302+
}
303+
str = NULL;
304+
}
305+
rc = n;
306+
out:
307+
ERRNO_SAFE_WRAP (free, copy);
308+
return rc;
309+
}
310+
311+
/* Resolve a path-like string where one or more ".." separated by "/" specify
312+
* to resolve parent instance URIs (possibly multiple levels up), "."
313+
* indicates the current instance, and a single slash ("/") specifies the
314+
* root instance URI.
315+
*/
316+
static char *resolve_path_uri (const char *path, flux_error_t *errp)
317+
{
318+
char *uri = NULL;
319+
int nparents;
320+
flux_t *h;
321+
322+
/* Always start from current enclosing instance
323+
*/
324+
if (!(h = flux_open_ex (NULL, 0, errp)))
325+
return NULL;
326+
327+
if (path[0] == '/') {
328+
/* Leading slash only allowed if it is the only character in the
329+
* uri path string:
330+
*/
331+
if (!streq (path, "/")) {
332+
errprintf (errp, "%s: invalid URI", path);
333+
errno = EINVAL;
334+
goto out;
335+
}
336+
/* nparents < 0 means resolve to root
337+
*/
338+
nparents = -1;
339+
}
340+
else if ((nparents = count_parents (path, errp)) < 0)
341+
goto out;
342+
343+
uri = resolve_ancestor_uri (h, nparents, errp);
344+
out:
345+
flux_close (h);
346+
return uri;
347+
}
348+
174349
flux_t *flux_open_ex (const char *uri, int flags, flux_error_t *errp)
175350
{
176-
char *default_uri = NULL;
351+
char *tmp = NULL;
177352
char *path = NULL;
178353
char *scheme = NULL;
179354
void *dso = NULL;
@@ -197,15 +372,20 @@ flux_t *flux_open_ex (const char *uri, int flags, flux_error_t *errp)
197372

198373
/* Try to get URI from (in descending precedence):
199374
* argument > environment > builtin
375+
*
376+
* If supplied argument starts with "." or "/", then treat it as
377+
* a path-like argument which may reference an ancestor (as processed
378+
* by resolve_path_uri())
200379
*/
201-
if (!uri)
202-
uri = getenv ("FLUX_URI");
203380
if (!uri) {
204-
if (asprintf (&default_uri, "local://%s/local",
205-
flux_conf_builtin_get ("rundir",
206-
FLUX_CONF_INSTALLED)) < 0)
381+
if (!(tmp = get_local_uri ()))
382+
goto error;
383+
uri = tmp;
384+
}
385+
else if (uri[0] == '.' || uri[0] == '/') {
386+
if (!(tmp = resolve_path_uri (uri, errp)))
207387
goto error;
208-
uri = default_uri;
388+
uri = tmp;
209389
}
210390
if (!(scheme = strdup (uri)))
211391
goto error;
@@ -244,13 +424,13 @@ flux_t *flux_open_ex (const char *uri, int flags, flux_error_t *errp)
244424
goto error_handle;
245425
}
246426
free (scheme);
247-
free (default_uri);
427+
free (tmp);
248428
return h;
249429
error_handle:
250430
flux_handle_destroy (h);
251431
error:
252432
ERRNO_SAFE_WRAP (free, scheme);
253-
ERRNO_SAFE_WRAP (free, default_uri);
433+
ERRNO_SAFE_WRAP (free, tmp);
254434
/*
255435
* Fill errp->text with strerror only when a more specific error has not
256436
* already been set.

t/t3100-flux-in-flux.t

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ test_expect_success "flux --root works in subinstance" '
5454
id=$(flux batch -n1 --wrap \
5555
flux run flux start ${ARGS} \
5656
flux --root getattr instance-level) &&
57-
flux job wait-event -vt 30 $id clean &&
57+
flux job wait-event -vt 60 $id clean &&
5858
test_debug "cat flux-${id}.out" &&
5959
test "$(cat flux-${id}.out)" -eq 0
6060
'
@@ -106,4 +106,47 @@ test_expect_success 'flux can launch multiple brokers per node (R lookup fallbac
106106
--conf=resource.no-update-watch=true \
107107
flux resource info
108108
'
109+
test_expect_success 'flux_open("/") works at top-level' '
110+
flux python -c "import flux; print(flux.Flux(\"/\").attr_get(\"instance-level\"));"
111+
'
112+
test_expect_success 'flux_open(3) accepts path-like URIs: "/", "../.." etc' '
113+
cat <<-EOF >flux_open.py &&
114+
import unittest
115+
import flux
116+
import sys
117+
from collections import namedtuple
118+
119+
Test = namedtuple("Test", ["arg", "level"])
120+
tests = [
121+
Test(None, 3),
122+
Test(".", 3),
123+
Test("./", 3),
124+
Test("..", 2),
125+
Test("../", 2),
126+
Test("./..", 2),
127+
Test("../..", 1),
128+
Test("..//..", 1),
129+
Test("../../", 1),
130+
Test(".././..", 1),
131+
Test("../../..", 0),
132+
Test("../../../..", 0),
133+
Test("../../../../../../../", 0),
134+
Test("/", 0),
135+
]
136+
137+
class TestFluxOpen(unittest.TestCase):
138+
def test_pathlike_uris(self):
139+
for test in tests:
140+
l = flux.Flux(test.arg).attr_get("instance-level")
141+
self.assertEqual(int(l), test.level)
142+
143+
def test_pathlike_bad_arg(self):
144+
for arg in ("/.", "//", "../f", "../f/.."):
145+
with self.assertRaises(OSError, msg=f"arg={arg}"):
146+
h = flux.Flux(arg)
147+
print(h.attr_get("instance-level"))
148+
unittest.main(testRunner=unittest.TextTestRunner(verbosity=2))
149+
EOF
150+
flux alloc -n1 flux alloc -n1 flux alloc -n1 flux python ./flux_open.py
151+
'
109152
test_done

0 commit comments

Comments
 (0)