Browse Source

support mappings, that is one host has the same files as another...

this fleshes out hostuuid better..
add a more useful debug print..
main
John-Mark Gurney 2 years ago
parent
commit
356be8332b
3 changed files with 229 additions and 39 deletions
  1. +0
    -21
      ui/fixtures/cmd.hosts.json
  2. +100
    -0
      ui/fixtures/cmd.mapping.json
  3. +129
    -18
      ui/medashare/cli.py

+ 0
- 21
ui/fixtures/cmd.hosts.json View File

@@ -1,21 +0,0 @@
[
{
"title": "gen ident",
"cmd": [ "genident", "name=A Test User" ]
},
{
"special": "set hostid",
"comment": "and that a modified hostid",
"hostid": "ceaa4862-dd00-41ba-9787-7480ec1b2679"
},
{
"title": "that hosts lists itself",
"cmd": [ "hosts" ],
"stdout_re": ".*\tceaa4862-dd00-41ba-9787-7480ec1b2679\n"
},
{
"special": "verify store object cnt",
"comment": "and that the host object was created",
"count": 1
}
]

+ 100
- 0
ui/fixtures/cmd.mapping.json View File

@@ -0,0 +1,100 @@
[
{
"title": "gen ident",
"cmd": [ "genident", "name=A Test User" ]
},
{
"special": "set hostid",
"comment": "and that a modified hostid",
"hostid": "ceaa4862-dd00-41ba-9787-7480ec1b2679"
},
{
"title": "that hosts lists itself",
"cmd": [ "hosts" ],
"stdout_re": ".*\tceaa4862-dd00-41ba-9787-7480ec1b2679\n"
},
{
"special": "verify store object cnt",
"comment": "and that the host object was created",
"count": 1
},
{
"special": "set hostid",
"comment": "and that a different hostid",
"hostid": "efdb5d9c-d123-4b30-aaa8-45a9ea8f6053"
},
{
"title": "that hosts lists itself first",
"cmd": [ "hosts" ],
"stdout_re": ".*\tefdb5d9c-d123-4b30-aaa8-45a9ea8f6053\n.*\tceaa4862-dd00-41ba-9787-7480ec1b2679\n"
},
{
"special": "verify store object cnt",
"comment": "and that the host object was created",
"count": 2
},
{
"title": "that a mapping with unknown host errors",
"cmd": [ "mapping", "--create", "ceaa4862-dd00-41ba-9787-7480ec1b267a:/foo", "efdb5d9c-d123-4b30-aaa8-45a9ea8f6053:/bar" ],
"exit": 1,
"stderr": "ERROR: Unable to find host 'ceaa4862-dd00-41ba-9787-7480ec1b267a'\n"
},
{
"title": "that a host mapping that isn't absolute errors",
"cmd": [ "mapping", "--create", ".", "efdb5d9c-d123-4b30-aaa8-45a9ea8f6053:." ],
"stderr": "ERROR: host path must be absolute, is '.'.\n",
"exit": 1
},
{
"special": "setup mapping paths"
},
{
"title": "that a host mapping works",
"cmd": [ "mapping", "--create", "mapa", "ceaa4862-dd00-41ba-9787-7480ec1b2679:{mappathb}" ],
"format": [ "cmd" ]
},
{
"special": "verify store object cnt",
"comment": "and that the host object was created",
"count": 3
},
{
"title": "that it was created w/ correct values",
"cmd": [ "dump" ],
"format": [ "stdout_re" ],
"stdout_re": ".*\n.*\n.*mapping.*efdb5d9c-d123-4b30-aaa8-45a9ea8f6053:{mappatha}.*ceaa4862-dd00-41ba-9787-7480ec1b2679:{mappathb}.*type.*mapping.*\n"
},
{
"title": "that a file on one mapping can have metadata",
"cmd": [ "modify", "+sometag=value", "mapa/text.txt" ]
},
{
"special": "verify store object cnt",
"comment": "and that the file and metadata was created",
"count": 5
},
{
"special": "set hostid",
"comment": "that the other host",
"hostid": "ceaa4862-dd00-41ba-9787-7480ec1b2679"
},
{
"special": "delete files",
"format": [ "files" ],
"files": [ "{mappatha}/text.txt" ]
},
{
"title": "can see the other host's metadata",
"cmd": [ "list", "mapb/text.txt" ],
"stdout_re": ".*\n.*\nsometag:\tvalue\n"
},
{
"title": "and add it's own value",
"cmd": [ "modify", "+sometag=anothervalue", "mapb/text.txt" ]
},
{
"special": "verify store object cnt",
"comment": "but didn't create an additional FileObject",
"count": 5
}
]

+ 129
- 18
ui/medashare/cli.py View File

@@ -44,6 +44,18 @@ _NAMESPACE_MEDASHARE_CONTAINER = uuid.UUID('890a9d5c-0626-4de1-ab05-9e14947391eb
# useful for debugging when stderr is redirected/captured # useful for debugging when stderr is redirected/captured
_real_stderr = sys.stderr _real_stderr = sys.stderr


def _debprint(*args): # pragma: no cover
import traceback, sys, os.path
st = traceback.extract_stack(limit=2)[0]

sep = ''
if args:
sep = ':'

print('%s:%d%s' % (os.path.basename(st.filename), st.lineno, sep),
*args, file=_real_stderr)
sys.stderr.flush()

_defaulthash = 'sha512' _defaulthash = 'sha512'
_validhashes = set([ 'sha256', 'sha512' ]) _validhashes = set([ 'sha256', 'sha512' ])
_hashlengths = { len(getattr(hashlib, x)().hexdigest()): x for x in _hashlengths = { len(getattr(hashlib, x)().hexdigest()): x for x in
@@ -333,6 +345,11 @@ class Persona(object):


return self.sign(Host(*args, **kwargs)) return self.sign(Host(*args, **kwargs))


def Mapping(self, *args, **kwargs):
kwargs['created_by_ref'] = self.uuid

return self.sign(Mapping(*args, **kwargs))

def Container(self, *args, **kwargs): def Container(self, *args, **kwargs):
kwargs['created_by_ref'] = self.uuid kwargs['created_by_ref'] = self.uuid


@@ -478,6 +495,7 @@ class ObjectStore(object):
self._uuids = {} self._uuids = {}
self._hashes = {} self._hashes = {}
self._hostuuids = {} self._hostuuids = {}
self._hostmappings = []


def get_host(self, hostuuid): def get_host(self, hostuuid):
return self._hostuuids[hostuuid] return self._hostuuids[hostuuid]
@@ -569,7 +587,15 @@ class ObjectStore(object):
elif obj.type == 'host': elif obj.type == 'host':
self._uuids[obj.hostuuid] = obj self._uuids[obj.hostuuid] = obj
self._hostuuids[obj.hostuuid] = obj self._hostuuids[obj.hostuuid] = obj

elif obj.type == 'mapping':
hostid = _makeuuid(hostuuid())

maps = [ (lambda a, b: (uuid.UUID(a), pathlib.Path(b).resolve()))(*x.split(':', 1)) for x in obj.mapping ]
for idx, (id, path) in enumerate(maps):
if hostid == id:
# add other to mapping
other = tuple(maps[(idx + 1) % 2])
self._hostmappings.append((path, ) + other)
try: try:
hashes = obj.hashes hashes = obj.hashes
except AttributeError: except AttributeError:
@@ -623,7 +649,7 @@ class ObjectStore(object):
h = self.makehash(hash, strict=False) h = self.makehash(hash, strict=False)
return self._hashes[h] return self._hashes[h]


def get_metadata(self, fname, persona):
def get_metadata(self, fname, persona, create_metadata=True):
'''Get all MetaData objects for fname, or create one if '''Get all MetaData objects for fname, or create one if
not found. not found.


@@ -632,6 +658,9 @@ class ObjectStore(object):
A Persona must be passed in to create the FileObject and A Persona must be passed in to create the FileObject and
MetaData objects as needed. MetaData objects as needed.


A MetaData object will be created if create_metadata is
True, which is the default.

Note: if a new MetaData object is created, it is not Note: if a new MetaData object is created, it is not
stored in the database automatically. It is expected that stored in the database automatically. It is expected that
it will be modified and then saved, so call ObjectStore.loadobj it will be modified and then saved, so call ObjectStore.loadobj
@@ -640,12 +669,8 @@ class ObjectStore(object):


try: try:
fobj = self.by_file(fname, ('file',))[0] fobj = self.by_file(fname, ('file',))[0]
#print('x:', repr(objs), file=_real_stderr)
except KeyError: except KeyError:
#print('b:', repr(fname), file=_real_stderr)

fobj = persona.by_file(fname) fobj = persona.by_file(fname)
#print('c:', repr(fobj), file=_real_stderr)


self.loadobj(fobj) self.loadobj(fobj)


@@ -653,13 +678,19 @@ class ObjectStore(object):
try: try:
objs = self.by_file(fname) objs = self.by_file(fname)
except KeyError: except KeyError:
objs = [ persona.MetaData(hashes=fobj.hashes) ]
if create_metadata:
objs = [ persona.MetaData(hashes=fobj.hashes) ]
else:
objs = [ ]


return objs return objs


def by_file(self, fname, types=('metadata', )): def by_file(self, fname, types=('metadata', )):
'''Return a metadata object for the file named fname. '''Return a metadata object for the file named fname.


Will check the mapping database to get hashes, and possibly
return that FileObject if requested.

Will raise a KeyError if this file does not exist in Will raise a KeyError if this file does not exist in
the database. the database.


@@ -670,8 +701,26 @@ class ObjectStore(object):
fid = FileObject.make_id(fname) fid = FileObject.make_id(fname)


#print('bf:', repr(fid), file=_real_stderr) #print('bf:', repr(fid), file=_real_stderr)
fobj = self.by_id(fid)
fobj.verify()
try:
fobj = self.by_id(fid)
lclfile = None
except KeyError:
# check mappings
fname = pathlib.Path(fname).resolve()
for lclpath, hostid, rempath in self._hostmappings:
if fname.parts[:len(lclpath.parts)] == lclpath.parts:
try:
rempath = pathlib.Path(*rempath.parts + fname.parts[len(lclpath.parts):])
fid = FileObject.make_id(rempath, hostid)
fobj = self.by_id(fid)
lclfile = fname
break
except KeyError:
continue
else:
raise

fobj.verify(lclfile)


for i in fobj.hashes: for i in fobj.hashes:
j = self.by_hash(i) j = self.by_hash(i)
@@ -703,6 +752,9 @@ def _hashfile(fname):
class Host(MDBase): class Host(MDBase):
_type = 'host' _type = 'host'


class Mapping(MDBase):
_type = 'mapping'

class FileObject(MDBase): class FileObject(MDBase):
_type = 'file' _type = 'file'


@@ -713,14 +765,17 @@ class FileObject(MDBase):
} }


@staticmethod @staticmethod
def make_id(fname):
def make_id(fname, hostid=None):
'''Take a local file name, and make the id for it. Note that '''Take a local file name, and make the id for it. Note that
converts from the local path separator to a forward slash so converts from the local path separator to a forward slash so
that it will be the same between Windows and Unix systems.''' that it will be the same between Windows and Unix systems.'''


if hostid is None:
hostid = hostuuid()

fname = os.path.realpath(fname) fname = os.path.realpath(fname)
return uuid.uuid5(_NAMESPACE_MEDASHARE_PATH, return uuid.uuid5(_NAMESPACE_MEDASHARE_PATH,
str(hostuuid()) + '/'.join(os.path.split(fname)))
str(hostid) + '/'.join(os.path.split(fname)))


@classmethod @classmethod
def from_file(cls, filename, created_by_ref): def from_file(cls, filename, created_by_ref):
@@ -741,13 +796,17 @@ class FileObject(MDBase):


return cls(obj) return cls(obj)


def verify(self, complete=False):
def verify(self, lclfile=None):
'''Verify that this FileObject is still valid. It will '''Verify that this FileObject is still valid. It will
by default, only do a mtime verification. by default, only do a mtime verification.


It will raise a ValueError if the file does not match.''' It will raise a ValueError if the file does not match.'''


s = os.stat(os.path.join(self.dir, self.filename))
if lclfile is None:
s = os.stat(os.path.join(self.dir, self.filename))
else:
s = os.stat(lclfile)

mtimets = datetime.datetime.fromtimestamp(s.st_mtime, mtimets = datetime.datetime.fromtimestamp(s.st_mtime,
tz=datetime.timezone.utc).timestamp() tz=datetime.timezone.utc).timestamp()


@@ -919,6 +978,36 @@ def cmd_modify(options):
def printhost(host): def printhost(host):
print('%s\t%s' % (host.name, host.hostuuid)) print('%s\t%s' % (host.name, host.hostuuid))


def cmd_mapping(options):
persona, objstr = get_objstore(options)

if options.mapping is not None:
parts = [ x.split(':', 1) for x in options.mapping ]

if len(parts[0]) == 1:
parts[0] = [ hostuuid(), parts[0][0] ]

if parts[0][0] == hostuuid():
parts[0][1] = str(pathlib.Path(parts[0][1]).resolve())

if parts[1][1][0] != '/':
print('ERROR: host path must be absolute, is %s.' %
repr(parts[1][1][0]), file=sys.stderr)
sys.exit(1)

try:
[ objstr.get_host(x[0]) for x in parts ]
except KeyError as e:
print('ERROR: Unable to find host %s' %
repr(e.args[0]), file=sys.stderr)
sys.exit(1)

m = persona.Mapping(mapping=[ ':'.join(x) for x in parts ])

objstr.loadobj(m)

write_objstore(options, objstr)

def cmd_hosts(options): def cmd_hosts(options):
persona, objstr = get_objstore(options) persona, objstr = get_objstore(options)


@@ -987,11 +1076,6 @@ def cmd_list(options):
except (FileNotFoundError, KeyError) as e: except (FileNotFoundError, KeyError) as e:
print('ERROR: file not found: %s' % repr(i), file=sys.stderr) print('ERROR: file not found: %s' % repr(i), file=sys.stderr)
sys.exit(1) sys.exit(1)
except FileNotFoundError:
# XXX - tell the difference?
print('ERROR: file not found: %s' % repr(i),
file=sys.stderr)
sys.exit(1)


for j in objstr.by_file(i): for j in objstr.by_file(i):
for k, v in _iterdictlist(j): for k, v in _iterdictlist(j):
@@ -1149,6 +1233,11 @@ def main():
parser_hosts = subparsers.add_parser('hosts', help='dump all the hosts, self is always first') parser_hosts = subparsers.add_parser('hosts', help='dump all the hosts, self is always first')
parser_hosts.set_defaults(func=cmd_hosts) parser_hosts.set_defaults(func=cmd_hosts)


parser_mapping = subparsers.add_parser('mapping', help='list mappings, or create a mapping')
parser_mapping.add_argument('--create', dest='mapping', nargs=2,
help='mapping to add, host|hostuuid:path host|hostuuid:path')
parser_mapping.set_defaults(func=cmd_mapping)

parser_dump = subparsers.add_parser('dump', help='dump all the objects') parser_dump = subparsers.add_parser('dump', help='dump all the objects')
parser_dump.set_defaults(func=cmd_dump) parser_dump.set_defaults(func=cmd_dump)


@@ -1633,6 +1722,13 @@ class _TestCases(unittest.TestCase):
except KeyError: except KeyError:
pass pass


for i in cmd.get('format', []):
if i in { 'cmd', 'files' }:
vars = locals()
cmd[i] = [ x.format(**vars) for x in cmd[i] ]
else:
cmd[i] = cmd[i].format(**locals())

try: try:
special = cmd['special'] special = cmd['special']
except KeyError: except KeyError:
@@ -1673,6 +1769,21 @@ class _TestCases(unittest.TestCase):
sd.mkdir(exist_ok=True) sd.mkdir(exist_ok=True)


bttestcase.make_files(sd, btfiles) bttestcase.make_files(sd, btfiles)
elif special == 'setup mapping paths':
mappatha = self.tempdir / 'mapa'
mappatha.mkdir()

mappathb = self.tempdir / 'mapb'
mappathb.mkdir()

filea = mappatha / 'text.txt'
filea.write_text('abc123\n')
fileb = mappathb / 'text.txt'
shutil.copyfile(filea, fileb)
shutil.copystat(filea, fileb)
elif special == 'delete files':
for i in cmd['files']:
os.unlink(i)
else: # pragma: no cover else: # pragma: no cover
raise ValueError('unhandled special: %s' % repr(special)) raise ValueError('unhandled special: %s' % repr(special))




Loading…
Cancel
Save