#!/usr/bin/python

# Copyright (C) 2005-2006 Matthias Klose <doko@ubuntu.com>
#                         Fabio Tranchitella <kobold@debian.org>
# Copyright (C)      2007 Bernd Zeimetz <bernd@bzed.de> 
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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 License for more details.
#
# You should have received a copy of the GNU General Public License
# 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 sys
import re
import glob
import fnmatch
import shutil
import pwd, grp
import logging
import subprocess

from optparse import OptionParser

program = os.path.basename(sys.argv[0])

# Debian/Ubuntu zope packages:
# ----------------------------

zope_packages = (
    { 'name': 'zope2.6',
      'version': '2.6',
      'prefix': '/usr/lib/zope',
      'instance': '/var/lib/zope/instance',
      'zeoinstance': '/var/lib/zope/zeo',
      'pyver': '2.2'
      },
    { 'name': 'zope2.7',
      'version': '2.7',
      'prefix': '/usr/lib/zope2.7',
      'instance': '/var/lib/zope2.7/instance',
      'zeoinstance': '/var/lib/zope2.7/zeo',
      'pyver': '2.3'
      },
    { 'name': 'zope2.8',
      'version': '2.8',
      'prefix': '/usr/lib/zope2.8',
      'instance': '/var/lib/zope2.8/instance',
      'zeoinstance': '/var/lib/zope2.8/zeo',
      'pyver': '2.3'
      },
    { 'name': 'zope2.9',
      'version': '2.9',
      'prefix': '/usr/lib/zope2.9',
      'instance': '/var/lib/zope2.9/instance',
      'zeoinstance': '/var/lib/zope2.9/zeo',
      'pyver': '2.4'
      },
    { 'name': 'zope2.10',
      'version': '2.10',
      'prefix': '/usr/lib/zope2.10',
      'instance': '/var/lib/zope2.10/instance',
      'zeoinstance': '/var/lib/zope2.10/zeo',
      'pyver': '2.4'
      },
    { 'name': 'zope2.11',
      'version': '2.11',
      'prefix': '/usr/lib/zope2.11',
      'instance': '/var/lib/zope2.11/instance',
      'zeoinstance': '/var/lib/zope2.11/zeo',
      'pyver': '2.4'
      },
    { 'name': 'zope2.12',
      'version': '2.12',
      'prefix': '/usr/lib/zope2.12',
      'instance': '/var/lib/zope2.12/instance',
      'zeoinstance': '/var/lib/zope2.12/zeo',
      'pyver': '2.6'
      },
    { 'name': 'zope2.13',
      'version': '2.13',
      'prefix': '/usr/lib/zope2.13',
      'instance': '/var/lib/zope2.13/instance',
      'zeoinstance': '/var/lib/zope2.13/zeo',
      'pyver': '2.6'
      },
    { 'name': 'zope2.14',
      'version': '2.14',
      'prefix': '/usr/lib/zope2.14',
      'instance': '/var/lib/zope2.14/instance',
      'zeoinstance': '/var/lib/zope2.14/zeo',
      'pyver': '2.7'
      },
    { 'name': 'zope3',
      'version': '3',
      'prefix': '/usr/lib/zope3',
      'instance': '/var/lib/zope3/instance',
      'zeoinstance': '/var/lib/zope3/zeo',
      'pyver': '2.4'
      }
    )

# installation types for addons:
# ------------------------------
# ADDON_MASTER      addon from Debian package in /usr/lib/zope
# [a-z]DDON_LINKED      addon in instance, addon directory symlinked to ADDON_MASTER
# ADDON_TREELINKED  addon in instance, all files in addon symlinked to ADDON_MASTER
# ADDON_COPIED      addon in instance, copied from ADDON_MASTER
# ADDON_MANUAL      addon in instance without .dzfile, manually copied?

NO_ADDON, ADDON_MASTER, ADDON_LINKED, ADDON_TREELINKED, ADDON_COPIED, ADDON_MANUAL = range(6)
addon_options = ['-', 'master', 'linked', 'tree-linked', 'copied', 'manually installed']
addon_techniques = addon_options[ADDON_LINKED:ADDON_COPIED+1]
addon_modes = ['all', 'manual']

def addon_technique_name(code):
    return addon_options[code]

def addon_technique_code(name):
    return addon_options.index(name)

known_actions = {}
def register_action(action_class):
    known_actions[action_class.name] = action_class

personal_conf = {}

class DZError(Exception):
    pass

# --------------------------------------------------------------------------- #

def is_root():
    return os.getuid() == 0

def strlist(sequence):
    return ', '.join(["`%s'" % s for s in sequence])

def available_zope_versions():
    return [z['version'] for z in zope_packages if os.path.isdir(z['prefix'])]

def filter_zope_version(version):
    return [z for z in zope_packages if z['version'] == version][0]

def read_config_file(fn, required=None):
    attrs = {}
    for line in file(fn):
        line = line.strip()
        if not line or line.startswith('#'):
            continue
        try:
            key, value = [field.strip() for field in line.split(':', 1)]
            if value.count('#') > 0: value = value.split('#')[0].strip()
        except:
            raise DZError, "error reading %s\n\t%s" % (fn, line)
        if attrs.has_key(key):
            raise DZError, "duplicate field `%s' in %s" % (key, fn)
        if value == '':
            raise DZError, "missing value for field `%s' in %s" % (key, fn)
        attrs[key] = value
    if required:
        for attr in required:
            if not attrs.has_key(attr):
                raise DZError, "missing field `%s' in %s" % (attr, fn)
    return attrs

def write_config_file(fn, attrs, uid=None, gid=None):
    fd = file(fn, 'w')
    for key, value in attrs.items():
        if value != None:
            fd.write('%s: %s\n' % (key, value))
    fd.close()

# --------------------------------------------------------------------------- #

class InstanceListFile:
    def __init__(self, mode, addon_name, oklist=True):
        if oklist:
            self.fn = '/var/lib/zope/dzhandle/%s__%s' % (mode, addon_name)
        else:
            self.fn = '/var/lib/zope/dzhandle/%s_failed__%s' % (mode, addon_name)
        self.read()

    def read(self):
        """read file with <instance name> <instance version> lines,
        return list of fields, empty list, if file does not exist"""
        self.lines = []
        try:
            fd = file(self.fn)
            for line in fd.readlines():
                fields = line.split()
                if not (fields[:2]) in self.lines:
                    self.lines.append(fields[:2])
            self.exists = True
            fd.close()
        except IOError:
            self.exists = False

    def write(self):
        if self.lines:
            fd = file(self.fn, 'w')
            for name, version in self.lines:
                fd.write("%s %s\n" % (name, version))
            fd.close()
        else:
            os.unlink(self.fn)

    def remove(self, instance):
        """remove instance from list and write file,
        unlink file, if list is empty"""
        try:
            self.lines.remove([instance.name, instance.version])
        except ValueError:
            return
        self.write()

    def append(self, instance):
        """append instance information to instance list"""
        info = [instance.name, instance.version]
        if info in self.lines:
            return
        self.lines.append(info)
        self.write()

    def get_lines(self):
        for line in self.lines:
            yield line

class Addon:
    def __init__(self, path, dzfile_required=False):
        self.type = None
        if not os.path.isdir(path):
            raise DZError, "%s directory doesn't exist: `%s'" % (self.kind, path)
        self.path = path
        if not os.path.isfile(os.path.join(path, self.dzname)):
            if dzfile_required:
                raise DZError, "`%s' file not found`%s'" \
                      % (self.dzname, os.path.join(self.path, self.dzname))
            self.type = None
            self.name = os.path.basename(path)
            self.directory = self.name.split(':', 1)[0]
            self.package = 'unknown'
            self.version = os.path.exists(os.path.join(self.path, 'version.txt')) and \
                open(os.path.join(self.path, 'version.txt')).read().strip() or 'unknown'
            self.depends = None
            self.recommends = None
            self.suggests = None
            self.is_global = False
            self.zopeversions = " ".join([z['version'] for z in zope_packages])
        else:
            self.read_dzfile()
        if self.type == None:
            if path.startswith('/usr/share/zope') or \
               path.startswith('/usr/lib/zope') or \
               path.startswith('/usr/local/share/zope'):
                self.type = ADDON_MASTER
            elif os.path.islink(path):
                self.type = ADDON_LINKED
            elif os.path.islink(os.path.join(path, '__init__.py')):
                self.type = ADDON_TREELINKED
            else:
                self.type = ADDON_COPIED

    def read_dzfile(self):
        fn = os.path.join(self.path, self.dzname)
        attrs = read_config_file(fn, ('Name', 'Package', 'Version'))
        self.name = attrs['Name']
        self.package = attrs['Package']
        self.version = attrs['Version']
        self.directory = attrs.get('Directory', self.name)
        if not os.path.basename(self.path) in (self.name, self.directory):
            raise DZError, "Product or Directory field doesn't match path in `%s'" % fn
        self.depends = attrs.get('Depends', None)
        self.recommends = attrs.get('Recommends', None)
        self.suggests = attrs.get('Suggests', None)
        self.zopeversions = attrs.get('ZopeVersions', \
                            " ".join([z['version'] for z in zope_packages]))
        self.is_global = (attrs.get('Global', None) == 'yes')
    
    def write_dzfile(self, fn=None):
        fn = fn or os.path.join(self.path, self.dzname)
        fd = file(fn, 'w')
        fd.write('Package: %s\n' % self.package)
        fd.write('Name: %s\n' % self.name)
        fd.write('Version: %s\n' % self.version)
        if self.name != self.directory:
            fd.write('Directory: %s\n' % self.directory)
        if self.depends:
            fd.write('Depends: %s\n' % self.depends)
        if self.recommends:
            fd.write('Recommends: %s\n' % self.recommends)
        if self.suggests:
            fd.write('Suggests: %s\n' % self.suggests)
        if self.zopeversions:
            fd.write('ZopeVersions: %s\n' % self.zopeversions)
        fd.close()

    def available_in_instance(self, instance):
        """checks if the addon is available in the instance
        """

        if self.is_global:
            ipath = os.path.join(instance.home, self.subdir, self.name + '.installed')
            return os.path.exists(ipath)

        ipath = os.path.join(instance.home, self.subdir, self.name)
        return os.path.isdir(ipath)

    def installed_in_instance(self, instance):
        """If the addon is available in the instance, return it, else return None.
        """
        
        if self.is_global:
            ipath = os.path.join(instance.home, self.subdir, self.name + '.installed')
            return os.path.exists(ipath) and self or None

        ipath = os.path.join(instance.home, self.subdir, self.name)
        if not os.path.isdir(ipath):
            return None
        iaddon = self.__class__(ipath)
        return iaddon
    
    def installed_by_in_instances(self, instances, exclude=[]):
        """Return a list of addons, which are installed by this master addon in
        instances.  Filter out all instances with a addon type found in exclude.
        """
        assert self.type == ADDON_MASTER

        iaddons = []
        for instance in instances:
            iaddon = self.installed_in_instance(instance)
            if iaddon and not iaddon.type in exclude and iaddon.installed_by(self):
                iaddons.append(iaddon)
        return iaddons

    def find_master(self):
        """locate the addon, that this addon was installed from"""

        if self.type == ADDON_MANUAL:
            return None
        master = None
        for prefix in ('/usr/share/zope', '/usr/lib/zope', '/usr/local/share/zope'):
            path = os.path.join(prefix, self.subdir, self.directory)
            try:
                master = self.addonClass(path)
            except DZError, msg:
                pass
            else:
                break
        if not master:
            raise DZError, msg
        return master
        
    def installed_by(self, master):
        """checks if the installed addon was installed by the `master' addon
        """
        assert self.type != ADDON_MASTER and master.type == ADDON_MASTER or self.is_global
        
        if self.is_global:
            return True
        elif self.name != master.name or self.package != master.package:
            # other addon, or same addon installed by other package
            return False
        elif self.type == ADDON_COPIED:
            # copied from master to instance, version may mismatch
            return True
        
        # consistency checks ...
        realpath = os.path.realpath(self.path)
        master_realpath = os.path.realpath(master.path)
        if self.type == ADDON_LINKED and realpath != master_realpath:
            print "CONSISTENCY CHECK FAILED: %s != %s" % (realpath, master_realpath)
            return False
        realpath = os.path.realpath(os.path.join(self.path, self.dzname))
        master_realpath = os.path.realpath(os.path.join(master.path, self.dzname))
        if self.type == ADDON_TREELINKED and realpath != master_realpath:
            print "CONSISTENCY CHECK FAILED: %s != %s" % (realpath, master_realpath)
            return False
        
        return True
        
    def printit(self, stream=sys.stdout):
        print >>stream, self.kind, self.name, self.directory, \
            self.package, self.version, self.path, self.depends
        
class ProductAttributes:
    kind = 'product'
    subdir = 'Products'
    dzname = '.dzproduct'

class ExtensionAttributes:
    kind = 'extension'
    subdir = 'Extensions'
    dzname = '.dzextension'

class Product(ProductAttributes, Addon):
    pass

class Extension(ExtensionAttributes, Addon):
    pass

ProductAttributes.addonClass = Product
ExtensionAttributes.addonClass = Extension

# --------------------------------------------------------------------------- #

class DZAction:
    _option_parser = None
    name = None
    help = ""
    list_on_help = True
    def __init__(self):
        self.errors_occured = 0
        parser = self.get_option_parser()
        parser.set_usage(
            'usage: %s [<option> ...] %s [<option> ...]' % (program, self.name))

    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            self._option_parser = p
        return self._option_parser

    def info(self, msg, stream=sys.stderr):
        logging.info('%s %s: %s', program, self.name, msg)

    def warn(self, msg, stream=sys.stderr):
        logging.warn('%s %s: %s', program, self.name, msg)

    def error(self, msg, stream=sys.stderr, go_on=False):
        logging.error('%s %s: %s', program, self.name, msg)
        self.errors_occured += 1
        if not go_on:
            sys.exit(1)

    def parse_args(self, arguments):
        self.options, self.args = self._option_parser.parse_args(arguments)
        return self.options, self.args

    def check_args(self, global_options):
        return self.errors_occured

    def run(self, global_opts):
        pass

# --------------------------------------------------------------------------- #

class DZPreinst(DZAction):
    kind = None
    list_on_help = False
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-m', '--mode',
                         action='store', dest='mode')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) != 3:
            self.error('--mode=<script args> <package> <directory> <%s name>' % self.kind)
        if self.options.mode == None:
            self.error("missing `--mode=<script args>'")
        self.mode_args = self.options.mode.split()
        if not self.mode_args[0] in ('install', 'upgrade'):
            self.error("unknown mode `%s'" % self.options.mode)
        self.mode = self.mode_args[0]
        del self.mode_args[0]
        package, directory, addon_name = self.args

        ilist = InstanceListFile(self.mode, addon_name)
        if ilist.exists:
            self.info("file `%s' exists" % ilist.fn)
        ilist = InstanceListFile(self.mode, addon_name, oklist=False)
        if ilist.exists:
            self.info("file `%s' exists" % ilist.fn)

        # nothing to check for upgrades
        if self.mode == 'upgrade':
            return self.errors_occured
        
        instances = locate_instances(versions=global_options.zversion)
        for instance in instances:
            addon_path = os.path.join(instance.home, self.subdir, addon_name)
            
            if instance.is_addon_excluded(addon_name):
                continue
            if not os.path.exists(addon_path):
                # not installed in instance
                continue
            try:
                iaddon = self.addonClass(directory)
            except DZError, msg:
                # some error reading the dzfile ... 
                # maybe the product hasn't been unpacked yet, let's skip it
                continue
            if iaddon.type == ADDON_MANUAL:
                if instance.addon_installation_required():
                    self.error("keep manually installed %s `%s' in instance "
                              "`%s' (%s), ignoring %s `%s' from package `%s'." %
                              (self.kind, iaddon.name, instance.name, instance.version,
                               self.kind, addon_name, package),
                               go_on=True)
                else:
                    self.info("keep manually installed %s `%s' in instance "
                              "`%s' (%s), ignoring %s `%s' from package `%s'." %
                              (self.kind, iaddon.name, instance.name, instance.version,
                               self.kind, addon_name, package))
                continue
            if (iaddon.package, iaddon.name, iaddon.directory) \
                   != (package, addon_name, os.path.basename(directory)):
                # not installed from this product
                self.info("found %s `%s' installed from package `%s' in instance "
                          "`%s' (%s) while installing package `%s'." %
                          (self.kind, iaddon.name, iaddon.package,
                           instance.name, instance.version, package))
                continue
            if iaddon.type == ADDON_COPIED:
                if instance.addon_installation_required():
                    self.error("keep copied %s `%s' in instance "
                               "`%s' (%s), ignoring %s `%s' from package `%s'." %
                               (self.kind, iaddon.name, instance.name, instance.version,
                                self.kind, addon_name, package),
                               go_on=True)
                    self.warn("keep copied %s `%s' in instance "
                              "`%s' (%s), ignoring %s `%s' from package `%s'." %
                              (self.kind, iaddon.name, instance.name, instance.version,
                               self.kind, addon_name, package))
                continue
            assert iaddon.type in (ADDON_LINKED, ADDON_TREELINKED)

        return self.errors_occured

    def run(self, global_options):
        pass
        
class DZPreinstProduct(ProductAttributes, DZPreinst):
    name = 'preinst-%s' % ProductAttributes.kind
    help = 'handle preinst of a packaged %s' % ProductAttributes.kind

class DZPreinstExtension(ExtensionAttributes, DZPreinst):
    name = 'preinst-%s' % ExtensionAttributes.kind
    help = 'handle preinst of a packaged %s' % ExtensionAttributes.kind
    list_on_help = False

register_action(DZPreinstProduct)
register_action(DZPreinstExtension)


class DZPostinst(DZAction):
    kind = None
    list_on_help = False
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-m', '--mode',
                         action='store', dest='mode')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) != 1:
            self.error('--mode=<script args> <directory>')
        if self.options.mode == None:
            self.error("missing `--mode=<script arguments>'")
        self.mode_args = self.options.mode.split()
        if not self.mode_args[0] in ('configure', 'abort-upgrade', 'abort-remove'):
            self.error("unknown mode `%s'" % self.options.mode)
        self.mode = self.mode_args[0]
        del self.mode_args[0]
        
        directory = self.args[0]
        try:
            self.addon = addon = self.addonClass(directory)
        except DZError, msg:
            self.error(msg)

        all_instances = locate_instances(versions=global_options.zversion)

        if self.mode == 'configure':
            # instances which are beeing upgraded go first
            self.upgrade_ilist = InstanceListFile('upgrade', addon.directory)
            self.toupgrade = []
            for i_name, i_version in self.upgrade_ilist.get_lines():
                match = [i for i in all_instances
                         if i.name == i_name and i.version == i_version]
                if len(match) > 0:
                    self.toupgrade.append(match[0])
            
            self.toinstall = self.toupgrade[:]

            # new installations, same checks as in 'preinst install'
            for instance in all_instances:
                addon_path = os.path.join(instance.home, self.subdir, addon.name)

                if instance in self.toupgrade:
                    continue
                if instance.is_addon_excluded(addon.name):
                    continue
                if not instance.addon_installation_required():
                    continue
                if not os.path.exists(addon_path):
                    # not installed in instance
                    if instance.version in self.addon.zopeversions:
                        self.toinstall.append(instance)
                    continue
                try:
                    iaddon = self.addonClass(addon_path)
                except DZError, msg:
                    # some error reading the dzfile ...
                    self.error(msg, go_on=True)
                    continue
                if iaddon.type == ADDON_MANUAL:
                    if instance.addon_installation_required():
                        self.error("keep manually installed %s `%s' in instance "
                                  "`%s' (%s), ignoring %s `%s' from package `%s'." %
                                  (self.kind, iaddon.name, instance.name, instance.version,
                                   self.kind, addon.name, addon.package),
                                   go_on=True)
                    else:
                        self.info("keep manually installed %s `%s' in instance "
                                  "`%s' (%s), ignoring %s `%s' from package `%s'." %
                                  (self.kind, iaddon.name, instance.name, instance.version,
                                   self.kind, addon.name, addon.package))
                    continue
                if (iaddon.package, iaddon.name, iaddon.directory) \
                       != (addon.package, addon.name, os.path.basename(directory)):
                    # not installed from this product
                    self.info("found %s `%s' installed from package `%s' in instance "
                              "`%s' (%s) while installing package `%s'." %
                              (self.kind, iaddon.name, iaddon.package,
                               instance.name, instance.version, addon.package))
                    continue
                if iaddon.type == ADDON_COPIED:
                    if instance.addon_installation_required():
                        self.error("keep copied %s `%s' in instance "
                                   "`%s' (%s), ignoring %s `%s' from package `%s'." %
                                   (self.kind, iaddon.name, instance.name, instance.version,
                                    self.kind, addon.name, addon.package),
                                   go_on=True)
                        self.warn("keep copied %s `%s' in instance "
                                  "`%s' (%s), ignoring %s `%s' from package `%s'." %
                                  (self.kind, iaddon.name, instance.name, instance.version,
                                   self.kind, addon.name, addon.package))
                    continue
                assert iaddon.type in (ADDON_LINKED, ADDON_TREELINKED)
                self.toinstall.append(instance)
                
        elif self.mode == 'abort-upgrade':
            self.info("%s nyi" % self.mode)
        elif self.mode == 'abort-remove':
            self.info("%s nyi" % self.mode)

        return self.errors_occured
        
    def run(self, global_options):
        if self.mode == 'configure':
            for instance in self.toinstall:
                if instance in self.toupgrade:
                    self.toupgrade.remove(instance)
                    try:
                        instance.add_addon(self.addon, global_options)
                    except DZError, msg:
                        failed_ilist = InstanceListFile('upgrade', self.addon.directory, oklist=False)
                        failed_ilist.append(instance)
                        self.error(msg)
                    else:
                        self.upgrade_ilist.remove(instance)
                else:
                    try:
                        instance.add_addon(self.addon, global_options)
                    except DZError, msg:
                        self.error(msg)
                instance.handle_restart_policy()
                
        elif self.mode == 'abort-upgrade':
            self.info("%s nyi" % self.mode)
        elif self.mode == 'abort-remove':
            self.info("%s nyi" % self.mode)
        
class DZPostinstProduct(ProductAttributes, DZPostinst):
    name = 'postinst-%s' % ProductAttributes.kind
    help = 'handle postinst of a packaged %s' % ProductAttributes.kind
    list_on_help = False

class DZPostinstExtension(ExtensionAttributes, DZPostinst):
    name = 'postinst-%s' % ExtensionAttributes.kind
    help = 'handle postinst of a packaged %s' % ExtensionAttributes.kind
    list_on_help = False

register_action(DZPostinstProduct)
register_action(DZPostinstExtension)


class DZPrerm(DZAction):
    kind = None
    list_on_help = False
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-m', '--mode',
                         action='store', dest='mode')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) != 1:
            self.error('--mode=<script args> <directory>')
        if self.options.mode == None:
            self.error("missing `--mode=<script arguments>'")
        self.mode_args = self.options.mode.split()
        if not self.mode_args[0] in ('upgrade', 'remove'):
            self.error("unknown mode `%s'" % self.options.mode)
        self.mode = self.mode_args[0]
        del self.mode_args[0]

        directory = self.args[0]
        try:
            self.addon = self.addonClass(directory)
        except DZError, msg:
            self.error(msg)

        instances = locate_instances(versions=global_options.zversion)
        self.toremove = []
        for instance in instances:
            iaddon = self.addon.installed_in_instance(instance)
            if not iaddon:
                continue
            elif iaddon.installed_by(self.addon):
                # consider removal
                if iaddon.type == ADDON_COPIED:
                    if global_options.force:
                        self.toremove.append((iaddon, instance))
                    else:
                        self.warn("keep copy of %s `%s' in `%s'"
                                  % (iaddon.kind, iaddon.name, instance.name))
                        
                else:
                    assert iaddon.type in (ADDON_LINKED, ADDON_TREELINKED) or \
                           iaddon.is_global
                    if self.mode == 'remove' and instance.addon_mode == 'manual':
                        self.warn("denied removal of %s `%s' from instance `%s' (%s)"
                                   " in addon-mode `%s'"
                                   % (iaddon.kind, iaddon.name, instance.name,
                                      instance.version, instance.addon_mode))
                    else:
                        self.toremove.append((iaddon, instance))
            else:
                # manually installed addon, or installed by another package
                self.warn("keep %s `%s' in instance `%s' not installed from `%s'" %
                          (iaddon.kind, iaddon.name, instance.name, self.addon.path))

        return self.errors_occured

    def run(self, global_options):
        for iaddon, instance in self.toremove:
            try:
                instance.remove_addon(self.addon, global_options)
            except DZError, msg:
                failed_ilist = InstanceListFile(self.mode,
                                                self.addon.directory,
                                                oklist=False)
                failed_ilist.append(instance)
                self.error(msg)
            else:
                if self.mode == 'upgrade':
                    ilist = InstanceListFile(self.mode, self.addon.directory)
                    ilist.append(instance)

        
class DZPrermProduct(ProductAttributes, DZPrerm):
    name = 'prerm-%s' % ProductAttributes.kind
    help = 'handle prerm of a packaged %s' % ProductAttributes.kind

class DZPrermExtension(ExtensionAttributes, DZPrerm):
    name = 'prerm-%s' % ExtensionAttributes.kind
    help = 'handle prerm of a packaged %s' % ExtensionAttributes.kind
    list_on_help = False

register_action(DZPrermProduct)
register_action(DZPrermExtension)


class DZPostrm(DZAction):
    kind = None
    list_on_help = False
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-m', '--mode',
                         action='store', dest='mode')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) != 3:
            self.error('--mode=<script args> <package> <directory> <%s name>' % self.kind)
        if self.options.mode == None:
            self.error("missing `--mode=<script arguments>'")
        self.mode_args = self.options.mode.split()
        if not self.mode_args[0] in ('remove'):
            self.error("unknown mode `%s'" % self.options.mode)
        self.mode = self.mode_args[0]
        del self.mode_args[0]
        package, directory, addon_name = self.args
        return self.errors_occured
    
    def run(self, global_options):
        pass
        
class DZPostrmProduct(ProductAttributes, DZPostrm):
    name = 'postrm-%s' % ProductAttributes.kind
    help = 'handle postrm of a packaged %s' % ProductAttributes.kind

class DZPostrmExtension(ExtensionAttributes, DZPostrm):
    name = 'postrm-%s' % ExtensionAttributes.kind
    help = 'handle postrm of a packaged %s' % ExtensionAttributes.kind

register_action(DZPostrmProduct)
register_action(DZPostrmExtension)


class DZDinstall(DZAction):
    kind = None
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-r', '--restart',
                         action='store', dest='restart', default='end')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if not self.args:
            self.error('no %ss to install' % self.kind)
        if not self.options.restart in ('configuring', 'end', 'manually'):
            self.error("invalid restart argument `%s'" % self.options.restart)
        self.addons = []
        for arg in self.args:
            try:
                addon = self.addonClass(arg)
            except DZError, msg:
                self.error(msg)
            self.addons.append(addon)

        instances = locate_instances()
        # consider instances with Addon-Mode 'all' only
        self.instances = [i for i in instances if i.addon_installation_required()]
        self.toinstall = []
        for addon in self.addons:
            for instance in self.instances:
                iaddon = addon.installed_in_instance(instance)
                if iaddon:
                    # verify it's from the same package
                    if iaddon.installed_by(addon):
                        # safe to upgrade
                        self.toinstall.append((addon, iaddon, instance))
                    elif iaddon.type == ADDON_MANUAL:
                        self.error("found manually installed %s `%s' in instance "
                                   "`%s' (%s) while installing package `%s'. Keep it" %
                                   (self.kind, iaddon.name, instance.name, instance.version,
                                    addon.package),
                                   go_on=True)
                    elif iaddon.package != addon.package:
                        self.error("found %s `%s' installed from package `%s' in instance "
                                   "`%s' (%s) while installing package `%s'. Keep it" %
                                   (self.kind, iaddon.name, iaddon.package,
                                    instance.name, instance.version, addon.package),
                                   go_on=True)
                else:
                    self.toinstall.append((addon, None, instance))
        return self.errors_occured

    def run(self, global_options):
        for addon, old_addon, instance in self.toinstall:
            if old_addon:
                instance.remove_addon(old_addon, global_options)
            instance.add_addon(addon, global_options)
            instance.handle_restart_policy()

class DZDinstallProduct(ProductAttributes, DZDinstall):
    name = 'dinstall-product'
    help = 'install a packaged product'
    list_on_help = False

class DZDinstallExtension(ExtensionAttributes, DZDinstall):
    name = 'dinstall-extension'
    help = 'install a packaged extension'
    list_on_help = False

register_action(DZDinstallProduct)
register_action(DZDinstallExtension)


class DZDremove(DZAction):
    kind = None
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if not self.args:
            self.error('no %ss to remove' % self.kind)
        self.addons = []
        for arg in self.args:
            try:
                addon = self.addonClass(arg)
            except DZError, msg:
                self.error(msg)
            else:
                self.addons.append(addon)

        instances = locate_instances()
        self.toremove = []
        for addon in self.addons:
            for instance in instances:
                iaddon = addon.installed_in_instance(instance)
                if not iaddon:
                    continue
                if iaddon.installed_by(addon):
                    # consider removal
                    if iaddon.type == ADDON_COPIED:
                        if global_options.force:
                            self.toremove.append((addon, iaddon, instance))
                        else:
                            self.warn("leaving copy of %s `%s' in `%s'"
                                      % (iaddon.kind, iaddon.name, instance.name))
                    else:
                        assert iaddon.type in (ADDON_LINKED, ADDON_TREELINKED)
                        self.toremove.append((addon, iaddon, instance))
                else:
                    # manually installed addon, or installed by another package
                    self.warn("leaving %s `%s' in instance `%s' not installed from `%s'" %
                              (iaddon.kind, iaddon.name, instance.name, addon.path))
        return self.errors_occured
                    
    def run(self, global_options):
        for addon, iaddon, instance in self.toremove:
            instance.remove_addon(addon, global_options)


class DZDremoveProduct(ProductAttributes, DZDremove):
    name = 'dremove-product'
    help = 'remove a packaged product'
    list_on_help = False
    
class DZDremoveExtension(ExtensionAttributes, DZDremove):
    name = 'dremove-extension'
    help = 'remove a packaged extension'
    list_on_help = False

register_action(DZDremoveProduct)
register_action(DZDremoveExtension)


class DZAddAddon(DZAction):
    kind = None
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-l', '--lazy',
                         help="add missing addons only (error on manually installed addons)",
                         default=False, action='store_true', dest='lazy')
            p.add_option('-t', '--installation-technique',
                         help="how to install addons (linked, tree-linked, copied)",
                         default=None, action='store', dest='atechnique')
            self._option_parser = p
        return self._option_parser

    def find_addon(self, arg, lazy = False):
        candidates = [addon for addon in self.locatedaddons
                      if (getattr(addon, 'directory', None) == arg) and \
                         addon.zopeversions.count(self.instance.version) > 0]

        if not candidates:
            candidates = [addon for addon in self.locatedaddons
                          if (addon.name == arg) and \
                              addon.zopeversions.count(self.instance.version) > 0]

        if candidates: return candidates[0]
        elif not lazy: self.error("unknown %s `%s'" % (self.kind, arg))
        else: return None

    def find_related(self, related, installed):
        if not related:
            return []

        depends = []
        for d in related.replace(' ','').split(','):
            if d.count('|') > 0:
                match = None
                for alt in d.split('|'):
                    match = [i for i in installed if i.name == alt or \
                             getattr(i, 'directory', None) == alt]
                    if match: break
                if match: continue

                for alt in d.split('|'):
                    match = self.find_addon(alt, True)
                    if match: break
            else:
                match = self.find_addon(d)

            if match and match not in (installed + depends):
                depends.append(match)
                depends += self.find_related(match.depends, installed + depends)

        return depends

    def check_args(self, global_options):
        if len(self.args) < 2:
            self.error('<instance> <%s> [<%s>]' % (self.kind, self.kind))
        try:
            instance = locate_instance(self.args[0],
                                       versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)

        self.instance = instance

        if self.options.atechnique:
            if not self.options.atechnique in addon_techniques:
                self.error("unknown addon technique `%s'" % self.options.atechnique)
            self.options.atechnique = addon_technique_code(self.options.atechnique)
            if self.options.atechnique != instance.addon_technique \
                   and not global_options.force:
                self.error("addon technique `%s' doesn't match "
                           "the instance's default (`%s'). Use -f to override"
                           % (self.options.atechnique, instance.addon_technique))

        self.addons = []
        self.locatedaddons = locate_addons(self.addonClass, zopeversions=global_options.zversion)
        for arg in self.args[1:]:
            self.addons.append(self.find_addon(arg))

        to_be_installed = [i.name for i in self.addons]

        self.installed = instance.installed_addons()
        for i in self.addons:
            self.addons += self.find_related(i.depends, self.installed + self.addons)

        for addon in self.addons[:]:
            iaddon = addon.installed_in_instance(instance)
            if not iaddon:
                continue
            if iaddon.installed_by(addon):
                if self.options.lazy:
                    # warn about copied addons
                    if iaddon.type == ADDON_COPIED:
                        self.warn("%s `%s' (%s, %s) in instance `%s' (%s) . Keep it" %
                                  (self.kind, iaddon.name, iaddon.version,
                                   addon_technique_name(iaddon.type),
                                   instance.name, instance.version))
                    self.addons.remove(addon)
                elif addon.name not in to_be_installed:
                    self.addons.remove(addon)
                else:
                    self.error("%s `%s' (%s, %s) in instance `%s' (%s) . Keep it" %
                           (self.kind, iaddon.name, iaddon.version,
                            addon_technique_name(iaddon.type),
                            instance.name, instance.version),
                           go_on=True)
            elif iaddon.type == ADDON_MANUAL:
                self.error("manually installed %s `%s' in instance `%s' (%s). Keep it" %
                           (self.kind, iaddon.name, instance.name, instance.version),
                           go_on=True)
            else:
                self.error("found %s `%s' installed from package `%s' in instance "
                           "`%s' (%s) while adding adding %s from `%s'. Keep it" %
                           (self.kind, iaddon.name, iaddon.package,
                            instance.name, instance.version, self.kind, addon.package),
                           go_on=True)

        return self.errors_occured
    
    def run(self, global_options):
        for addon in self.addons:
            self.instance.add_addon(addon, global_options, self.options.atechnique)
            self.info("added %s `%s' to instance `%s' (%s)"
                      % (self.kind, addon.name, self.instance.name, self.instance.version))


class DZAddProduct(ProductAttributes, DZAddAddon):
    name = 'add-product'
    help = 'add a product to an instance'

class DZAddExtension(ExtensionAttributes, DZAddAddon):
    name = 'add-extension'
    help = 'add an extension to an instance'

register_action(DZAddProduct)
register_action(DZAddExtension)


class DZRemoveAddon(DZAction):
    kind = None
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-f', '--force',
                         help="force removal of %ss" % self.kind,
                         default=False, action='store_true', dest='force')
            p.add_option('-l', '--lazy',
                         help="don't complain about already removed addons",
                         default=False, action='store_true', dest='lazy')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) < 2:
            self.error('<instance> <%s> [<%s>]' % (self.kind, self.kind))
        try:
            self.instance = locate_instance(self.args[0],
                                            versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)

        addons = []
        for arg in self.args[1:]:
            if ':' in arg:
                addon_base, addon_suffix = arg.split(':', 1)
            else:
                addon_base, addon_suffix = arg, ''
            ipath = os.path.join(self.instance.home, self.subdir, addon_base)
            if os.path.exists(ipath + '.installed'):
                path = open(ipath + '.installed', 'r').read()
                addon = self.addonClass(path)
                addons.append((addon, addon))
                continue
            elif not os.path.isdir(ipath):
                if not self.options.lazy:
                    self.error("%s `%s' not found in instance `%s' (%s)"
                               % (self.kind, addon_base, self.instance.name, self.instance.version),
                               go_on=True)
                continue
            try:
                iaddon = self.addonClass(ipath)
            except DZError, msg:
                self.error(msg, go_on=True)
            else:
                try:
                    addon = iaddon.find_master()
                except DZError, msg:
                    self.error(msg, go_on=True)
                else:
                    addons.append((addon, iaddon))

        for addon, iaddon in addons:
            if global_options.force or self.options.force:
                continue
            if iaddon.type in (ADDON_COPIED, ADDON_MANUAL):
                self.error("not removing %s %s `%s' from instance `%s', use -f to override"
                           % (addon_technique_name(iaddon.type), self.kind, iaddon.name,
                              self.instance.name),
                           go_on=True)
            elif self.instance.addon_mode == 'manual':
                # linked or tree-linked
                self.error("not removing %s %s `%s' from manually managed instance `%s', use -f to override"
                           % (addon_technique_name(iaddon.type), self.kind, iaddon.name,
                              self.instance.name),
                           go_on=True)
                    
        self.addons = addons
        return self.errors_occured

    def run(self, global_options):
        for addon, iaddon in self.addons:
            try:
                if not addon:
                    addon = iaddon
                self.instance.remove_addon(addon, global_options)
                self.info("removed %s `%s' from instance `%s' (%s)"
                      % (self.kind, addon.name, self.instance.name, self.instance.version))
            except DZError, msg:
                self.error(msg, go_on=True)

        if self.errors_occured:
            sys.exit(1)

class DZRemoveProduct(ProductAttributes, DZRemoveAddon):
    name = 'remove-product'
    help = 'remove a product from an instance'

class DZRemoveExtension(ExtensionAttributes, DZRemoveAddon):
    name = 'remove-extension'
    help = 'remove an extension from an instance'

register_action(DZRemoveProduct)
register_action(DZRemoveExtension)


class DZListAddons(DZAction):
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if not self.args:
            # list available products/extensions
            self.instance = None
        elif len(self.args) == 1:
            try:
                self.instance = locate_instance(self.args[0],
                                                versions=global_options.zversion)
            except DZError, msg:
                self.error(msg)
        else:
            self.error('[<instance>]')
        return self.errors_occured

    def run(self, global_options):
        if self.instance:
            print "Listing %ss installed for instance" % (self.addon_type), self.instance.name
            addons = self.instance.installed_addons()
        else:
            if len(global_options.zversion) > 1:
                print "Listing %ss available for all version(s):" % (self.addon_type), ' '.join(global_options.zversion)
            addons = locate_addons(self.addonClass, zopeversions=global_options.zversion)
        for addon in addons:
            addon.printit()

class DZListProducts(ProductAttributes, DZListAddons):
    name = 'list-products'
    addon_type = 'product'
    help = 'show all products managed by dzhandle'

class DZListExtensions(ExtensionAttributes, DZListAddons):
    name = 'list-extensions'
    addon_type = 'extension'
    help = 'show all extensions managed by dzhandle'

register_action(DZListProducts)
register_action(DZListExtensions)


class DZListInstances(DZAction):
    name = 'list-instances'
    help = 'print list of instances'

    def check_args(self, global_options):
        if len(self.args) > 1:
            self.error("[<instance pattern>]")
        self.instances = locate_instances(versions=global_options.zversion)
        if self.args:
            pattern = self.args[0]
            instances = [i for i in self.instances if fnmatch.fnmatch(i.name, pattern)]
            if not instances:
                self.error("no instance matching pattern `%s'" % pattern)
            self.instances = instances
        return self.errors_occured

    def run(self, global_options):
        for instance in self.instances:
            print instance.formatted_str(global_options.verbose)
            if global_options.verbose:
                pass

register_action(DZListInstances)
 

class DZListZeoInstances(DZAction):
    name = 'list-zeoinstances'
    help = 'print list of ZEO instances'

    def check_args(self, global_options):
        if len(self.args) > 1:
            self.error("[<instance pattern>]")
        self.instances = locate_zeoinstances(versions=global_options.zversion)
        if self.args:
            pattern = self.args[0]
            instances = [i for i in self.instances if fnmatch.fnmatch(i.name, pattern)]
            if not instances:
                self.error("no ZEO instance matching pattern `%s'" % pattern)
            self.instances = instances
        return self.errors_occured

    def run(self, global_options):
        for instance in self.instances:
            print instance.formatted_str(global_options.verbose)
            if global_options.verbose:
                pass

register_action(DZListZeoInstances)


class DZRestartPendingInstances(DZAction):
    name = 'restart-pending-instances'
    help = "restart instances with `restart-pending' markers"
    list_on_help = False

    def check_args(self, global_options):
        if self.args:
            self.error("no arguments required")
        return self.errors_occured

    def run(self, global_options):
        for zversion in available_zope_versions():
            zope = filter_zope_version(version=zversion)
            pattern = os.path.join(zope['instance'], '*', 'var', 'restart-pending')
            for pending in glob.glob(pattern):
                home = os.path.dirname(os.path.dirname(pending))
                instance = ZopeInstance(home, zope['version'])
                if instance.is_running():
                    #instance.zopectl('restart')
                    subprocess.call(['/usr/sbin/invoke-rc.d', 'zope%s' % zope['version'], 'restart', 'INSTANCE=%s' % zope['instance']])
                os.unlink(pending)

        # compatibility with old 2.6 installations
        if os.path.exists('/var/run/zope.restart'):
            subprocess.call(['/usr/sbin/invoke-rc.d', 'zope', 'restart'])
            os.unlink('/var/run/zope.restart')

register_action(DZRestartPendingInstances)


class DZShowInstance(DZAction):
    name = 'show-instance'
    help = 'print information about an instance'

    def check_args(self, global_options):
        if not self.args:
            self.error('no instance to list')
        if len(self.args) > 1:
            self.error('<instance>')
        try:
            self.instance = locate_instance(self.args[0],
                                            versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)
        return self.errors_occured

    def run(self, global_options):
        print self.instance.formatted_str(global_options.verbose)

register_action(DZShowInstance)


class DZInstanceRestartPolicy(DZAction):
    name = 'instance-restart-policy'
    help = 'get/set a policy on addon installation for an instance'

    def check_args(self, global_options):
        if not self.args:
            self.error('<instance> [<policy>]')
        self.policy = None
        if len(self.args) > 1:
            self.policy = self.args[1]
        elif len(self.args) > 2:
            self.error('<instance> [<policy>]')
        try:
            self.instance = locate_instance(self.args[0],
                                            versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)
        if self.policy:
            try:
                self.instance.set_restart_policy(self.policy)
            except DZError, msg:
                self.error(msg)
        return self.errors_occured

    def run(self, global_options):
        if self.policy:
            self.instance.set_restart_policy(self.policy)
            self.instance.write_dzfile(global_options.uid, global_options.gid)
        else:
            print self.instance.restart_policy

register_action(DZInstanceRestartPolicy)


class DZInstanceAddonTechnique(DZAction):
    name = 'instance-addon-technique'
    help = 'get/set an addon install technique for an instance'

    def check_args(self, global_options):
        if not self.args:
            self.error('<instance> [<addon technique>]')
        self.technique = None
        if len(self.args) > 1:
            self.technique = self.args[1]
            if not self.technique in addon_techniques:
                self.error("unknown addon technique `%s'" % self.technique)
        elif len(self.args) > 2:
            self.error('<instance> [<addon technique>]')
        try:
            self.instance = locate_instance(self.args[0],
                                            versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)
        return self.errors_occured

    def run(self, global_options):
        if self.technique:
            # set technique
            self.instance.set_addon_technique(self.technique)
            self.instance.write_dzfile(global_options.uid, global_options.gid)
        else:
            print self.instance.addon_technique

register_action(DZInstanceAddonTechnique)


class DZInstanceAddonMode(DZAction):
    name = 'instance-addon-mode'
    help = 'set an addon mode for an instance'

    def check_args(self, global_options):
        if not self.args:
            self.error('<instance> [<addon mode>]')
        self.addon_mode = None
        if len(self.args) > 1:
            self.addon_mode = self.args[1]
            if not self.addon_mode in addon_modes:
                self.error("unknown addon mode `%s'" % self.addon_mode)
        elif len(self.args) > 2:
            self.error('<instance> [<addon mode>]')
        try:
            self.instance = locate_instance(self.args[0],
                                            versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)
        return self.errors_occured

    def run(self, global_options):
        if self.addon_mode:
            # set addon_mode
            self.instance.set_addon_mode(self.addon_mode)
            self.instance.write_dzfile(global_options.uid, global_options.gid)
            # TODO: manual -> all: add addons
        else:
            print self.instance.addon_mode

register_action(DZInstanceAddonMode)


class DZMakeInstance(DZAction):
    name = 'make-instance'
    help = 'run zope version mkzopeinstance'

    def __init__(self):
        DZAction.__init__(self)
        self.get_option_parser().set_usage(
            'usage: %s [<option> ...] %s <instance-name> [<option> ...]' % (program, self.name))

    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-m', '--addon-mode',
                         help="which products and extension to install (all, manual)",
                         action='store', dest='amode')
            p.add_option('-t', '--addon-install-technique',
                         help="how to install addons (linked, tree-linked, copied)",
                         default="tree-linked", action='store', dest='atechnique')
            p.add_option('-r', '--restart',
                         help="when to restart on configuration (configuring, end, manually)",
                         default="end", action='store', dest='restart')
            p.add_option('-u', '--user', help="user and password for the initial user (user:password)",
                         action='store', dest='user')
            p.add_option('', '--service-user', help="system user used to run this instance (user:group)",
                         action='store', dest='srvuser', default='zope:zope')
            p.add_option('', '--service-port', help="HTTP port used to run this instance",
                         action='store', dest='srvport', default='9673')
            p.add_option('', '--skelsrc', help="the directory from which skeleton files should be copied",
                         action='store', dest='skel')
            p.add_option('', '--zeo-server', help="use ZEO instead of a local ZODB database (host:port)",
                         action='store', dest='zeoserver', default=None)
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) != 1:
            self._option_parser.print_help()
            sys.exit(1)

        if not global_options.zversion:
            self.error('no zope version to make instance for')

        if len(global_options.zversion) > 1:
            self.error('ambiguous zope version to make instance for: %s'
                       % strlist(global_options.zversion))

        if self.options.user and not ":" in self.options.user:
            self.error('user must be specified as name:password')

        if not is_root() and self.options.srvuser == 'zope:zope':
            self.uid = os.getuid()
            self.gid = os.getgid()
            self.options.srvuser = "%s:%s" % \
                (pwd.getpwuid(os.getuid())[0], grp.getgrgid(os.getgid())[0])
        elif not ":" in self.options.srvuser:
            self.error('service user must be specified as user:group')
        else:
            try:
                uid, gid = ':' in self.options.srvuser and \
                    self.options.srvuser.split(':', 1) or \
                    (self.options.srvuser, 'zope')
                self.uid = pwd.getpwnam(uid)[2]
                self.gid = grp.getgrnam(gid)[2]
            except KeyError, msg:
                self.error(msg)

        self.zversion = global_options.zversion[0]
        self.instance_name = self.args[0]

        if not self.options.amode:
            self.error("missing option -m <addon-mode>")
        if not self.options.amode in addon_modes:
            self.error("unknown addon mode `%s'" % self.options.amode)

        if not self.options.atechnique in addon_techniques:
            self.error("unknown addon technique `%s'" % self.options.atechnique)
    
        if not self.options.restart in ('configuring', 'end', 'manually'):
            self.error("unknown restart policy `%s'" % self.options.restart)

        if self.options.skel:
            self.options.skel = os.path.normpath(os.path.expanduser(self.options.skel))
            if not os.path.exists(self.options.skel):
                self.error("skeleton source path `%s' does not exist" % self.options.skel)
            needed_dirs = ('bin', 'etc', 'Extensions', 'import', 'lib', 'log',
                           'Products', 'var')
            for n_dir in needed_dirs:
                if not os.path.exists("%s/%s" % (self.options.skel, n_dir)):
                    self.error("subdirectory `%s' missing in skeleton source `%s'" % (n_dir, self.options.skel))
            needed_files = ('bin/zopeservice.py.in', 'bin/runzope.bat.in',
                            'bin/runzope.in', 'bin/runzope.in', 'bin/zopectl.in',
                            'etc/zope.conf.in')
            for n_file in needed_files:
                n_file = "%s/%s" %(self.options.skel, n_file) 
                if not os.access(n_file, os.R_OK):
                    self.error("`%s' missing or not readable" % n_file)

        self.zope = filter_zope_version(version=self.zversion)
        if is_root():
            self.instance_home = os.path.join(self.zope['instance'], self.instance_name)
        else:
            self.instance_home = os.path.join(personal_conf['instances'], self.zope['name'], self.instance_name)
        if os.path.exists(self.instance_home) and \
           not os.path.exists(os.path.join(self.instance_home, 'bin', 'zopectl.disabled')):
            self.error("instance home `%s' already exists" % self.instance_home)
        return self.errors_occured

    def run(self, global_options):
        zopectl = os.path.join(self.instance_home, 'bin', 'zopectl')
        if os.path.exists(zopectl + '.disabled'):
            os.rename(zopectl + '.disabled', zopectl)
            return
        if self.zversion in ('2.6', '2.7', '2.8', '2.9', '2.10', '2.11'):
            cmd = [os.path.join(self.zope['prefix'], 'bin', 'mkzopeinstance.py')]
        else:
            cmd = [os.path.join(self.zope['prefix'], 'bin', 'mkzopeinstance')] ##, '--password-manager=MD5']
        cmd.append('--dir=%s' % self.instance_home)
        cmd.append('--layout=%s' % (is_root() and 'fhs' or 'zope'))
        if self.options.user:
            cmd.append('--user=' + self.options.user)
        if self.options.srvuser:
            cmd.append('--service-user=' + self.options.srvuser)
        if self.options.srvport:
            cmd.append('--service-port=' + self.options.srvport)
        if self.options.skel:
            cmd.append('--skelsrc=' + self.options.skel)
        if self.options.zeoserver:
            cmd.append('--zeo-server=' + self.options.zeoserver)

        if self.zope['version'] in ('2.12', '2.13', '2.14'):
            cmd.append('--python=' + os.path.join(self.zope['prefix'], 'bin', 'python'))

        # zope3's mkzopeinstance doesn't create the parents dir
        if self.zope['version'] in ('2.11', '2.12', '2.13', '2.14', '3') and not os.path.isdir(self.instance_home):
            os.makedirs(self.instance_home)
            os.rmdir(self.instance_home)

        rv = subprocess.call(cmd)
        if rv:
            subprocess.call(['/bin/rm', '-fr', self.instance_home])
            sys.exit(rv)
        
        # giving the bin dir to root:root (#388072)
        if is_root():
            for chown_file in os.listdir(os.path.join(self.instance_home, 'bin')):
                os.chown(os.path.join(self.instance_home, 'bin', chown_file), 0, 0)
            os.chown(os.path.join(self.instance_home, 'bin'), 0, 0)
            
        instance = ZopeInstance(self.instance_home, self.zversion, read_dzfile=False)
        instance.set_addon_mode(self.options.amode)
        instance.set_addon_technique(self.options.atechnique)
        instance.set_restart_policy(self.options.restart)
        instance.set_zope_user(self.options.srvuser.split(":"))
        instance.write_dzfile(uid=self.uid, gid=self.gid)
        if not instance.addon_installation_required():
            return
        for addon in locate_addons(zopeversions=global_options.zversion):
            try:
                instance.add_addon(addon, global_options, None)
            except DZError, msg:
                # more than one product version ..., ignore it for now
                self.warn(msg)

register_action(DZMakeInstance)


class DZMakeZeoInstance(DZAction):
    name = 'make-zeoinstance'
    help = 'run zope version mkzeoinstance'

    def __init__(self):
        DZAction.__init__(self)
        self.get_option_parser().set_usage(
            'usage: %s [<option> ...] %s <zeoinstance-name> [<port>]' % (program, self.name))

    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) not in (1, 2):
            self._option_parser.print_help()
            sys.exit(1)

        if not global_options.zversion:
            self.error('no zope version to make instance for')

        if len(global_options.zversion) > 1:
            self.error('ambiguous zope version to make instance for: %s'
                       % strlist(global_options.zversion))

        self.zversion = global_options.zversion[0]
        self.instance_name = self.args[0]
        if len(self.args) > 1:
            try:
                self.port = int(self.args[1])
            except ValueError:
                self.error('the zeo-instance port number has to be integer')
        else:
            self.port = None
        self.zope = filter_zope_version(version=self.zversion)

        if is_root():
            self.instance_home = os.path.join(self.zope['zeoinstance'], self.instance_name)
        else:
            self.instance_home = os.path.join(personal_conf['zeoinstances'], self.zope['name'], self.instance_name)
        if os.path.exists(self.instance_home) and \
          not os.path.exists(os.path.join(self.instance_home, 'bin', 'zeoctl.disabled')):
            self.error("instance home `%s' already exists" % self.instance_home)
        return self.errors_occured

    def run(self, global_options):
        zeoctl = os.path.join(self.instance_home, 'bin', 'zeoctl')
        if os.path.exists(zeoctl + '.disabled'):
            os.rename(zeoctl + '.disabled', zeoctl)
            return

        if self.zversion in ('2.6', '2.7', '2.8', '2.9', '2.10', '2.11'):
            cmd = [os.path.join(self.zope['prefix'], 'bin', 'mkzeoinstance.py')]
        else:
            cmd = [os.path.join(self.zope['prefix'], 'bin', 'mkzeoinstance')]

        cmd.append(self.instance_home)
        if self.port != None:
            cmd.append(str(self.port))
            
        rv = subprocess.call(cmd)
        if rv:
            subprocess.call(['/bin/rm', '-fr', self.instance_home])
            sys.exit(rv)

register_action(DZMakeZeoInstance)


class DZRemoveInstance(DZAction):
    name = 'remove-instance'
    help = 'remove addons from an instance (except data files), mark it as removed'

    def check_args(self, global_options):
        if len(self.args) != 1:
            self.error('<instance>')
        try:
            self.instance = locate_instance(self.args[0],
                                            versions=global_options.zversion)
            if self.instance.is_removed:
                self.error("Zope instance `%s' is already removed" % self.instance.name)
        except DZError, msg:
            self.error(msg)
        return self.errors_occured

    def run(self, global_options):
        os.rename(os.path.join(self.instance.home, 'bin', 'zopectl'), 
                  os.path.join(self.instance.home, 'bin', 'zopectl.disabled'))

register_action(DZRemoveInstance)


class DZRemoveZEOInstance(DZAction):
    name = 'remove-zeoinstance'
    help = 'remove a zeo instance (except data files), mark it as removed'

    def check_args(self, global_options):
        if len(self.args) != 1:
            self.error('<instance>')
        try:
            self.instance = locate_zeoinstance(self.args[0],
                                            versions=global_options.zversion)
            if self.instance.is_removed:
                self.error("ZEO instance `%s' is already removed" % self.instance.name)
        except DZError, msg:
            self.error(msg)
        return self.errors_occured

    def run(self, global_options):
        os.rename(os.path.join(self.instance.home, 'bin', 'zeoctl'),
                  os.path.join(self.instance.home, 'bin', 'zeoctl.disabled'))

register_action(DZRemoveZEOInstance)


class DZPurgeInstance(DZAction):
    name = 'purge-instance'
    help = 'purge files in an instance (including data files)'

    def check_args(self, global_options):
        if len(self.args) != 1:
            self.error('<instance>')
        try:
            self.instance = locate_instance(self.args[0],
                                            versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)
        return self.errors_occured

    def run(self, global_options):
        cmd = ["rm", "-rf", 
               self.instance.home,
               "/etc/zope%s/%s" % (self.instance.version, self.instance.name),
               "/var/log/zope%s/%s" % (self.instance.version, self.instance.name),
               ]
        rv = subprocess.call(cmd)
        if rv:
            sys.exit(rv)

register_action(DZPurgeInstance)


class DZPurgeZeoInstance(DZAction):
    name = 'purge-zeoinstance'
    help = 'purge files in a ZEO instance (including data files)'

    def check_args(self, global_options):
        if len(self.args) != 1:
            self.error('<zeo_instance>')
        try:
            self.instance = locate_zeoinstance(self.args[0],
                                            versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)
        return self.errors_occured

    def run(self, global_options):
        cmd = ["rm", "-rf", 
               self.instance.home]
        rv = subprocess.call(cmd)
        if rv:
            sys.exit(rv)

register_action(DZPurgeZeoInstance)


class DZZopectl(DZAction):
    name = 'zopectl'
    help = 'call zopectl for a given instance'

    def get_option_parser(self):
        if not self._option_parser:
            class UnknownOptionParser(OptionParser):
                def parse_args(self, args):
                    opts, tmp = OptionParser.parse_args(self, [])
                    return opts, args
            p = UnknownOptionParser()
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) < 1 or self.args[0].startswith('-'):
            self.error('<instance> <zdctl-action> [<zdctl options>]')
        try:
            self.instance = locate_instance(self.args[0],
                                            versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)
        if len(self.args) > 1 and self.args[1].startswith('-'):
            print >>sys.stderr, ("missing zdctl action, entering interactive mode ...")
        if [arg for arg in self.args if arg in ('-i', '--interactive')]:
            print >>sys.stderr, ("entering interactive mode after executing command ...")
        return self.errors_occured

    def run(self, global_options):
        rv = self.instance.zopectl(self.args[1:])
        if rv:
            sys.exit(rv)

register_action(DZZopectl)


class DZZeoctl(DZAction):
    name = 'zeoctl'
    help = 'call zeoctl for a given instance'

    def get_option_parser(self):
        if not self._option_parser:
            class UnknownOptionParser(OptionParser):
                def parse_args(self, args):
                    opts, tmp = OptionParser.parse_args(self, [])
                    return opts, args
            p = UnknownOptionParser()
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) < 1 or self.args[0].startswith('-'):
            self.error('<instance> <zeoctl-action> [<zeoctl options>]')
        try:
            self.instance = locate_zeoinstance(self.args[0],
                                            versions=global_options.zversion)
        except DZError, msg:
            self.error(msg)
        if len(self.args) > 1 and self.args[1].startswith('-'):
            print >>sys.stderr, ("missing zeoctl action, entering interactive mode ...")
        if [arg for arg in self.args if arg in ('-i', '--interactive')]:
            print >>sys.stderr, ("entering interactive mode after executing command ...")
        return self.errors_occured

    def run(self, global_options):
        rv = self.instance.zeoctl(self.args[1:])
        if rv:
            sys.exit(rv)

register_action(DZZeoctl)



class DZShowPackageInfo(DZAction):
    name = 'show-package-info'
    help = 'show various informations about zope packages'
    kind = None

    def __init__(self):
        DZAction.__init__(self)
        self.get_option_parser().set_usage(
            'usage: %s [<option> ...] %s' % (program, self.name))

    def run(self, global_options):
        package_info={}
        for package in zope_packages:
            package_info[package['version']]=[
                  "%s: %s" % (key, value) for key, value in package.iteritems()]
        
        if len(global_options.zversion) == 0:
            out=[package_info[version] for version in sorted(package_info.keys())]
        else:
            out=[package_info[version] for version in global_options.zversion]
        print '\n\n'.join(['\n'.join(info) for info in out])
        
register_action(DZShowPackageInfo)



class ZopeInstance:
    def __init__(self, home, version, read_dzfile = True):
        self.home = home
        self.name = os.path.basename(home)
        self.version = version
        self.addon_technique = None
        self.restart_policy = None
        self.addon_mode = None
        self.excluded_addons = []
        self.zope_user = None
        self.new_layout = os.path.exists(os.path.join(home, 'bin', 'runzope'))
        self.is_purged = False
        self.zeo = False
        re_zeo = re.compile('[^#]+<zeoclient>').match
        zope_conf = os.path.join(home, 'etc', 'zope.conf')
        if os.path.exists(zope_conf) and \
           filter(lambda x: re_zeo(x) is not None, open(zope_conf).readlines()):
            self.zeo = True
        else:
            self.is_purged |= not os.path.exists(os.path.join(home, 'var', 'Data.fs'))
            self.is_purged |= not os.path.exists(os.path.join(home, 'var', 'Data.fs.index'))
        self.is_removed = False
        self.is_removed |= not os.path.exists(os.path.join(home, 'bin', 'zopectl'))
        if os.path.isfile(os.path.join(home, 'inituser')):
            self.userfile = 'inituser'
        elif os.path.isfile(os.path.join(home, 'access')):
            self.userfile = 'access'
        else:
            self.userfile = None
        if read_dzfile: self.read_dzfile()
        self._installed_addons = None

    def set_addon_technique(self, addon_technique):
        old_technique = self.addon_technique
        if isinstance(addon_technique, str):
            self.addon_technique = list(addon_options).index(addon_technique)
        elif isinstance(addon_technique, int):
            self.addon_technique = addon_technique
        else:
            raise TypeError, "unknown type for addon technique"
        if old_technique != None and self.addon_technique != old_technique:
            print >>sys.stderr, "WARNING: change of instance addon technique not yet supported"
        # manage addons
        # self.write_dzfile()

    def set_zope_user(self, zopeuser):
        if len(zopeuser) != 2:
            raise ValueError, "Zope-User: `%s' is not in 'user:group' format" % ':'.join(zopeuser)
        try:
            pwd.getpwnam(zopeuser[0])
        except KeyError:
            raise ValueError, "unknown username `%s' in Zope-User option" % zopeuser[0]
        try:
            grp.getgrnam(zopeuser[1])
        except KeyError:
            raise ValueError, "unknown groupname `%s' in Zope-User option" % zopeuser[1]
        self.zope_user = zopeuser

    def set_restart_policy(self, policy):
        if policy in (None, 'configuring', 'end', 'manually'):
            self.restart_policy = policy
        else:
            raise ValueError, "unknown restart policy `%s'" % policy

    def set_addon_mode(self, addon_mode):
        if addon_mode in addon_modes:
            self.addon_mode = addon_mode
        else:
            raise ValueError, "unknown addon_mode `%s'" % addon_mode

    def addon_installation_required(self, addon=None):
        return self.addon_mode == 'all'

    def is_addon_excluded(self, name):
        return name in self.excluded_addons

    def read_dzfile(self):
        fn = os.path.join(self.home, 'etc', 'debian_policy')
        self.dzfile_exists = os.path.exists(fn)
        if not self.dzfile_exists:
            self.set_addon_mode('manual')
            self.set_addon_technique(ADDON_MANUAL)
            self.set_restart_policy('manually')
            return
        attrs = read_config_file(fn, required=['Name', 'Addon-Mode',
                                               'Addon-Technique', 'Restart-Policy'])

        try:
            self.set_addon_technique(attrs['Addon-Technique'])
        except ValueError:
            raise DZError, \
                  "unknown addon technique `%s' in `%s'" % (attrs['Addon-Technique'], fn)

        try:
            self.set_addon_mode(attrs['Addon-Mode'])
        except ValueError, msg:
            raise DZError, \
                  "%s in `%s'" % (msg, fn)

        try:
            self.set_restart_policy(attrs['Restart-Policy'])
        except KeyError:
            pass
        except ValueError, msg:
            raise DZError, "%s in `%s'" % (msg, fn)

        try:
            self.excluded_addons = attrs['Excluded-Addons'].split()
        except KeyError:
            pass

        try:
            self.set_zope_user(attrs['Zope-User'].split(':'))
        except KeyError:
            pass
        except ValueError, msg:
            raise DZError, "%s in `%s'" % (msg, fn)

        if attrs['Name'] != self.name:
            raise DZError, "instance name `%s' doesn't match in `%s'" % (attrs['Name'], fn)
            
    def write_dzfile(self, uid=None, gid=None):
        fn = os.path.join(self.home, 'etc', 'debian_policy')
        if os.path.exists(fn):
            try:
                os.path.unlink(fn + '.old')
            except:
                pass
            os.rename(fn, fn + '.old')
        attrs = {'Name': self.name,
                 'Addon-Technique': addon_technique_name(self.addon_technique),
                 'Addon-Mode': self.addon_mode,
                 'Restart-Policy': self.restart_policy
                 }
        if self.excluded_addons:
            attrs['Excluded-Addons'] = ' '.join(self.excluded_addons)
        if self.zope_user:
            attrs['Zope-User'] = ':'.join(self.zope_user)
        write_config_file(fn, attrs, uid=uid, gid=gid)

    def installed_addons(self):
        if self._installed_addons == None:
            self._installed_addons = locate_addons(instance=self)
        return self._installed_addons

    def add_addon(self, addon, global_options, addon_technique=None):
        if self.zope_user:
            try:
                uid, gid = self.zope_user
                uid = pwd.getpwnam(uid)[2]
                gid = grp.getgrnam(gid)[2]
            except KeyError, msg:
                self.error(msg)
        else:
            uid = uid=global_options.uid
            gid = global_options.gid

        installed = [a for a in self.installed_addons() if addon.name == a.name]
        if installed:
            a = installed[0]
            print "%s `%s' already available in instance `%s'" % (a.kind, a.name, self.name)
            return

        addon_technique = addon_technique or self.addon_technique
        if addon_technique == ADDON_MANUAL:
            return
        assert addon_technique in (ADDON_LINKED, ADDON_TREELINKED, ADDON_COPIED)

        if self.version.startswith("3"):
            files = []
            files.extend(glob.glob(os.path.join(addon.path, '*-configure.zcml')))
            files.extend(glob.glob(os.path.join(addon.path, '*-meta.zcml')))
            files.extend(glob.glob(os.path.join(addon.path, '*-ftesting.zcml')))
            for f in files:
                filename = os.path.split(f)[1]
                os.symlink(f, os.path.join(self.home, "etc/package-includes/", filename))

        target_path = os.path.join(self.home, addon.subdir, addon.name)
        if addon.is_global:
            open(target_path + '.installed', 'w').write(addon.path)
            target_path = addon.path
        elif addon_technique == ADDON_LINKED:
            os.symlink(addon.path, target_path)
            if global_options.verbose:
                print "linked: %s -> %s" % (target_path, addon.path)
        elif addon_technique == ADDON_TREELINKED:
            copytree(addon.path, target_path, copy_all=False,
                     uid=uid, gid=gid)
            if global_options.verbose:
                print "tree linked: %s -> %s" % (target_path, addon.path)
        elif addon_technique == ADDON_COPIED:
            copytree(addon.path, target_path, copy_all=True,
                     uid=uid, gid=gid)
            if global_options.verbose:
                print "copied: %s -> %s" % (addon.path, target_path)

        dzlib = os.path.join(addon.path, '.dzlib')
        if os.path.isdir(dzlib):
            for dir in os.listdir(dzlib):
                if not os.path.exists(os.path.join(self.home, 'lib', 'python', dir)):
                    os.symlink(os.path.join(dzlib, dir),
                        os.path.join(self.home, 'lib', 'python', dir))

        added_addon = addon.addonClass(target_path, False)
        self._installed_addons.append(added_addon)
        logging.info("added %s `%s'", added_addon.kind, added_addon.path)

    def remove_addon(self, addon, global_options):
        installed = [a for a in self.installed_addons() if getattr(addon, 'name', None) == a.name]
        if len(installed) != 1:
            raise DZError, "%s `%s' not installed in instance `%s'" \
                  % (addon.kind, addon.name, self.name)
        installed = installed[0]
        assert addon.name == installed.name

        if self.version.startswith("3"):
            files = []
            files.extend(glob.glob(os.path.join(addon.path, '*-configure.zcml')))
            files.extend(glob.glob(os.path.join(addon.path, '*-meta.zcml')))
            files.extend(glob.glob(os.path.join(addon.path, '*-ftesting.zcml')))
            for f in files:
                filename = os.path.split(f)[1]
                target = os.path.join(self.home, "etc/package-includes/", filename)
                if os.path.exists(target):
                    os.unlink(target)

        dzlib = os.path.join(addon.path, '.dzlib')
        if os.path.isdir(dzlib):
            for dir in os.listdir(dzlib):
                if os.path.exists(os.path.join(self.home, 'lib', 'python', dir)):
                    os.remove(os.path.join(self.home, 'lib', 'python', dir))

        if addon.is_global:
            os.unlink(os.path.join(self.home, addon.subdir, addon.name) + '.installed')
        elif installed.type == ADDON_LINKED:
            os.unlink(installed.path)
        elif installed.type in (ADDON_MANUAL, ADDON_COPIED) and not global_options.force:
            raise DZError, "not removing copied or manually installed %s `%s'" \
                  % (installed.kind, installed.name)
        elif installed.type in (ADDON_TREELINKED, ADDON_COPIED) and not global_options.force:
            if os.path.islink(installed.path):
                os.unlink(installed.path)
            else:
                try:
                    deltree(addon.path, installed.path)
                except Exception, msg:
                    raise DZError, msg
        elif global_options.force:
            if os.path.islink(installed.path):
                os.unlink(installed.path)
            else:
                shutil.rmtree(installed.path)
        else:
            return

        self._installed_addons.remove(installed)
        logging.info("removed %s `%s'", installed.kind, installed.path)

    def handle_restart_policy(self, policy='end', reason='-'):
        # instance restart policy overwrites option
        if self.restart_policy:
            policy = self.restart_policy

        if not self.is_running():
            return

        if policy == 'manually':
            return
        elif policy == 'end' and self.is_running():
            fd = file(os.path.join(self.home, 'var', 'restart-pending'), 'a+')
            fd.write(reason + '\n')
            fd.close()
        elif policy == 'configuring':
            rv = self.zopectl('restart')
            if rv:
                sys.exit(rv)

    def is_running(self):
        cmd = [os.path.join(self.home, 'bin', 'zopectl'), 'status']
        return os.path.isfile(cmd[0]) and \
            "program running;" in subprocess.Popen(args=cmd,
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.read()

    def zopectl(self, *args):
        cmd = [os.path.join(self.home, 'bin', 'zopectl')]
        os.chdir(self.home)
        if args and isinstance(args[0], list):
            cmd.extend(args[0])
        else:
            cmd.extend(args)
        return subprocess.call(cmd)
        
    def formatted_str(self, verbose):
        s = '%(name)-20s %(version)-5s ' % self.__dict__
        s += ' addon-mode=%s' % self.addon_mode
        if isinstance(self.addon_technique, int):
            addon_technique = addon_options[self.addon_technique]
        else:
            addon_technique = self.addon_technique
        s += ' addon-technique=%s' % addon_technique
        if self.userfile:
            s += ' userfile=' + self.userfile
        if not self.new_layout and not self.is_removed:
            s += ' layout=(2.5 and before)'
        if self.zeo:
            s += ' zeo'
        if self.is_purged:
            s += ' purged'
        elif self.is_removed:
            s += ' removed'
        if verbose:
            pass
        return s


class ZeoInstance:

    def __init__(self, home, version):
        self.home = home
        self.name = os.path.basename(home)
        self.version = version
        self.is_removed = False
        self.is_removed |= not os.path.exists(os.path.join(home, 'bin', 'zeoctl'))

    def is_running(self):
        cmd = [os.path.join(self.home, 'bin', 'zeoctl'), 'status']
        return not "not running" in subprocess.Popen(args=cmd,
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT).read()

    def zeoctl(self, *args):
        cmd = [os.path.join(self.home, 'bin', 'zeoctl')]
        os.chdir(self.home)
        if args and isinstance(args[0], list):
            cmd.extend(args[0])
        else:
            cmd.extend(args)
        return subprocess.call(cmd)
        
    def formatted_str(self, verbose):
        s = '%(name)-20s %(version)-5s ' % self.__dict__
        if verbose:
            pass
        if self.is_removed: s += ' removed'
        return s


def copytree(sourcedir, destdir, copy_all=True, uid=None, gid=None):
    os.mkdir(destdir)
    destdir = os.path.abspath(destdir)
    shutil.copymode(sourcedir, destdir)
    if not uid is None:
        os.chown(destdir, uid, gid)
    saved_pwd = os.getcwd()
    os.chdir(sourcedir)
    for root, dirs, files in os.walk(sourcedir):
        if root == sourcedir:
            relroot = ''
        else:
            relroot = root[len(sourcedir)+1:]
        for name in files:
            source = os.path.join(root, name)
            target = os.path.join(destdir, relroot, name)
            if copy_all and os.path.islink(source):
                print "copy target of symlink %s" % source
            if copy_all:
                shutil.copy2(source, target)
                if not uid is None:
                    os.chown(target, uid, gid)
            else:
                os.symlink(source, target)
        for name in dirs:
            source = os.path.join(root, name)
            target = os.path.join(destdir, relroot, name)
            if os.path.islink(source):
                print "symlink replaced by directory %s" % source
            os.mkdir(target)
            shutil.copymode(sourcedir, destdir)
            if not uid is None:
                os.chown(target, uid, gid)
    os.chdir(saved_pwd)

def deltree(sourcedir, destdir):
    """remove files found in sourcedir in destdir, remove
    corresponding .'py[co]' files as well"""

    destdir = os.path.abspath(destdir)
    dzfile = None
    if os.path.exists(os.path.join(destdir, '.dzproduct')):
        dzfile = os.path.join(destdir, '.dzproduct')
    elif os.path.exists(os.path.join(destdir, '.dzextension')):
        dzfile = os.path.join(destdir, '.dzextension')
    if dzfile:
        saved_dzfile = os.path.join(os.path.dirname(destdir),
                                    '.dz.%d' % os.getpid())
        shutil.move(dzfile, saved_dzfile)
    try:
        saved_pwd = os.getcwd()
        os.chdir(sourcedir)
        for root, dirs, files in os.walk(sourcedir, topdown=False):
            if root == sourcedir:
                relroot = ''
            else:
                relroot = root[len(sourcedir)+1:]
            for name in files:
                #source = os.path.join(root, name)
                target = os.path.join(destdir, relroot, name)

                if os.path.exists(target):
                    os.unlink(target)
                if target.endswith('.py'):
                    if os.path.exists(target + 'c'):
                        os.unlink(target + 'c')
                    if os.path.exists(target + 'o'):
                        os.unlink(target + 'o')
            for name in dirs:
                #source = os.path.join(root, name)
                target = os.path.join(destdir, relroot, name)
                if os.path.islink(target):
                    # should not happen
                    os.unlink(target)
                else:
                    try:
                        os.rmdir(target)
                    except OSError, msg:
                        pass
                        #print >>sys.stderr, "rmdir `%s' failed: %s" % (target, msg)
        os.rmdir(destdir)
    except (IOError, OSError), msg:
        print >>sys.stderr, msg
        if dzfile:
            shutil.move(saved_dzfile, dzfile)
        raise
    else:
        os.unlink(saved_dzfile)
    os.chdir(saved_pwd)

def locate_instances(versions=None):
    instances = []
    pkgs = [(z['version'], z['instance']) for z in zope_packages
            if not versions or z['version'] in versions]
    for version, instance_dir in pkgs:
        inst_list = glob.glob(os.path.join(instance_dir, '*'))
        if not is_root():
            inst_list.extend(glob.glob(os.path.join(
                personal_conf['instances'], 'zope%s' % version, '*')))
        for home in inst_list:
            if os.path.isdir(os.path.join(home, 'var')):
                instance = ZopeInstance(home, version)
                instances.append(instance)
    return instances

def locate_zeoinstances(versions=None):
    instances = []
    pkgs = [(z['version'], z['zeoinstance']) for z in zope_packages
            if not versions or z['version'] in versions]
    for version, instance_dir in pkgs:
        inst_list = glob.glob(os.path.join(instance_dir, '*'))
        if not is_root():
            inst_list.extend(glob.glob(os.path.join(
                personal_conf['zeoinstances'], 'zope%s' % version, '*')))
        for home in inst_list:
            if os.path.isdir(os.path.join(home, 'var')):
                instance = ZeoInstance(home, version)
                instances.append(instance)
    return instances

def locate_instance(name, versions=None):
    instances = locate_instances(versions=versions)
    known = [i for i in instances if i.name == name]
    if not known:
        raise DZError, "unknown instance `%s'" % name
    elif len(known) > 1:
        s = ', '.join(["`%s' (%s)" % (i.name, i.version) for i in known])
        raise DZError, "ambiguous instance name `%s' matching %s" % (name, s)
    return known[0]

def locate_zeoinstance(name, versions=None):
    instances = locate_zeoinstances(versions=versions)
    known = [i for i in instances if i.name == name]
    if not known:
        raise DZError, "unknown ZEO instance `%s'" % name
    elif len(known) > 1:
        s = ', '.join(["`%s' (%s)" % (i.name, i.version) for i in known])
        raise DZError, "ambiguous ZEO instance name `%s' matching %s" % (name, s)
    return known[0]

def locate_addons(addonClass=None, arch=None, instance=None, zopeversions=None):
    if instance:
        prefixes = [instance.home]
    elif arch == 'any':
        prefixes = ['/usr/lib/zope']
    elif arch == 'all':
        prefixes = ['/usr/share/zope', '/usr/local/share/zope']
    else:
        prefixes = ['/usr/share/zope', '/usr/lib/zope', '/usr/local/share/zope']
    if addonClass == Product:
        subdirs = [Product.subdir]
    elif addonClass == Extension:
        subdirs = [Extension.subdir]
    else:
        subdirs = [Product.subdir, Extension.subdir]
    locals = []
    if not instance and not is_root():
        locals.append(personal_conf['products'])
    addons = []
    for d in locals + [os.path.join(pf, sd) for pf in prefixes for sd in subdirs]:
        for path in glob.glob(os.path.join(d, '*')):
            if os.path.isfile(path) and path.endswith('.installed'):
                path = open(path, 'r').read()
                addonClass = Product
            elif not os.path.isdir(path):
                continue
            elif not addonClass:
                if os.path.basename(os.path.dirname(path)) == Product.subdir:
                    addonClass = Product
                else:
                    addonClass = Extension
            addon = addonClass(path, False)
            zver = False
            if zopeversions != None:
                for i in addon.zopeversions.split():
                    if i in zopeversions:
                        zver = True
                        break
            else:
                zver = True
            if zver:
                addons.append(addon)
    return addons

# --------------------------------------------------------------------------- #

# print an error message
def usage(stream, msg=None):
    print >>stream, msg
    print >>stream, "use `%s help' for help on actions and arguments" % program
    print >>stream
    sys.exit(1)

# match a string with the list of available actions
def action_matches(action, actions):
    prog = re.compile('[^-]*?-'.join(action.split('-')))
    return [a for a in actions if prog.match(a)]

# parse command line arguments
def parse_options(args):
    shortusage = 'usage: %prog [<option> ...] <action> [<option> ...]'
    parser = OptionParser(usage=shortusage)
    parser.disable_interspersed_args()

    # setup the parsers object
    parser.remove_option('-h')
    parser.add_option('-h', '--help',
                      help='help screen',
                      action='store_true', dest='help')
    parser.add_option('-v', '--verbose',
                      help='verbose mode',
                      action='store_true', dest='verbose')
    parser.add_option('-f', '--force',
                      help='force things, i.e. overwriting, removing stuff',
                      action='store_true', dest='force')
    parser.add_option('-z', '--zope-version',
                      help='limit actions to comma separated list of zope versions',
                      action='store', dest='zversion')
    parser.add_option('-n', '--dry-run',
                      help='do not execute commands, print only (not yet implemented)',
                      action='store', default=False, dest='dryrun')
    parser.add_option('-u', '--user',
                      help='<user>[:<group>] ownership for new and copied files',
                      action='store', dest='user')
    parser.add_option('-c', '--config-file',
                      help='configuration file (default is /etc/dzhandle.conf)',
                      action='store', dest='conffile', default="/etc/dzhandle.conf")
    global_options, args = parser.parse_args()

    # Print the help screen and exit
    if len(args) == 0 or args[0].lower() == 'help' or global_options.help:
        parser.print_help()
        print "\nactions:"
        for n, a in sorted(known_actions.items()):
            if a.list_on_help:
                print "  %-21s %s" % (n, a.help)
        print ""
        sys.exit(1)

    # dry-run option not yet implemented
    if global_options.dryrun:
        print >>sys.stderr, "option --dry-run not yet implemented"
        sys.exit(1)

    # check if the specified zope versions really exist
    known_versions = [z['version'] for z in zope_packages]
    if global_options.zversion:
        zversion = global_options.zversion.split(',')
        unknown_versions = [v for v in zversion if not v in known_versions]
        if unknown_versions:
            usage(sys.stderr, 'unknown zope version(s) %s' % strlist(unknown_versions))
        global_options.zversion = zversion
    else:
        global_options.zversion = known_versions

    # get the uid/gid for zope user/group
    global_options.uid = None
    global_options.gid = None
    if global_options.user:
        if ':' in global_options.user:
            user, group = global_options.user.split(':')
        else:
            user = global_options.user
            group = None
    elif not is_root():
        user = pwd.getpwuid(os.getuid())[0]
        group = grp.getgrgid(os.getgid())[0]
    else:
        user = 'zope'
        group = 'zope'
    try:
        user_info = pwd.getpwnam(user)
        if group:
            group_info = grp.getgrnam(group)
        else:
            group_info = grp.getgrgid(user_info[3])
    except KeyError, msg:
        usage(sys.stderr, msg)
    global_options.uid = user_info[2]
    global_options.gid = group_info[2]

    # check if the specified action really exists
    action_name = args[0]
    del args[0]
    matching_actions = action_matches(action_name, known_actions.keys())
    if len(matching_actions) == 0:
        usage(sys.stderr, "unknown action `%s'" % action_name)
    elif len(matching_actions) > 1:
        usage(sys.stderr,
              "ambiguous action `%s', matching actions: %s"
              % (action_name, strlist(matching_actions)))
    else:
        action_name = matching_actions[0]

    # instantiate an object for the action and parse the remaining arguments
    action = known_actions[action_name]()
    action_options, action_names = action.parse_args(args)

    return global_options, action

# setup logging stuff
def setup_logging(logfile=None, logfilelevel='INFO', loglevel='WARN'):
    env_level = os.environ.get('DZHANDLE', None)
    if env_level != None:
        loglevel = logging.getLevelName(env_level)
        if isinstance(loglevel, str):
            loglevel = logging.INFO
    
    logging.basicConfig(format='%(message)s',
                        level=loglevel,
                        stream=sys.stderr)

    # are we logging to a file?
    if logfile:
        logfilelevel = logging.getLevelName(env_level)
        if isinstance(logfilelevel, str):
            logfilelevel = logging.INFO

        from logging.handlers import TimedRotatingFileHandler
        try:
            fhandler = TimedRotatingFileHandler(logfile, when='D', backupCount=3)
        except IOError, msg:
            print >>sys.stderr, msg
        else:
            fhandler.setLevel(logfilelevel)
            formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
            fhandler.setFormatter(formatter)
            logging.getLogger('').addHandler(fhandler)

# main routine
def main():
    global_options, action = parse_options(sys.argv[1:])

    # read config file
    try:
        config = read_config_file(global_options.conffile)
    except IOError:
        config = {}
    except DZError, msg:
        print >>sys.stderr, msg
        sys.exit(1)

    # read personal configuration file
    uid = os.getuid()
    if not is_root():
        home_dir = pwd.getpwuid(uid)[5]
        conf = os.path.join(home_dir, '.dzhandle.conf')
        personal_conf['instances'] = os.path.join(home_dir, 'zope/instance')
        personal_conf['products'] = os.path.join(home_dir, 'zope/products')
        personal_conf['zeoinstances'] = os.path.join(home_dir, 'zope/zeo')
        if os.path.exists(conf):
            personal_conf.update(read_config_file(conf))
        else:
            write_config_file(conf, personal_conf)

    # setup logging stuff
    setup_logging(config.get('logfile', None),
                  config.get('logfilelevel', logging.INFO),
                  config.get('loglevel', logging.WARN)
                  )
    logging.debug('dzhandle ' + ' '.join(sys.argv[1:]))

    # check the arguments according to the action called
    if action.check_args(global_options):
        sys.exit(1)

    # run the action and exit
    rv = action.run(global_options)
    sys.exit(rv)

# call the main routine
if __name__ == '__main__':
    main()
