]> www.infradead.org Git - users/dhowells/kafs-utils.git/commitdiff
Create 'vos listvldb' command
authorDavid Howells <dhowells@redhat.com>
Mon, 7 Apr 2014 22:41:20 +0000 (23:41 +0100)
committerDavid Howells <dhowells@redhat.com>
Mon, 7 Apr 2014 23:23:10 +0000 (00:23 +0100)
12 files changed:
.gitignore
suite/afs [new file with mode: 0755]
suite/argparse.py [new file with mode: 0644]
suite/commands/__init__.py [new file with mode: 0644]
suite/commands/vos/__init__.py [new file with mode: 0644]
suite/commands/vos/listvldb.py [new file with mode: 0644]
suite/exception.py [new file with mode: 0644]
suite/lib/addrcache.py [new file with mode: 0644]
suite/lib/cell.py [new file with mode: 0644]
suite/lib/debug.py [new file with mode: 0644]
suite/lib/vlserver.py [new file with mode: 0644]
suite/main.py [new file with mode: 0644]

index 64e3998c8f5667a4c8f5929ed8ea8b7037507f94..9c2285bb04099b428a7269480261efb0a7c99978 100644 (file)
@@ -5,3 +5,4 @@ afs_py.h
 afs_xg.c
 afs_xg.h
 *.so
+__pycache__
diff --git a/suite/afs b/suite/afs
new file mode 100755 (executable)
index 0000000..7699e4c
--- /dev/null
+++ b/suite/afs
@@ -0,0 +1,48 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# -*- python-mode -*-
+
+"""AFS Toolkit Volume management command set
+"""
+
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public Licence version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public Licence for more details.
+
+You should have received a copy of the GNU General Public Licence
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+import sys, os
+
+# Try to detect where it is run from and set prefix and the search path.
+# It is assumed that the user installed Afs using the --prefix= option
+prefix, bin = os.path.split(sys.path[0])
+
+if bin == 'bin' and prefix != sys.prefix:
+    sys.prefix = prefix
+    sys.exec_prefix = prefix
+
+    major, minor = sys.version_info[0:2]
+    local_path = [os.path.join(prefix, 'lib', 'python'),
+                  os.path.join(prefix, 'lib', 'python%s.%s' % (major, minor)),
+                  os.path.join(prefix, 'lib', 'python%s.%s' % (major, minor),
+                               'site-packages')]
+    sys.path = local_path + sys.path
+
+from afs.main import main
+
+if __name__ == '__main__':
+    main()
diff --git a/suite/argparse.py b/suite/argparse.py
new file mode 100644 (file)
index 0000000..f0ffcec
--- /dev/null
@@ -0,0 +1,214 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+#
+# Parse an argument list for a subcommand
+#
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public Licence
+as published by the Free Software Foundation; either version
+2 of the Licence, or (at your option) any later version.
+"""
+
+def get_cell(switch, params):
+    from afs.lib.cell import cell
+    return cell(params[0])
+
+def get_vlserver(switch, params):
+    from afs.lib.vlserver import vlserver
+    return vlserver(params[0])
+
+def get_volume_name(switch, params):
+    return params
+
+def get_machine_name(switch, params):
+    return params
+
+def get_path_name(switch, params):
+    return params
+
+def get_file_name(switch, params):
+    return params
+
+def get_partition_id(switch, params):
+    part = params[0]
+    if part.startswith("/vice"):
+        part = part[5:]
+    elif part.startswith("vice"):
+        part = part[4:]
+
+    if part.isnumeric():
+        part = int(part)
+    elif len(part) == 1 and part >= "a" and part <= "z":
+        part = bytes(part)[0] - 97
+    elif len(part) == 2 and part[0] >= "a" and part[0] <= "z" and part[1] >= "a" and part[1] <= "z":
+        part = (bytes(part)[0] - 97) * 26 | bytes(part)[1] - 97
+    else:
+        raise RuntimeError("Unparseable partition ID '" + params[0] + "'")
+
+    if part > 255:
+        raise RuntimeError("Partition ID '" + params[0] + "' out of range")
+    return part
+
+def get_auth(switch, params):
+    return params
+
+def get_dummy(switch, params):
+    return params
+
+###############################################################################
+#
+# Parse an argument list according to a defined set of switches.
+#
+# The following is an excerpt from the AFS Reference Manual, Introduction to
+# AFS commands:
+#
+# CONDITIONS FOR OMITTING SWITCHES
+#
+#    It is always acceptable to type the switch part of an argument, but in
+#    many cases it is not necessary. Specifically, switches can be omitted if
+#    the following conditions are met.
+#
+#    (*) All of the command's required arguments appear in the order prescribed
+#        by the syntax statement.
+#
+#    (*) No switch is provided for any argument.
+#
+#    (*) There is only one value for each argument (but note the important
+#        exception discussed in the following paragraph).
+#
+#    Omitting switches is possible only because there is a prescribed order for
+#    each command's arguments. When the issuer does not include switches, the
+#    command interpreter relies instead on the order of arguments; it assumes
+#    that the first element after the operation code is the command's first
+#    argument, the next element is the command's second argument, and so
+#    on. The important exception is when a command's final required argument
+#    accepts multiple values. In this case, the command interpreter assumes
+#    that the issuer has correctly provided one value for each argument up
+#    through the final one, so any additional values at the end belong to the
+#    final argument.
+#
+#    The following list describes the rules for omitting switches from the
+#    opposite perspective: an argument's switch must be provided when any of
+#    the following conditions apply.
+#
+#    (*) The command's arguments do not appear in the prescribed order.
+#
+#    (*) An optional argument is omitted but a subsequent optional argument is
+#        provided.
+#
+#    (*) A switch is provided for a preceding argument.
+#
+#    (*) More than one value is supplied for a preceding argument (which must
+#        take multiple values, of course); without a switch on the current
+#        argument, the command interpreter assumes that the current argument is
+#        another value for the preceding argument.
+#
+###############################################################################
+def parse_arguments(args, available_arguments):
+    result = {}
+    need_switch = False
+    i = 0       # Given arguments index
+    av = 0      # Available arguments index
+
+    #print args
+
+    if len(args) == 0:
+        if len(available_arguments) > 0 and available_arguments[0][0] == "r":
+            raise RuntimeError("Missing required parameters")
+        return result
+
+    # Process all the optional arguments or switch-based required arguments
+    while i < len(args):
+        match = False
+        params = []
+
+        if args[i][0] != "-":
+            # Deal with positional arguments
+            if need_switch:
+                raise RuntimeError("Need switch before argument " + i)
+            if av >= len(available_arguments):
+                raise RuntimeError("Unexpected positional argument")
+            match = available_arguments[av]
+            pattern = match[2]
+            if pattern[0] == "f":
+                raise RuntimeError("Unexpected positional argument")
+            av = av + 1
+
+            params.append(args[i])
+            i = i + 1
+
+            if match[2][1] == "m":
+                # All remaining arguments up to the next switch belong to this
+                while i < len(args):
+                    if args[i][0] == "-":
+                        break
+                    params.append(args[i])
+                    i = i + 1
+                need_switch = True
+
+        else:
+            # Deal with tagged arguments
+            switch = args[i][1:]
+            i = i + 1
+
+            if switch == "help":
+                raise RuntimeError("Print help message")
+            if switch == "":
+                raise RuntimeError("Missing switch name")
+
+            # Look up the switch in the table of possible arguments and flags
+            for j in available_arguments:
+                if j[0] == switch:
+                    match = j
+                    break
+                if j[0].startswith(switch):
+                    if match:
+                        raise RuntimeError("Ambiguous switch name abbreviation '-" + switch + "'")
+                    match = j
+            if not match:
+                raise RuntimeError("Unsupported switch '-" + switch + "'")
+
+            # Reject repeat flags
+            if match[0] in result:
+                raise RuntimeError("Duplicate switch '-" + switch + "' not permitted")
+
+            # Arrange the parameters associated with the switch into a list
+            while i < len(args):
+                if args[i][0] == "-":
+                    break
+                params.append(args[i])
+                i = i + 1
+
+        # Check that we have the number of arguments we're expecting
+        pattern = match[2]
+        if pattern[1] == "n" and len(params) != 0:
+            raise RuntimeError("Switch '-" + switch + "' expects no arguments")
+        if pattern[1] == "s" and len(params) != 1:
+            raise RuntimeError("Switch '-" + switch + "' expects one argument")
+        if pattern[1] == "m" and len(params) < 1:
+            raise RuntimeError("Switch '-" + switch + "' expects one or more arguments")
+
+        # Call the syntax checker
+        syntax = match[1]
+        result[match[0]] = syntax(match[0], params)
+
+    # Check for missing required arguments
+    for j in available_arguments:
+        switch = j[0]
+        pattern = j[2]
+        if j[2][0] != "r":
+            break
+        if switch not in result:
+            raise RuntimeError("Missing '-" + switch + "' argument")
+
+    return result
+
+
+#if len(sys.argv) < 2:
+#    raise RuntimeError("Need command name")
+#
+#print(parse_arguments(sys.argv[2:], fs_mkmount_arguments))
diff --git a/suite/commands/__init__.py b/suite/commands/__init__.py
new file mode 100644 (file)
index 0000000..213839a
--- /dev/null
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+Derived from StGIT:
+
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+Copyright (C) 2008, Karl Hasselström <kha@treskal.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public Licence version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public Licence for more details.
+
+You should have received a copy of the GNU General Public Licence
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+import os
+import stat
+
+def get_command_sets():
+    sets = []
+    for p in __path__:
+        for fn in os.listdir(p):
+            if fn[0] == "_" or fn[-1] == "~" or not stat.S_ISDIR(os.stat(p + "/" + fn).st_mode):
+                continue
+            sets.append(fn)
+    return sets
+
+def import_command_set(cmdset):
+    return __import__(__name__ + '.' + cmdset, globals(), locals(), ['*'])
+
diff --git a/suite/commands/vos/__init__.py b/suite/commands/vos/__init__.py
new file mode 100644 (file)
index 0000000..b1801d0
--- /dev/null
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+Derived from StGIT:
+
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+Copyright (C) 2008, Karl Hasselström <kha@treskal.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public Licence version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public Licence for more details.
+
+You should have received a copy of the GNU General Public Licence
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+import os
+
+def get_command(mod):
+    """Import and return the given command module."""
+    return __import__(__name__ + '.' + mod, globals(), locals(), ['*'])
+
+def get_command_list():
+    commands = []
+    for p in __path__:
+        for fn in os.listdir(p):
+            if not fn.startswith("_") and fn.endswith('.py'):
+                commands.append(fn[:-3])
+    return commands
+
+def py_commands(commands, f):
+    f.write('command_list = {\n')
+    for key, val in sorted(commands.iteritems()):
+        f.write('    %r: %r,\n' % (key, val))
+    f.write('    }\n')
+
+#def _command_list(commands):
+#    for cmd, (mod, help) in commands.iteritems():
+
+def pretty_command_list(commands, f):
+    cmd_len = max(len(cmd) for cmd in commands.iterkeys())
+    for cmd, help in cmds:
+        f.write('  %*s  %s\n' % (-cmd_len, cmd, help))
+
+def _write_underlined(s, u, f):
+    f.write(s + '\n')
+    f.write(u*len(s) + '\n')
+
+def asciidoc_command_list(commands, f):
+    for cmd, help in commands:
+        f.write('linkstgsub:%s[]::\n' % cmd)
+        f.write('    %s\n' % help)
+    f.write('\n')
diff --git a/suite/commands/vos/listvldb.py b/suite/commands/vos/listvldb.py
new file mode 100644 (file)
index 0000000..41aa96e
--- /dev/null
@@ -0,0 +1,143 @@
+#
+# AFS Volume management toolkit: Volume location database query
+# -*- coding: utf-8 -*-
+#
+
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public Licence version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public Licence for more details.
+
+You should have received a copy of the GNU General Public Licence
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+from afs.argparse import *
+import afs.lib.addrcache as addrcache
+import kafs
+from socket import inet_ntoa
+
+help = "Query the VLDB"
+
+command_arguments = [
+    [ "name",           get_volume_name,        "os",         "<volume name or ID>" ],
+    [ "server",         get_vlserver,           "os",         "<machine name>" ],
+    [ "partition",      get_partition_id,       "os",         "<partition name>" ],
+    [ "locked",         get_dummy,              "fn" ],
+    [ "quiet",          get_dummy,              "fn" ],
+    [ "nosort",         get_dummy,              "fn" ],
+    [ "cell",           get_cell,               "os",         "<cell name>" ],
+    [ "noauth",         get_auth,               "fn" ],
+    [ "localauth",      get_auth,               "fn" ],
+    [ "verbose",        get_dummy,              "fn" ],
+    [ "encrypt",        get_dummy,              "fn" ],
+    [ "noresolve",      get_dummy,              "fn" ],
+]
+
+description = r"""
+Displays a volume's VLDB entry
+"""
+
+def print_record(params, vldb):
+    """Display a single VLDB record"""
+    print(vldb.name)
+    print(" RWrite: {:<12d} ROnly: {:<12d} Backup: {:<12d}".format(vldb.volumeId[0],
+                                                                   vldb.volumeId[1],
+                                                                   vldb.volumeId[2]))
+    print(" number of sites ->", vldb.nServers)
+    for i in range(0, vldb.nServers):
+        inaddr = bytearray(4)
+        inaddr[0] = (vldb.serverNumber[i] >> 24) & 0xff
+        inaddr[1] = (vldb.serverNumber[i] >> 16) & 0xff
+        inaddr[2] = (vldb.serverNumber[i] >>  8) & 0xff
+        inaddr[3] = (vldb.serverNumber[i] >>  0) & 0xff
+        addr = inet_ntoa(bytes(inaddr))
+
+        if "noresolve" not in params:
+            name = addrcache.addr2name(addr)
+            if name != None:
+                addr = name
+
+        partnum = vldb.serverPartition[i]
+        if partnum < 26:
+            part = "{:c}".format(97 + partnum)
+        else:
+            partnum -= 26
+            part = "{:c}{:c}".format(97 + partnum / 26, 97 + partnum % 26)
+
+        flags = vldb.serverFlags[i]
+        if flags & kafs.VLSF_ROVOL:
+            ptype = "RO"
+        elif flags & kafs.VLSF_RWVOL:
+            ptype = "RW"
+        else:
+            ptype = "Back"
+
+        print("    server {:s} partition /vicep{:s} {:s} Site".format(addr, part, ptype))
+
+    if vldb.flags & (kafs.VLOP_MOVE | kafs.VLOP_RELEASE | kafs.VLOP_BACKUP | kafs.VLOP_DELETE | kafs.VLOP_DUMP):
+        print(" Volume is currently LOCKED")
+        if vldb.flags & kafs.VLOP_MOVE:
+            print("Volume is locked for a move operation")
+        if vldb.flags & kafs.VLOP_RELEASE:
+            print("Volume is locked for a release operation")
+        if vldb.flags & kafs.VLOP_BACKUP:
+            print("Volume is locked for a backup operation")
+        if vldb.flags & kafs.VLOP_DELETE:
+            print("Volume is locked for a delete/misc operation")
+        if vldb.flags & kafs.VLOP_DUMP:
+            print("Volume is locked for a dump/restore operation")
+
+def main(params):
+    # Get a list of VLDB servers to query
+    cell = params["cell"]
+    z_conn = cell.open_vl_server()
+    quiet = "quiet" in params
+
+    if "name" in params:
+        if "server" in params or "partition" in params or "locked" in params:
+            raise RuntimeError("Can't combine -name with -server, -partition or -locked")
+        ret = kafs.VL_GetEntryByName(z_conn, params["name"][0])
+        vldb = ret.entry
+        print_record(params, vldb)
+        return
+
+    if "name" in params:
+        raise RuntimeError("Can't combine -server, -partition or -locked with -name")
+
+    attributes = kafs.VldbListByAttributes()
+    attributes.Mask = 0
+
+    if "server" in params:
+        attributes.Mask |= kafs.VLLIST_SERVER
+        attributes.server = addrcache.name2addr_int(str(params["server"]))
+    if "partition" in params:
+        attributes.Mask |= kafs.VLLIST_PARTITION
+        attributes.partition = params["partition"]
+    if "locked" in params:
+        attributes.Mask |= kafs.VLLIST_FLAG
+        attributes.flag = kafs.VLOP_MOVE | kafs.VLOP_RELEASE | kafs.VLOP_BACKUP | kafs.VLOP_DELETE | kafs.VLOP_DUMP
+
+    ret = kafs.VL_ListAttributes(z_conn, attributes)
+    blkentries = ret.blkentries
+
+    if not quiet and "server" in params:
+        print("VLDB entries for server", params["server"])
+
+    if "nosort" not in params:
+        blkentries.sort(key=lambda vldb: vldb.name)
+
+    for vldb in blkentries:
+        print_record(params, vldb)
+
+    if not quiet:
+        print("Total entries:", len(blkentries))
diff --git a/suite/exception.py b/suite/exception.py
new file mode 100644 (file)
index 0000000..b031378
--- /dev/null
@@ -0,0 +1,3 @@
+class AFSException(Exception):
+    """Base class for all AFS Toolkit exceptions."""
+    pass
diff --git a/suite/lib/addrcache.py b/suite/lib/addrcache.py
new file mode 100644 (file)
index 0000000..e83d318
--- /dev/null
@@ -0,0 +1,109 @@
+#
+# AFS Volume management toolkit: IP Address cache
+# -*- coding: utf-8 -*-
+#
+
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public Licence version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public Licence for more details.
+
+You should have received a copy of the GNU General Public Licence
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+from afs import exception
+from socket import inet_pton, AF_INET, AF_INET6;
+import dns.reversename
+import dns.resolver
+
+cache_n2a = dict()
+cache_a2n = dict()
+
+class NetAddressError(exception.AFSException):
+    """Error raised by L{address cache}."""
+
+def add(name, addr):
+    name = name.lower().rstrip(".")
+    if name not in cache_n2a:
+        cache_n2a[name] = []
+    if addr not in cache_n2a[name]:
+        cache_n2a[name].append(addr)
+    cache_a2n[addr] = name
+
+def name2addr(name):
+    # Try parsing as a numeric IPv4 address
+    try:
+        addr = inet_pton(AF_INET, name)
+        return name
+    except OSError:
+        pass
+
+    # Try parsing as a numeric IPv6 address
+    try:
+        addr = inet_pton(AF_INET6, name)
+        raise NetAddressError("IPv6 is not currently supported")
+    except OSError:
+        pass
+
+    if name in cache_n2a:
+        return cache_n2a[name]
+
+    try:
+        for A in dns.resolver.query(name, 'A'):
+            addrcache.add(name, A.address)
+    except dns.resolver.NoAnswer:
+        pass
+
+    return cache_n2a[name]
+
+def name2addr_int(name):
+    # Try parsing as a numeric IPv4 address
+    try:
+        addr = inet_pton(AF_INET, name)
+        inaddr  = addr[0] << 24
+        inaddr |= addr[1] << 16
+        inaddr |= addr[2] <<  8
+        inaddr |= addr[3] <<  0
+        return inaddr
+    except OSError:
+        pass
+
+    # Try parsing as a numeric IPv6 address
+    try:
+        addr = inet_pton(AF_INET6, name)
+        raise NetAddressError("IPv6 is not currently supported")
+    except OSError:
+        pass
+
+    if name not in cache_n2a:
+        return None
+    addr = inet_pton(AF_INET, cache_n2a[name][0])
+    inaddr  = addr[0] << 24
+    inaddr |= addr[1] << 16
+    inaddr |= addr[2] <<  8
+    inaddr |= addr[3] <<  0
+    return inaddr
+
+def addr2name(addr):
+    if addr not in cache_a2n:
+        try:
+            revname = dns.reversename.from_address(addr)
+            for PTR in dns.resolver.query(revname, 'PTR'):
+                name = str(PTR).lower().rstrip(".")
+                add(name, addr)
+                return name
+
+            return None
+        except dns.resolver.NoAnswer:
+            return None
+    return cache_a2n[addr]
diff --git a/suite/lib/cell.py b/suite/lib/cell.py
new file mode 100644 (file)
index 0000000..267c715
--- /dev/null
@@ -0,0 +1,140 @@
+#
+# AFS Volume management toolkit: Cell record
+# -*- coding: utf-8 -*-
+#
+
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public Licence version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public Licence for more details.
+
+You should have received a copy of the GNU General Public Licence
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+from afs import exception
+from afs.lib.vlserver import vlserver, VLServerError
+from afs.lib.debug import debug
+import dns.resolver
+import linecache
+import kafs
+
+class CellError(exception.AFSException):
+    """Error raised by L{cell} objects."""
+
+class cell:
+    """Represents an AFS cell.  We hold the cell name here and the list of
+    VL servers once we've looked it up"""
+    def __init__(self, name = None):
+        if name == None:
+            name = linecache.getline("/etc/afs/ThisCell", 1)
+            name = name.strip()
+            debug("Default Cell:", name)
+            if name != "":
+                debug("Found", name)
+            else:
+                raise CellError("Couldn't determine default cell")
+
+        debug("New Cell", name)
+        self.__name = name
+        self.__looked_up = False
+        self.__vlserver_names = dict()
+        self.__vlservers = []
+        self.__vlconn = None
+
+    def __repr__(self):
+        return "<" + "AFS:" + self.__name + ">"
+
+    def __str__(self):
+        return self.__name
+
+    def add_server(self, dnstype, name):
+        n = str(name)
+        if n not in self.__vlserver_names:
+            s = vlserver(n)
+            self.__vlserver_names[n] = s
+            self.__vlservers.append(s)
+
+    # Look up the servers
+    def look_up_vl_servers(self):
+        debug("-- Find VL servers for cell:", self.__name, "--")
+
+        # Start by looking for SRV records in the DNS
+        try:
+            for SRV in dns.resolver.query("_afs3-vlserver._udp." + self.__name, "SRV"):
+                self.add_server("SRV", SRV.target)
+        except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+            debug("Couldn't find any SRV records")
+
+        # Then we look for AFSDB records in the DNS
+        try:
+            for AFSDB in dns.resolver.query(self.__name, "AFSDB"):
+                self.add_server("AFSDB", AFSDB.hostname)
+        except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+            debug("Couldn't find any AFSDB records")
+
+        # Then parse the local afsdb file
+        # ... TODO ...
+
+        self.__looked_up = True
+
+    # Get the VLDB database server list for the cell
+    def query_vl_servers(self):
+        if not self.__looked_up:
+            self.look_up_vl_servers()
+        if not self.__vlservers:
+            raise CellError("Couldn't find any VL servers")
+        return self.__vlservers
+
+    # Force a particular set of servers
+    def override_vlserver_list(self, servers):
+        self.__vlservers = servers
+        for i in servers:
+            self.__vlserver_names[str(i)] = i
+        self.__looked_up = True
+
+    # Get the VLDB database server addresses for the cell
+    def query_vl_addrs(self):
+        addrs = []
+        for i in self.query_vl_servers():
+            try:
+                for j in i.addrs():
+                    if j not in addrs:
+                        addrs.append(j)
+            except VLServerError:
+                pass
+
+        if len(addrs) == 0:
+            raise CellError("Couldn't find any VL servers in cell")
+
+        return addrs
+
+    # Open a VL Server connection
+    def open_vl_server(self):
+        if self.__vlconn:
+            return
+
+        for vlserver in self.query_vl_addrs():
+            debug("Trying", vlserver)
+
+            z_conn = kafs.rx_new_connection(vlserver, kafs.VL_PORT, kafs.VL_SERVICE)
+            try:
+                ret = kafs.VL_Probe(z_conn)
+                self.__vlconn = z_conn
+                break
+            except ConnectionRefusedError:
+                pass
+            del z_conn
+        else:
+            raise CellError("Couldn't connect to a VL server")
+
+        return self.__vlconn
diff --git a/suite/lib/debug.py b/suite/lib/debug.py
new file mode 100644 (file)
index 0000000..6d2097b
--- /dev/null
@@ -0,0 +1,30 @@
+#
+# AFS Volume management toolkit: Debugging
+# -*- coding: utf-8 -*-
+#
+
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public Licence version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public Licence for more details.
+
+You should have received a copy of the GNU General Public Licence
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+import sys
+
+debugging_level = 0
+
+def debug(*args, **kwargs):
+    if debugging_level > 0:
+        print(*args, file=sys.stdout)
diff --git a/suite/lib/vlserver.py b/suite/lib/vlserver.py
new file mode 100644 (file)
index 0000000..9570fff
--- /dev/null
@@ -0,0 +1,90 @@
+#
+# AFS Volume management toolkit: Volume Location server record
+# -*- coding: utf-8 -*-
+#
+
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public Licence version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public Licence for more details.
+
+You should have received a copy of the GNU General Public Licence
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+from afs import exception
+import afs.lib.addrcache as addrcache
+from afs.lib.debug import debug
+from socket import inet_pton, AF_INET, AF_INET6;
+import dns.resolver
+
+class VLServerError(exception.AFSException):
+    """Error raised by L{vlserver} objects."""
+
+class vlserver:
+    """Represents an AFS Volume Location server.  We hold the server address here."""
+    def __init__(self, name):
+        debug("New VLServer", name)
+        self.__name = name
+        self.__looked_up = False
+
+        # We want to maintain the record order provided by the DNS to entry
+        # force rotation, so we don't want to use an unordered set here.
+        #
+        # Note that the addrs are stored as text strings of the form "a.b.c.d"
+        self.__addrs = []
+
+    def __repr__(self):
+        return "<" + "AFSVL:" + self.__name + ">"
+
+    def __str__(self):
+        return self.__name
+
+    def look_up_addresses(self):
+        addr = False
+
+        # Try parsing as a numeric IPv4 address
+        try:
+            addr = inet_pton(AF_INET, self.__name)
+            self.__addrs.append(self.__name)
+        except OSError:
+            pass
+
+        # Try parsing as a numeric IPv6 address
+        if not addr:
+            try:
+                addr = inet_pton(AF_INET6, self.__name)
+                raise VLServerError("IPv6 is not currently supported")
+            except OSError:
+                pass
+
+        # Try looking up in the DNS
+        if not addr:
+            try:
+                for A in dns.resolver.query(self.__name, 'A'):
+                    if A.address not in self.__addrs:
+                        self.__addrs.append(A.address)
+                        addrcache.add(self.__name, A.address)
+            except dns.resolver.NoAnswer:
+                pass
+
+        self.__looked_up = True
+
+    def look_up(self):
+        if not self.__looked_up:
+            self.look_up_addresses()
+        if len(self.__addrs) == 0:
+            raise VLServerError("No VLServer addresses available")
+        return self.__addrs
+
+    def addrs(self):
+        return self.look_up()
diff --git a/suite/main.py b/suite/main.py
new file mode 100644 (file)
index 0000000..9a6cb2a
--- /dev/null
@@ -0,0 +1,196 @@
+#
+# AFS Toolset command switcher 
+# -*- coding: utf-8 -*-
+#
+
+__copyright__ = """
+Copyright (C) 2014 Red Hat, Inc. All Rights Reserved.
+Written by David Howells (dhowells@redhat.com)
+
+Derived from StGIT:
+
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public Licence version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public Licence for more details.
+
+You should have received a copy of the GNU General Public Licence
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+import sys, os, traceback
+
+import afs.commands
+from afs.lib.debug import debug, debugging_level
+
+#
+# The commands map
+#
+class Commands(dict):
+    """Commands class. It performs on-demand module loading
+    """
+    def canonical_cmd(self, key):
+        """Return the canonical name for a possibly-shortened
+        command name.
+        """
+        candidates = [cmd for cmd in self.keys() if cmd.startswith(key)]
+
+        if not candidates:
+            out.error('Unknown command: %s' % key,
+                      'Try "%s help" for a list of supported commands' % prog)
+            sys.exit(2)
+
+        if len(candidates) > 1:
+            out.error('Ambiguous command: %s' % key,
+                      'Candidates are: %s' % ', '.join(candidates))
+            sys.exit(2)
+
+        return candidates[0]
+        
+    def __getitem__(self, key):
+        cmd_mod = self.get(key) or self.get(self.canonical_cmd(key))
+        return afs.commands.get_command(cmd_mod)
+
+#cmd_list = afs.commands.get_commands()
+#print(cmd_list);
+
+#commands = Commands((cmd, mod) for cmd, (mod, kind, help)
+#                    in cmd_list.iteritems())
+
+def print_help():
+    print('usage: %s <command> [options]' % os.path.basename(sys.argv[0]))
+    print()
+    print('Generic commands:')
+    print('  help        print the detailed command usage')
+    print('  version     display version information')
+    print('  copyright   display copyright information')
+    print()
+    afs.commands.pretty_command_list(cmd_list, sys.stdout)
+
+#
+# The main function (command dispatcher)
+#
+def _main():
+    """The main function
+    """
+    global prog
+
+    prog = os.path.basename(sys.argv[0])
+
+    cmdsets = afs.commands.get_command_sets()
+    #print("CMDSETS:", cmdsets)
+
+    if prog == "afs":
+        if len(sys.argv) < 3:
+            print('usage: {:s} <cmdset> <command>'.format(prog), file=sys.stderr)
+            print('  Try "{:s} help" for a list of supported command sets'.format(prog), file=sys.stderr)
+            sys.exit(2)
+        cmdset = sys.argv[1]
+        cmd = sys.argv[2]
+        del sys.argv[1:3]
+    else:
+        if len(sys.argv) < 2:
+            print('usage: {:s} <command>'.format(prog), file=sys.stderr)
+            print('  Try "{:s} help" for a list of supported commands'.format(prog), file=sys.stderr)
+            sys.exit(2)
+        cmdset = prog
+        cmd = sys.argv[1]
+        del sys.argv[1:2]
+
+    if cmdset not in cmdsets:
+        raise RuntimeError("Unsupported command set '" + cmdset + "'")
+
+    if cmd in ['-v', '--version', 'version']:
+        from afs.version import version
+        print('AFS Toolkit %s' % version)
+        print('Python version %s' % sys.version)
+        sys.exit(0)
+
+    if cmd in ['copyright']:
+        print(__copyright__)
+        sys.exit(0)
+
+    # Import the command set
+    cmdsetmod = afs.commands.import_command_set(cmdset)
+    commands = cmdsetmod.get_command_list()
+    #print("CMDS:", commands)
+
+    # See if the command is in the set
+    found = False
+    for i in commands:
+        if i == cmd:
+            found = cmd
+            break
+        if i.startswith(cmd):
+            if found:
+                raise RuntimeError("Command '" + cmd + "' is ambiguous")
+            found = cmd
+    if not found:
+        raise RuntimeError("Command '" + cmd + "' is unknown")
+    cmd = found
+
+    # Rebuild the command line arguments
+    if prog == "afs":
+        sys.argv[0] += ' {:s} {:s}'.format(cmdset, cmd)
+    else:
+        sys.argv[0] += ' {:s}'.format(cmd)
+
+    command = cmdsetmod.get_command(cmd)
+    #print("MOD:", command)
+
+    # If it's an alias, then switch to the real module
+    if hasattr(command, "alias"):
+        cmd = command.alias
+        command = cmdsetmod.get_command(cmd)
+        #print("MOD:", command)
+
+    # Parse the parameters
+    params = afs.argparse.parse_arguments(sys.argv[1:], command.command_arguments)
+
+    # Stick in the default cell if there isn't one
+    if "cell" not in params:
+        from afs.lib.cell import cell
+        params["cell"] = cell()
+
+    # These modules are only used from this point onwards and do not
+    # need to be imported earlier
+    from afs.exception import AFSException
+
+    try:
+        debug_level = int(os.environ.get('AFS_DEBUG_LEVEL', 0))
+    except ValueError:
+        print('Invalid AFS_DEBUG_LEVEL environment variable', file=sys.stderr)
+        sys.exit(2)
+
+    try:
+        ret = command.main(params)
+    except (AFSException, IOError) as err:
+        print(str(err), file=sys.stderr)
+        if debug_level > 0:
+            traceback.print_exc()
+        sys.exit(1)
+    except SystemExit:
+        # Triggered by the option parser when it finds bad commandline
+        # parameters.
+        sys.exit(1)
+    except KeyboardInterrupt:
+        sys.exit(1)
+    except:
+        print('Unhandled exception:', file=sys.stderr)
+        traceback.print_exc()
+        sys.exit(3)
+
+    sys.exit(ret or 0)
+
+def main():
+    try:
+        _main()
+    finally:
+        pass