Skip to content

Commit 47b9a6e

Browse files
committed
enable links to be initialized in post groups
1 parent 00d7c96 commit 47b9a6e

File tree

5 files changed

+148
-20
lines changed

5 files changed

+148
-20
lines changed

hsds/group_dn.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,19 @@ async def POST_Group(request):
132132
else:
133133
attrs = {}
134134

135+
if "links" in body:
136+
# initialize links
137+
links = body["links"]
138+
log.debug(f"POST Group with links: {links}")
139+
else:
140+
links = {}
141+
135142
group_json = {
136143
"id": group_id,
137144
"root": root_id,
138145
"created": now,
139146
"lastModified": now,
140-
"links": {},
147+
"links": links,
141148
"attributes": attrs,
142149
}
143150

@@ -153,7 +160,7 @@ async def POST_Group(request):
153160
resp_json["root"] = root_id
154161
resp_json["created"] = group_json["created"]
155162
resp_json["lastModified"] = group_json["lastModified"]
156-
resp_json["linkCount"] = 0
163+
resp_json["linkCount"] = len(links)
157164
resp_json["attributeCount"] = len(attrs)
158165

159166
resp = json_response(resp_json, status=201)

hsds/group_sn.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# group handler for service node of hsds cluster
1414
#
1515

16+
import time
17+
1618
from aiohttp.web_exceptions import HTTPBadRequest, HTTPForbidden, HTTPNotFound
1719
from json import JSONDecodeError
1820

@@ -23,11 +25,12 @@
2325
from .util.authUtil import validateUserPassword
2426
from .util.domainUtil import getDomainFromRequest, isValidDomain
2527
from .util.domainUtil import getBucketForDomain, getPathForDomain, verifyRoot
26-
from .util.linkUtil import validateLinkName
28+
from .util.linkUtil import validateLinkName, getLinkClass
2729
from .servicenode_lib import getDomainJson, getObjectJson, validateAction
2830
from .servicenode_lib import getObjectIdByPath, getPathForObjectId
2931
from .servicenode_lib import createObject, createObjectByPath, deleteObject
3032
from . import hsds_logger as log
33+
from . import config
3134

3235

3336
async def GET_Group(request):
@@ -189,6 +192,7 @@ async def POST_Group(request):
189192
h5path = None
190193
creation_props = None
191194
attrs = None
195+
links = None
192196

193197
if request.has_body:
194198
try:
@@ -236,28 +240,66 @@ async def POST_Group(request):
236240
creation_props = body["creationProperties"]
237241
if "attributes" in body:
238242
attrs = body["attributes"]
243+
if not isinstance(attrs, dict):
244+
msg = f"POST_Groups expected dict for for links, but got: {type(links)}"
245+
log.warn(msg)
246+
raise HTTPBadRequest(reason=msg)
239247
log.debug(f"POST Group attributes: {attrs}")
248+
if "links" in body:
249+
links = body["links"]
250+
if not isinstance(links, dict):
251+
msg = f"POST_Groups expected dict for for links, but got: {type(links)}"
252+
log.warn(msg)
253+
raise HTTPBadRequest(reason=msg)
254+
# validate the links
255+
now = time.time()
256+
257+
for title in links:
258+
try:
259+
validateLinkName(title)
260+
link_item = links[title]
261+
link_class = getLinkClass(link_item)
262+
if "class" in link_item:
263+
if link_class != link_item["class"]:
264+
msg = f"expected link class of: {link_class} but got {link_item}"
265+
log.warn(msg)
266+
raise HTTPBadRequest(reason=msg)
267+
else:
268+
link_item["class"] = link_class
269+
getLinkClass(link_item)
270+
if "created" in link_item:
271+
created = link_item["created"]
272+
# allow "pre-dated" attributes if recent enough
273+
predate_max_time = config.get("predate_max_time", default=10.0)
274+
if now - created > predate_max_time:
275+
link_item["created"] = created
276+
else:
277+
log.warn("stale created timestamp for link, ignoring")
278+
if "created" not in link_item:
279+
link_item["created"] = now
280+
281+
except ValueError:
282+
raise HTTPBadRequest(reason="invalid link item")
283+
284+
kwargs = {"bucket": bucket}
285+
if obj_id:
286+
kwargs["obj_id"] = obj_id
287+
if creation_props:
288+
kwargs["creation_props"] = creation_props
289+
if attrs:
290+
kwargs["attrs"] = attrs
291+
if links:
292+
kwargs["links"] = links
240293

241294
if parent_id:
242-
kwargs = {"bucket": bucket, "parent_id": parent_id, "h5path": h5path}
243-
if obj_id:
244-
kwargs["obj_id"] = obj_id
245-
if creation_props:
246-
kwargs["creation_props"] = creation_props
247-
if attrs:
248-
kwargs["attrs"] = attrs
295+
kwargs["parent_id"] = parent_id
296+
kwargs["h5path"] = h5path
249297
if implicit:
250298
kwargs["implicit"] = True
251299
group_json = await createObjectByPath(app, **kwargs)
252300
else:
253301
# create an anonymous group
254-
kwargs = {"bucket": bucket, "root_id": root_id}
255-
if obj_id:
256-
kwargs["obj_id"] = obj_id
257-
if creation_props:
258-
kwargs["creation_props"] = creation_props
259-
if attrs:
260-
kwargs["attrs"] = attrs
302+
kwargs["root_id"] = root_id
261303
group_json = await createObject(app, **kwargs)
262304

263305
log.debug(f"returning resp: {group_json}")

hsds/link_sn.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# service node of hsds cluster
1414
#
1515

16-
from aiohttp.web_exceptions import HTTPBadRequest
16+
from aiohttp.web_exceptions import HTTPBadRequest, HTTPInternalServerError
1717
from json import JSONDecodeError
1818

1919
from h5json.objid import isValidUuid, getCollectionForId
@@ -142,6 +142,10 @@ async def GET_Links(request):
142142

143143
# mix in collection key, target and hrefs
144144
for link in links:
145+
if "class" not in link:
146+
log.error("expected to find class key in link")
147+
raise HTTPInternalServerError()
148+
145149
if link["class"] == "H5L_TYPE_HARD":
146150
collection_name = getCollectionForId(link["id"])
147151
link["collection"] = collection_name

hsds/servicenode_lib.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,7 @@ async def createObject(app,
12941294
layout=None,
12951295
creation_props=None,
12961296
attrs=None,
1297+
links=None,
12971298
bucket=None):
12981299
""" create a group, ctype, or dataset object and return object json
12991300
Determination on whether a group, ctype, or dataset is created is based on:
@@ -1319,6 +1320,8 @@ async def createObject(app,
13191320
log.debug(f" cprops: {creation_props}")
13201321
if attrs:
13211322
log.debug(f" attrs: {attrs}")
1323+
if links:
1324+
log.debug(f" links: {links}")
13221325

13231326
if obj_id:
13241327
log.debug(f"using client supplied id: {obj_id}")
@@ -1347,8 +1350,13 @@ async def createObject(app,
13471350
attrs_json = {"attributes": attrs}
13481351
attr_items = await getAttributesFromRequest(app, attrs_json, **kwargs)
13491352
log.debug(f"got attr_items: {attr_items}")
1350-
13511353
obj_json["attributes"] = attr_items
1354+
if links:
1355+
if collection != "groups":
1356+
msg = "links can only be used with groups"
1357+
log.warn(msg)
1358+
raise HTTPBadRequest(reason=msg)
1359+
obj_json["links"] = links
13521360
log.debug(f"create {collection} obj, body: {obj_json}")
13531361
dn_url = getDataNodeUrl(app, obj_id)
13541362
req = f"{dn_url}/{collection}"
@@ -1368,6 +1376,7 @@ async def createObjectByPath(app,
13681376
layout=None,
13691377
creation_props=None,
13701378
attrs=None,
1379+
links=None,
13711380
bucket=None):
13721381

13731382
""" create an object at the designated path relative to the parent.
@@ -1394,6 +1403,12 @@ async def createObjectByPath(app,
13941403
log.debug(f" cprops: {creation_props}")
13951404
if attrs:
13961405
log.debug(f" attrs: {attrs}")
1406+
if links:
1407+
log.debug(f" links: {links}")
1408+
if obj_type:
1409+
msg = "only group objects can have links"
1410+
log.warn(msg)
1411+
raise HTTPBadRequest(reason=msg)
13971412

13981413
root_id = getRootObjId(parent_id)
13991414

@@ -1474,6 +1489,8 @@ async def createObjectByPath(app,
14741489
kwargs["creation_props"] = creation_props
14751490
if attrs:
14761491
kwargs["attrs"] = attrs
1492+
if links:
1493+
kwargs["links"] = links
14771494
if obj_id:
14781495
kwargs["obj_id"] = obj_id
14791496
obj_json = await createObject(app, **kwargs)

tests/integ/group_test.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def testPostWithLink(self):
316316
self.assertEqual(rspJson["linkCount"], 0)
317317
self.assertEqual(rspJson["attributeCount"], 0)
318318
new_group_id = rspJson["id"]
319-
self.assertTrue(helper.validateId(rspJson["id"]))
319+
self.assertTrue(helper.validateId(new_group_id))
320320
self.assertTrue(new_group_id != root_uuid)
321321

322322
# get root group and verify link count is 1
@@ -418,6 +418,64 @@ def testPostWithAttributes(self):
418418
self.assertTrue("attributes") in rspJson
419419
self.assertEqual(len(rspJson["attributes"]), attr_count)
420420

421+
def testPostWithLinks(self):
422+
# test POST with attribute initialization
423+
print("testPostWithLinks", self.base_domain)
424+
headers = helper.getRequestHeaders(domain=self.base_domain)
425+
426+
# get root id
427+
req = helper.getEndpoint() + "/"
428+
rsp = self.session.get(req, headers=headers)
429+
self.assertEqual(rsp.status_code, 200)
430+
rspJson = json.loads(rsp.text)
431+
root_uuid = rspJson["root"]
432+
helper.validateId(root_uuid)
433+
434+
# some objects to link
435+
link_count = 4
436+
links = {}
437+
req = helper.getEndpoint() + "/groups"
438+
439+
for i in range(link_count):
440+
rsp = self.session.post(req, headers=headers)
441+
self.assertEqual(rsp.status_code, 201)
442+
rspJson = json.loads(rsp.text)
443+
group_id = rspJson["id"]
444+
self.assertTrue(helper.validateId(group_id))
445+
links[f"obj_{i}"] = {"id": group_id}
446+
447+
# create new group
448+
payload = {"links": links, "link": {"id": root_uuid, "name": "g1"}}
449+
req = helper.getEndpoint() + "/groups"
450+
rsp = self.session.post(req, data=json.dumps(payload), headers=headers)
451+
self.assertEqual(rsp.status_code, 201)
452+
rspJson = json.loads(rsp.text)
453+
self.assertEqual(rspJson["linkCount"], link_count)
454+
self.assertEqual(rspJson["attributeCount"], 0)
455+
grp_id = rspJson["id"]
456+
helper.validateId(grp_id)
457+
458+
# fetch all the links
459+
req = helper.getEndpoint() + "/groups/" + grp_id + "/links"
460+
rsp = self.session.get(req, headers=headers)
461+
self.assertEqual(rsp.status_code, 200)
462+
rspJson = json.loads(rsp.text)
463+
464+
self.assertTrue("links" in rspJson)
465+
links_rsp = rspJson["links"]
466+
self.assertEqual(len(links_rsp), link_count)
467+
for i in range(link_count):
468+
link_rsp = links_rsp[i]
469+
self.assertTrue("class" in link_rsp)
470+
self.assertEqual(link_rsp["class"], "H5L_TYPE_HARD")
471+
self.assertTrue("id" in link_rsp)
472+
self.assertTrue("title" in link_rsp)
473+
self.assertEqual(link_rsp["title"], f"obj_{i}")
474+
self.assertTrue("collection" in link_rsp)
475+
self.assertEqual(link_rsp["collection"], "groups")
476+
self.assertTrue("target" in link_rsp)
477+
self.assertTrue("href" in link_rsp)
478+
421479
def testPostWithPath(self):
422480
# test POST with implicit parent group creation
423481
print("testPostWithPath", self.base_domain)

0 commit comments

Comments
 (0)