Commit 1b9889b3 authored by Sebastien Gougeaud's avatar Sebastien Gougeaud Committed by Thomas Leibovici
Browse files

cli: list objects

The new command 'phobos object list' will now print the list of the
stored objects, printing both their object ID and their user metadata.
The format '-f' option is available, and set the output format between
human, csv, json, etc.

Change-Id: Ifba483360ef68b92a7cdedd671f8e5848144fe99
Signed-off-by: default avatarSebastien Gougeaud <>

Tested-by: default avatarJenkins s8open_nr <>
Reviewed-by: default avatarThomas Leibovici <>
Reviewed-by: default avatarQuentin Bouget <>
parent 986afb27
......@@ -215,6 +215,16 @@ $ phobos getmd obj0123
##### Listing objects
To list objects, use `phobos object list`:
$ phobos object list -o oid,user_md
| oid | user_md |
| obj01 | {} |
| obj02 | {"user": "foo"} |
## Device and media management
### Listing resources
Any device or media can be listed using the 'list' operation. For instance,
......@@ -48,7 +48,7 @@ from phobos.core.cfg import load_file as cfg_load_file
from phobos.core.const import (PHO_LIB_SCSI, rsc_family2str,
from phobos.core.dss import Client as DSSClient
from phobos.core.ffi import DevInfo, MediaInfo, ResourceFamily
from phobos.core.ffi import DevInfo, MediaInfo, ObjectInfo, ResourceFamily
from phobos.core.ldm import LibAdapter
from phobos.core.log import LogControl, DISABLED, WARNING, INFO, VERBOSE, DEBUG
from import Client as XferClient, attrs_as_dict, PutParams
......@@ -528,6 +528,25 @@ class MediaListOptHandler(ListOptHandler):
"choose from {" + " ".join(attr) + "} "
"(default: %(default)s)")
class ObjectListOptHandler(ListOptHandler):
Specific version of the 'list' command for object, with a couple
def add_options(cls, parser):
"""Add object-specific options."""
super(ObjectListOptHandler, cls).add_options(parser)
attrs = list(ObjectInfo().get_display_dict().keys())
parser.add_argument('-o', '--output', type=lambda t: t.split(','),
help="attributes to output, comma-separated, "
"choose from {" + " ".join(attrs) + "} "
"default: %(default)s)")
class TapeAddOptHandler(MediaAddOptHandler):
"""Specific version of the 'add' command for tapes, with extra-options."""
......@@ -581,6 +600,39 @@ class BaseResourceOptHandler(DSSInteractHandler):
"""Return a list of objects that match the provided identifier."""
raise NotImplementedError("Abstract method subclasses must implement")
class ObjectOptHandler(BaseResourceOptHandler):
"""Shared interface for objects."""
label = 'object'
descr = 'handle objects'
verbs = [
def exec_list(self):
"""List objects."""
attrs = list(ObjectInfo().get_display_dict().keys())
attrs.extend(['*', 'all'])
out_attrs = self.params.get('output')
bad_attrs = set(out_attrs).difference(set(attrs))
if bad_attrs:
self.logger.error("Bad output attributes: %s", " ".join(bad_attrs))
objs = []
if self.params.get('res'):
for obj in self.params.get('res'):
curr = self.client.objects.get(oid=obj)
if not curr:
assert len(curr) == 1
objs = self.client.objects.get()
if len(objs) > 0:
dump_object_list(objs, 'object', attr=out_attrs,
class DeviceOptHandler(BaseResourceOptHandler):
"""Shared interface for devices."""
verbs = [
......@@ -1015,6 +1067,7 @@ class PhobosActionContext(object):
# Store command interfaces
......@@ -38,8 +38,8 @@ from phobos.core.const import PHO_ADDR_HASH1
from phobos.core.const import PHO_RSC_ADM_ST_LOCKED, PHO_RSC_ADM_ST_UNLOCKED
from phobos.core.const import DSS_SET_INSERT, DSS_SET_UPDATE, DSS_SET_DELETE
from phobos.core.ffi import DevInfo, Id, MediaInfo, MediaStats, Resource
from phobos.core.ffi import LIBPHOBOS
from phobos.core.ffi import (DevInfo, Id, MediaInfo, MediaStats, ObjectInfo,
Resource, LIBPHOBOS)
from phobos.core.ldm import ldm_device_query
......@@ -59,6 +59,7 @@ OBJECT_PREFIXES = {
'device': 'DSS::DEV::',
'layout': 'DSS::EXT::',
'media': 'DSS::MDA::',
'object': 'DSS::OBJ::',
class JSONFilter(Structure):
......@@ -139,13 +140,13 @@ class DSSResult(object):
item._dss_result_parent_link = self
return item
class BaseObjectManager(object):
class BaseEntityManager(object):
"""Proxy to manipulate (CRUD) objects in DSS."""
__metaclass__ = ABCMeta
def __init__(self, client, *args, **kwargs):
"""Initialize new instance."""
super(BaseObjectManager, self).__init__(*args, **kwargs)
super(BaseEntityManager, self).__init__(*args, **kwargs)
self.logger = logging.getLogger(__name__)
self.client = client
......@@ -249,7 +250,7 @@ class BaseObjectManager(object):
raise EnvironmentError(rc, err_message)
class DeviceManager(BaseObjectManager):
class DeviceManager(BaseEntityManager):
"""Proxy to manipulate devices."""
wrapped_class = DevInfo
wrapped_ident = 'device'
......@@ -308,7 +309,7 @@ class DeviceManager(BaseObjectManager):
None if force else self.lock_owner,
class MediaManager(BaseObjectManager):
class MediaManager(BaseEntityManager):
"""Proxy to manipulate media."""
wrapped_class = MediaInfo
wrapped_ident = 'media'
......@@ -364,6 +365,23 @@ class MediaManager(BaseObjectManager):
None if force else self.lock_owner,
class ObjectManager(BaseEntityManager):
"""Proxy to manipulate object."""
wrapped_class = ObjectInfo
wrapped_ident = 'object'
def __init__(self, *args, **kwargs):
super(ObjectManager, self).__init__(*args, **kwargs)
def _dss_get(self, hdl, qry_filter, res, res_cnt):
"""Invoke object-specific DSS get method."""
return LIBPHOBOS.dss_object_get(hdl, qry_filter, res, res_cnt)
def _dss_set(self, hdl, obj, obj_cnt, opcode):
"""Not supported."""
raise EnvironmentError(errno.ENOTSUP,
"Operation not supposed to be used")
class DSSHandle(Structure):
Wrap connection to the backend. Absolutely opaque and propagated everywhere.
......@@ -380,6 +398,7 @@ class Client(object):
self.handle = None = MediaManager(self)
self.devices = DeviceManager(self)
self.objects = ObjectManager(self)
def __enter__(self):
"""Enter a runtime context"""
......@@ -325,6 +325,20 @@ class MediaInfo(Structure, CLIManagedResourceMixin):
if hasattr(self, "_free_tags") and self._free_tags:
class ObjectInfo(Structure, CLIManagedResourceMixin):
"""Object descriptor."""
_fields_ = [
('oid', c_char_p),
('user_md', c_char_p)
def get_display_fields(self):
"""Return a dict of available fields and optional display formatters."""
return {
'oid': None,
'user_md': None,
class Timeval(Structure):
"""standard struct timeval."""
_fields_ = [
......@@ -63,7 +63,7 @@ def xml_dump(data, item_type='item'):
def human_dump(data):
"""Convert a list of dictionaries to an identifier list text"""
out = "\n".join(str(item['name']) for item in data)
out = "\n".join(str(item[item.keys()[0]]) for item in data)
return out
def human_pretty_dump(data):
......@@ -111,7 +111,7 @@ def dump_object_list(objs, item_type, attr=None, fmt="human"):
'human': human_dump,
if attr != ['name']:
if len(attr) > 1 or attr == ['*'] or attr == ['all']:
formats['human'] = human_pretty_dump
objlist = filter_display_dict(objs, attr)
......@@ -84,6 +84,7 @@ class CLIParametersTest(unittest.TestCase):
self.check_cmdline_exit(['tape', '-h'])
self.check_cmdline_exit(['drive', '-h'])
self.check_cmdline_exit(['lib', '-h'])
self.check_cmdline_exit(['object', '-h'])
def test_cli_basic(self):
"""test simple valid and invalid command lines."""
......@@ -98,6 +99,13 @@ class CLIParametersTest(unittest.TestCase):
self.check_cmdline_valid(['tape', 'list', 'I,J,K', '-o', '*'])
self.check_cmdline_valid(['tape', 'list', 'I,J,K', '-o',
self.check_cmdline_valid(['object', 'list'])
self.check_cmdline_valid(['object', 'list', 'oid'])
self.check_cmdline_valid(['object', 'list', 'oid1', 'oid2', 'oid3'])
self.check_cmdline_valid(['object', 'list', '-o', 'all'])
self.check_cmdline_valid(['object', 'list', '-o', '*'])
self.check_cmdline_valid(['object', 'list', '-o', 'oid'])
self.check_cmdline_valid(['object', 'list', '-o', 'all', 'oid'])
# Test invalid object and invalid verb
self.check_cmdline_exit(['voynichauthor', 'list'], code=2)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment