Service examples#
The Storage API allows data exposure from non-standard storage backends to services using the Storage API. For instance, local NAS data or an on-premise database can be made available to clients via a bespoke Storage API instance implementing the specified functions for that particular backend.
REST service instance example#
This section demonstrates a simple REST service for the Storage API using Python and FastAPI.
Implementing stat#
First, install fastapi, pydantic, asyncio, and uvicorn:
pip install fastapi pydantic uvicorn asyncio
Next, implement a service offering the stat endpoint, serving content of a local disk directory via the Storage API. We assume the directory to be served is stored in the global variable STATIC_DIR:
@app.head("/fileobject/by-address/{resource_address:path}")
async def stat_api(resource_address: str):
path = os.path.join(STATIC_DIR, resource_address)
if os.path.exists(path):
size = os.path.getsize(path)
modification_time = datetime.fromtimestamp(os.path.getmtime(path), tz=timezone.utc).isoformat()
metadata = Metadata(data_object_size=size, last_modified_timestamp=modification_time)
return JSONResponse(
status_code=204,
content=None,
headers={
"x-nvidia-omniverse-storage-metadata": metadata.model_dump_json(),
"x-nvidia-omniverse-storage-resource-identity": create_identity(resource_address, modification_time),
},
)
else:
raise HTTPException(status_code=404, detail=f"{path} not found on disk")
The code checks if the path exists, raises an HTTP 404 if not, gathers required metadata, and builds a JSON response.
Pydantic mapping is used for automatic serialization and type checking:
class Metadata(BaseModel):
data_object_size: int
last_modified_timestamp: Optional[str] = None
The resource identity is defined as the combination of the address (relative file path) and modification date, encoded as a JSON data package and encoded using base64. This is a design choice to emphasize that the resource identity’s format is storage service implementation specific and the client should make no assumption about its content.
def create_identity(relative_path: str, modification_time: str) -> str:
return binascii.b2a_base64(json.dumps({"p": relative_path, "t": modification_time}).encode("utf-8")).decode("utf-8").strip()
Running the service example is done with uvicorn:
server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=10))
add_FileObjectServiceServicer_to_server(FileSystemServiceServicer(), server)
server.add_insecure_port("[::]:50051")
server.start()
print(f"GRPC Server listening on port 50051 serving directory '{STATIC_DIR}'")
server.wait_for_termination()
Test the service with curl, here we have placed a file hello.txt in the STATIC_DIR being served:
> curl --head localhost:8011/object-store/by-address/hello.txt
HTTP/1.1 204 No Content
date: Fri, 28 Jun 2024 13:17:37 GMT
server: uvicorn
x-nvidia-omniverse-storage-metadata: {"resource_address":"hello.txt","resource_identity":"eyJwIjogImhlbGxvLnR4dCIsIC
J0IjogIjIwMjQtMDYtMjhUMTM6MTI6MzUuNzY3MTM4KzAwOjAwIn0=",
"metadata":{"data_object_size":12,"last_modified_timestamp":"2024-06-28T13:12:35.767138+00:00"}}
content-type: application/json
Direct download implementation#
The read operation streams the file back via the HTTP response body:
@app.get("/fileobject/by-identity/{resource_identity:path}")
async def read(resource_identity: str, download_preference: Optional[str] = None):
relative_path = path_from_identity(urllib.parse.unquote_plus(resource_identity))
path = os.path.join(STATIC_DIR, relative_path)
if os.path.exists(path):
def load_file():
with open(path, "rb") as f:
while chunk := f.read(1024):
yield chunk
return StreamingResponse(load_file(), media_type="application/octet-stream")
else:
raise HTTPException(status_code=404, detail=f"{path} not found on disk")
Use the previous stat operation result to download the file content from the Storage API service:
curl --get localhost:8011/object-store/by-identity/eyJwIjogImhlbGxvLnR4dCIsICJ0IjogIjIwMjQtMDYtMjhUMTM6MTI6MzUuNzY3MTM4KzAwOjAwIn0= -v
* Trying [::1]:8011...
* Trying 127.0.0.1:8011...
* Connected to localhost (127.0.0.1) port 8011
> GET /object-store/by-identity/eyJwIjogImhlbGxvLnR4dCIsICJ0IjogIjIwMjQtMDYtMjhUMTM6MTI6MzUuNzY3MTM4KzAwOjAwIn0= HTTP/1.1
> Host: localhost:8011
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< date: Fri, 28 Jun 2024 13:32:37 GMT
< server: uvicorn
< content-type: application/octet-stream
< transfer-encoding: chunked
<
Hello from your demo server, this is the content of the file hello.txt!
* Connection #0 to host localhost left intact
gRPC service example#
For the gRPC service, generate Python language bindings from the proto files. The client example section contains detailed information on that.
gRPC Stat#
The gRPC service is implemented by subclassing the base class and implementing the methods:
from nvidia.omniverse.storage.fileobject.v1alpha.fileobject_service_pb2_grpc import (
FileObjectServiceServicer,
add_FileObjectServiceServicer_to_server,
)
from service_utils import (
STATIC_DIR,
args,
create_identity,
)
class FileSystemServiceServicer(FileObjectServiceServicer):
def Stat(self, request, context):
path = os.path.join(STATIC_DIR, request.resource_address)
if os.path.exists(path):
# First build the ResourceIdentity from resource address and modification time
modification_time = os.path.getmtime(path)
modification_time_iso = datetime.fromtimestamp(modification_time, tz=timezone.utc).isoformat()
resource_identity = fileobject_pb2.ResourceIdentity(
encoded_identity=create_identity(request.resource_address, modification_time_iso)
)
# Then create the Metadata info using google Timestamp
stat_result = os.stat(path)
modification_time_seconds = int(stat_result.st_mtime)
nanos = int((stat_result.st_mtime - modification_time_seconds) * 1e9)
timestamp = timestamp_pb2.Timestamp(seconds=int(stat_result.st_mtime), nanos=nanos)
metadata = fileobject_pb2.Metadata(data_object_size=stat_result.st_size, last_modified_timestamp=timestamp)
# Now return the result
return fileobject_service_pb2.StatResponse(
resource_info=fileobject_pb2.ResourceInfo(resource_identity=resource_identity, metadata=metadata)
)
else:
context.abort(grpc.StatusCode.NOT_FOUND, f"file not found: {path}")
The server main loop is:
server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=10))
add_FileObjectServiceServicer_to_server(FileSystemServiceServicer(), server)
server.add_insecure_port("[::]:50051")
server.start()
print(f"GRPC Server listening on port 50051 serving directory '{STATIC_DIR}'")
server.wait_for_termination()
Run the server to get a working gRPC server in plaintext mode, queryable via grpcurl for example:
grpcurl -plaintext -d "{\"resource_address\": \"hello.txt\"}" -import-path c:\src\test_impl\storage-protos\proto\nvidia\omniverse\storage\fileobject\v1beta -import-path c:\src\test_impl\storage-protos\proto -proto fileobject_service.proto localhost:50051 nvidia.omniverse.storage.fileobject.v1beta.FileObjectService.Stat
The expected reply is:
{
"resourceInfo": {
"resourceIdentity": {
"encodedIdentity": "eyJwIjogImhlbGxvLnR4dCIsICJ0IjogIjIwMjQtMDYtMjhUMTM6MTI6MzUuNzY3MTM4KzAwOjAwIn0="
},
"metadata": {
"dataObjectSize": "12",
"lastModifiedTimestamp": "2024-06-28T13:12:35.767138242Z"
}
}
}
gRPC reflection#
Enable reflection in the gRPC server to simplify the grpcurl command. Install the package:
pip install grpcio-reflection
Modify the gRPC server launch sequence to include the reflection extension:
server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=10))
add_FileObjectServiceServicer_to_server(FileSystemServiceServicer(), server)
# Enable reflection
from grpc_reflection.v1alpha import reflection
SERVICE_NAMES = (
nvidia.omniverse.storage.fileobject.v1alpha.fileobject_service_pb2.DESCRIPTOR.services_by_name["FileObjectService"].full_name,
reflection.SERVICE_NAME,
)
reflection.enable_server_reflection(SERVICE_NAMES, server)
server.add_insecure_port("[::]:50051")
server.start()
print(f"GRPC Server listening on port 50051 serving directory '{STATIC_DIR}'")
server.wait_for_termination()
Relaunch the server, query the enabled services:
> grpcurl -plaintext localhost:50051 list
grpc.reflection.v1beta.ServerReflection
nvidia.omniverse.storage.fileobject.v1beta.FileObjectService
Get more information on the individual functions:
> grpcurl -plaintext localhost:50051 describe nvidia.omniverse.storage.fileobject.v1beta.FileObjectService
nvidia.omniverse.storage.fileobject.v1beta.FileObjectService is a service:
service FileObjectService {
rpc Stat ( .nvidia.omniverse.storage.fileobject.v1beta.StatRequest ) returns ( .nvidia.omniverse.storage.fileobject.v1beta.StatResponse );
}
Call the Stat function:
> grpcurl -plaintext -d "{\"resource_address\": \"hello.txt\"}" localhost:50051 nvidia.omniverse.storage.fileobject.v1beta.FileObjectService.Stat