Commit db3b84ec authored by Kirill Smelkov's avatar Kirill Smelkov

zodbinfo + zodb tool + open storages by URL

Hello up there.

Recently with Ivan we needed a way to obtain last transaction ID of a ZODB storage for resiliency checking. For this `zodb info` utility to print general information about a ZODB database is introduced. Along the way, as it was already planned, all zodbtools utilities are now covered by only one `zodb` command which can invoke other subcommands and show help topics. I also used the occassion to switch how storages are specified from being  ZConfig-file-always to be specified by URL e.g.

     neo://neo1@127.0.0.1:24573
     zeo://...
     file://...

without loosing generality because zconfig:// scheme is also supported.

Please find more details about all changes in individal commit messages.

Thanks for feedback,  
Kirill

/cc @kazuhiko, @jm, @vpelletier, @jerome, @Tyagov, @klaus, @alain.takoudjou, @rafael
/reviewed-on !2
parents 9e4305b8 37b9fbde
...@@ -8,6 +8,7 @@ scripts anymore. So we are here: ...@@ -8,6 +8,7 @@ scripts anymore. So we are here:
__ https://github.com/zopefoundation/ZODB/pull/128#issuecomment-260970932 __ https://github.com/zopefoundation/ZODB/pull/128#issuecomment-260970932
- `zodbanalyze` - analyze FileStorage or repozo deltafs usage. - `zodb analyze` - analyze FileStorage or repozo deltafs usage.
- `zodbcmp` - compare content of two ZODB databases bit-to-bit. - `zodb cmp` - compare content of two ZODB databases bit-to-bit.
- `zodbdump` - dump content of a ZODB database. - `zodb dump` - dump content of a ZODB database.
- `zodb info` - print general information about a ZODB database.
...@@ -19,17 +19,9 @@ setup( ...@@ -19,17 +19,9 @@ setup(
keywords = 'zodb utility tool', keywords = 'zodb utility tool',
packages = find_packages(), packages = find_packages(),
install_requires = ['ZODB'], install_requires = ['ZODB', 'zodburi', 'six'],
# TODO have only one console program "zodb" and then it is entry_points= {'console_scripts': ['zodb = zodbtools.zodb:main']},
# zodb cmd ...
# zodb dump ...
entry_points= {'console_scripts': [
'zodbanalyze = zodbtools.zodbanalyze:main',
'zodbcmp = zodbtools.zodbcmp:main',
'zodbdump = zodbtools.zodbdump:main',
]
},
classifiers = [_.strip() for _ in """\ classifiers = [_.strip() for _ in """\
Development Status :: 3 - Alpha Development Status :: 3 - Alpha
......
# zodbtools - help topics
# Copyright (C) 2017 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Open Source Initiative approved licenses and Convey
# the resulting work. Corresponding source of such a combination shall include
# the source code for all other software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
from collections import OrderedDict
# topic_name -> (topic_summary, topic_help)
topic_dict = OrderedDict()
help_zurl = """\
Almost every zodb command works with a database.
A database can be specified by way of providing URL for its storage.
The most general way to specify a storage is via preparing file with
ZConfig-based storage definition, e.g.
%import neo.client
<NEOStorage>
master_nodes ...
name ...
</NEOStorage>
and using path to that file with zconfig:// schema:
zconfig://<path-to-zconfig-storage-definition>
There are also following simpler ways:
- neo://<db>@<master> for a NEO database
- zeo://<host>:<port> for a ZEO database
- /path/to/file for a FileStorage database
Please see zodburi documentation for full details:
http://docs.pylonsproject.org/projects/zodburi/
"""
topic_dict['zurl'] = "specifying database URL", help_zurl
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
# See COPYING file for full licensing terms. # See COPYING file for full licensing terms.
import hashlib import hashlib
import zodburi
from six.moves.urllib_parse import urlsplit, urlunsplit
def ashex(s): def ashex(s):
return s.encode('hex') return s.encode('hex')
...@@ -72,3 +74,29 @@ def parse_tidrange(tidrange): ...@@ -72,3 +74,29 @@ def parse_tidrange(tidrange):
# empty tid means -inf / +inf respectively # empty tid means -inf / +inf respectively
# ( which is None in IStorage.iterator() ) # ( which is None in IStorage.iterator() )
return (tidmin or None, tidmax or None) return (tidmin or None, tidmax or None)
# storageFromURL opens a ZODB-storage specified by url
# read_only specifies read or read/write mode for requested access:
# - None: use default mode specified by url
# - True/False: explicitly request read-only / read-write mode
def storageFromURL(url, read_only=None):
# no schema -> file://
if "://" not in url:
url = "file://" + url
# read_only -> url
if read_only is not None:
scheme, netloc, path, query, fragment = urlsplit(url)
# XXX this won't have effect with zconfig:// but for file:// neo://
# zeo:// etc ... it works
if scheme != "zconfig":
if len(query) > 0:
query += "&"
query += "read_only=%s" % read_only
url = urlunsplit((scheme, netloc, path, query, fragment))
stor_factory, dbkw = zodburi.resolve_uri(url)
stor = stor_factory()
return stor
#!/usr/bin/env python
# Copyright (C) 2017 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Open Source Initiative approved licenses and Convey
# the resulting work. Corresponding source of such a combination shall include
# the source code for all other software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
"""Zodb is a driver program for invoking zodbtools subcommands"""
from __future__ import print_function
from zodbtools import help as help_module
import getopt
import importlib
import sys
# command_name -> command_module
command_dict = {}
def register_command(cmdname):
command_module = importlib.import_module('zodbtools.zodb' + cmdname)
command_dict[cmdname] = command_module
for _ in ('analyze', 'cmp', 'dump', 'info'):
register_command(_)
def usage(out):
print("""\
Zodb is a tool for managing ZODB databases.
Usage:
zodb command [arguments]
The commands are:
""", file=out)
cmdv = command_dict.keys()
cmdv.sort()
for cmd in cmdv:
cmd_module = command_dict[cmd]
print(" %-11s %s" % (cmd, cmd_module.summary), file=out)
print("""\
Use "zodb help [command]" for more information about a command.
Additional help topics:
""", file=out)
# NOTE no sorting here - topic_dict is pre-ordered
for topic, (topic_summary, _) in help_module.topic_dict.items():
print(" %-11s %s" % (topic, topic_summary), file=out)
print("""\
Use "zodb help [topic]" for more information about that topic.
""", file=out)
# help shows general help or help for a command/topic
def help(argv):
if len(argv) < 2: # help topic ...
usage(sys.stderr)
sys.exit(2)
topic = argv[1]
# topic can either be a command name or a help topic
if topic in command_dict:
command = command_dict[topic]
command.usage(sys.stdout)
sys.exit(0)
if topic in help_module.topic_dict:
_, topic_help = help_module.topic_dict[topic]
print(topic_help)
sys.exit(0)
print("Unknown help topic `%s`. Run 'zodb help'." % topic, file=sys.stderr)
sys.exit(2)
def main():
try:
optv, argv = getopt.getopt(sys.argv[1:], "h", ["help"])
except getopt.GetoptError as e:
print(e, file=sys.stderr)
usage(sys.stderr)
sys.exit(2)
for opt, _ in optv:
if opt in ("-h", "--help"):
usage(sys.stdout)
sys.exit(0)
if len(argv) < 1:
usage(sys.stderr)
sys.exit(2)
command = argv[0]
# help on a topic
if command=="help":
return help(argv)
# run subcommand
command_module = command_dict.get(command)
if command_module is None:
print('zodb: unknown subcommand "%s"' % command, file=sys.stderr)
print("Run 'zodb help' for usage.", file=sys.stderr)
sys.exit(2)
return command_module.main(argv)
if __name__ == '__main__':
main()
#!/usr/bin/env python
# Copyright (C) 2002-2017 Zope Foundation + Nexedi + Contributors # Copyright (C) 2002-2017 Zope Foundation + Nexedi + Contributors
# See LICENSE-ZPL.txt for full licensing terms. # See LICENSE-ZPL.txt for full licensing terms.
...@@ -237,17 +236,18 @@ Note: ...@@ -237,17 +236,18 @@ Note:
Input deltafs file should be uncompressed. Input deltafs file should be uncompressed.
""" """
summary = "analyze FileStorage or repozo deltafs usage"
def usage(stream, msg=None): def usage(stream, msg=None):
if msg: if msg:
print >>stream, msg print >>stream, msg
print >>stream print >>stream
program = os.path.basename(sys.argv[0]) print >>stream, __doc__ % {"program": "zodb analyze"}
print >>stream, __doc__ % {"program": program}
def main(): def main(argv):
try: try:
opts, args = getopt.getopt(sys.argv[1:], opts, args = getopt.getopt(argv[1:],
'hcd', ['help', 'csv', 'dbm']) 'hcd', ['help', 'csv', 'dbm'])
path = args[0] path = args[0]
except (getopt.GetoptError, IndexError), msg: except (getopt.GetoptError, IndexError), msg:
...@@ -275,6 +275,3 @@ def main(): ...@@ -275,6 +275,3 @@ def main():
return h return h
FileStorageFormatter._read_data_header = _read_data_header FileStorageFormatter._read_data_header = _read_data_header
report(analyze(path, use_dbm, delta_fs), csv) report(analyze(path, use_dbm, delta_fs), csv)
if __name__ == "__main__":
main()
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 Nexedi SA and Contributors. # Copyright (C) 2016-2017 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com> # Kirill Smelkov <kirr@nexedi.com>
...@@ -30,7 +29,8 @@ Exit status is 0 if inputs are the same, 1 if different, 2 if error. ...@@ -30,7 +29,8 @@ Exit status is 0 if inputs are the same, 1 if different, 2 if error.
""" """
from __future__ import print_function from __future__ import print_function
from zodbtools.util import ashex, inf, nextitem, txnobjv, parse_tidrange, TidRangeInvalid from zodbtools.util import ashex, inf, nextitem, txnobjv, parse_tidrange, TidRangeInvalid, \
storageFromURL
from time import time from time import time
# compare two storage transactions # compare two storage transactions
...@@ -107,22 +107,17 @@ def storcmp(stor1, stor2, tidmin, tidmax, verbose=False): ...@@ -107,22 +107,17 @@ def storcmp(stor1, stor2, tidmin, tidmax, verbose=False):
# ---------------------------------------- # ----------------------------------------
import ZODB.config
import sys, getopt import sys, getopt
import traceback import traceback
summary = "compare two ZODB databases"
def usage(out): def usage(out):
print("""\ print("""\
Usage: zodbcmp [OPTIONS] <storage1> <storage2> [tidmin..tidmax] Usage: zodb cmp [OPTIONS] <storage1> <storage2> [tidmin..tidmax]
Compare two ZODB databases. Compare two ZODB databases.
<storageX> is a file with ZConfig-based storage definition, e.g. <storageX> is an URL (see 'zodb help zurl') of a ZODB-storage.
%import neo.client
<NEOStorage>
master_nodes ...
name ...
</NEOStorage>
Options: Options:
...@@ -130,11 +125,11 @@ Options: ...@@ -130,11 +125,11 @@ Options:
-h --help show this help -h --help show this help
""", file=out) """, file=out)
def main2(): def main2(argv):
verbose = False verbose = False
try: try:
optv, argv = getopt.getopt(sys.argv[1:], "hv", ["help", "verbose"]) optv, argv = getopt.getopt(argv[1:], "hv", ["help", "verbose"])
except getopt.GetoptError as e: except getopt.GetoptError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
usage(sys.stderr) usage(sys.stderr)
...@@ -148,7 +143,7 @@ def main2(): ...@@ -148,7 +143,7 @@ def main2():
verbose = True verbose = True
try: try:
storconf1, storconf2 = argv[0:2] storurl1, storurl2 = argv[0:2]
except ValueError: except ValueError:
usage(sys.stderr) usage(sys.stderr)
sys.exit(2) sys.exit(2)
...@@ -162,20 +157,17 @@ def main2(): ...@@ -162,20 +157,17 @@ def main2():
print("E: invalid tidrange: %s" % e, file=sys.stderr) print("E: invalid tidrange: %s" % e, file=sys.stderr)
sys.exit(2) sys.exit(2)
stor1 = ZODB.config.storageFromFile(open(storconf1, 'r')) stor1 = storageFromURL(storurl1, read_only=True)
stor2 = ZODB.config.storageFromFile(open(storconf2, 'r')) stor2 = storageFromURL(storurl2, read_only=True)
zcmp = storcmp(stor1, stor2, tidmin, tidmax, verbose) zcmp = storcmp(stor1, stor2, tidmin, tidmax, verbose)
sys.exit(1 if zcmp else 0) sys.exit(1 if zcmp else 0)
def main(): def main(argv):
try: try:
main2() main2(argv)
except SystemExit: except SystemExit:
raise # this was sys.exit() call, not an error raise # this was sys.exit() call, not an error
except: except:
traceback.print_exc() traceback.print_exc()
sys.exit(2) sys.exit(2)
if __name__ == '__main__':
main()
#!/usr/bin/env python
# Copyright (C) 2016-2017 Nexedi SA and Contributors. # Copyright (C) 2016-2017 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com> # Kirill Smelkov <kirr@nexedi.com>
# #
...@@ -31,8 +30,8 @@ txn ... ...@@ -31,8 +30,8 @@ txn ...
""" """
from __future__ import print_function from __future__ import print_function
from zodbtools.util import ashex, sha1, txnobjv, parse_tidrange, TidRangeInvalid from zodbtools.util import ashex, sha1, txnobjv, parse_tidrange, TidRangeInvalid, \
storageFromURL
def zodbdump(stor, tidmin, tidmax, hashonly=False): def zodbdump(stor, tidmin, tidmax, hashonly=False):
first = True first = True
...@@ -68,21 +67,16 @@ def zodbdump(stor, tidmin, tidmax, hashonly=False): ...@@ -68,21 +67,16 @@ def zodbdump(stor, tidmin, tidmax, hashonly=False):
# ---------------------------------------- # ----------------------------------------
import ZODB.config
import sys, getopt import sys, getopt
summary = "dump content of a ZODB database"
def usage(out): def usage(out):
print("""\ print("""\
Usage: zodbdump [OPTIONS] <storage> [tidmin..tidmax] Usage: zodb dump [OPTIONS] <storage> [tidmin..tidmax]
Dump content of a ZODB database. Dump content of a ZODB database.
<storage> is a file with ZConfig-based storage definition, e.g. <storage> is an URL (see 'zodb help zurl') of a ZODB-storage.
%import neo.client
<NEOStorage>
master_nodes ...
name ...
</NEOStorage>
Options: Options:
...@@ -90,11 +84,11 @@ Options: ...@@ -90,11 +84,11 @@ Options:
-h --help show this help -h --help show this help
""", file=out) """, file=out)
def main(): def main(argv):
hashonly = False hashonly = False
try: try:
optv, argv = getopt.getopt(sys.argv[1:], "h", ["help", "hashonly"]) optv, argv = getopt.getopt(argv[1:], "h", ["help", "hashonly"])
except getopt.GetoptError as e: except getopt.GetoptError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
usage(sys.stderr) usage(sys.stderr)
...@@ -108,7 +102,7 @@ def main(): ...@@ -108,7 +102,7 @@ def main():
hashonly = True hashonly = True
try: try:
storconf = argv[0] storurl = argv[0]
except IndexError: except IndexError:
usage(sys.stderr) usage(sys.stderr)
sys.exit(2) sys.exit(2)
...@@ -122,9 +116,6 @@ def main(): ...@@ -122,9 +116,6 @@ def main():
print("E: invalid tidrange: %s" % e, file=sys.stderr) print("E: invalid tidrange: %s" % e, file=sys.stderr)
sys.exit(2) sys.exit(2)
stor = ZODB.config.storageFromFile(open(storconf, 'r')) stor = storageFromURL(storurl, read_only=True)
zodbdump(stor, tidmin, tidmax, hashonly) zodbdump(stor, tidmin, tidmax, hashonly)
if __name__ == '__main__':
main()
# -*- coding: utf-8 -*-
# Copyright (C) 2017 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Open Source Initiative approved licenses and Convey
# the resulting work. Corresponding source of such a combination shall include
# the source code for all other software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
"""Zodbinfo - Print general information about a ZODB database"""
from __future__ import print_function
from zodbtools.util import ashex, storageFromURL
from collections import OrderedDict
import sys
# {} parameter_name -> get_parameter(stor)
infoDict = OrderedDict([
("name", lambda stor: stor.getName()),
("size", lambda stor: stor.getSize()),
("last_tid", lambda stor: ashex(stor.lastTransaction())),
])
def zodbinfo(stor, parameterv):
wantnames = False
if not parameterv:
parameterv = infoDict.keys()
wantnames = True
for parameter in parameterv:
get_parameter = infoDict.get(parameter)
if get_parameter is None:
print("invalid parameter: %s" % parameter, file=sys.stderr)
sys.exit(1)
out = ""
if wantnames:
out += parameter + "="
out += "%s" % (get_parameter(stor),)
print(out)
# ----------------------------------------
import getopt
summary = "print general information about a ZODB database"
def usage(out):
print("""\
Usage: zodb info [OPTIONS] <storage> [parameter ...]
Print general information about a ZODB database.
<storage> is an URL (see 'zodb help zurl') of a ZODB-storage.
By default info prints information about all storage parameters. If one or
more parameter names are given as arguments, info prints the value of each
named parameter on its own line.
Options:
-h --help show this help
""", file=out)
def main(argv):
try:
optv, argv = getopt.getopt(argv[1:], "h", ["help"])
except getopt.GetoptError as e:
print(e, file=sys.stderr)
usage(sys.stderr)
sys.exit(2)
for opt, _ in optv:
if opt in ("-h", "--help"):
usage(sys.stdout)
sys.exit(0)
try:
storurl = argv[0]
except IndexError:
usage(sys.stderr)
sys.exit(2)
stor = storageFromURL(storurl, read_only=True)
zodbinfo(stor, argv[1:])
Markdown is supported
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