#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# Copyright 2008-2016 Glencoe Software, Inc.  All Rights Reserved.
# Use is subject to license terms supplied in LICENSE.txt
# Josh Moore, josh at glencoesoftware.com
"""
OMERO Grid admin controller.

This is a python wrapper around icegridregistry/icegridnode for master
and various other tools needed for administration.
"""
from __future__ import division
from __future__ import print_function

from builtins import str
from future.utils import bytes_to_native_str
from future.utils import isbytes
from past.utils import old_div
from builtins import object
import re
import os
import sys
import platform
import datetime
import time

from glob import glob
from math import ceil
from zipfile import ZipFile

import omero
import omero.config

from omero.grid import RawAccessRequest

from omero.cli import (
    admin_only,
    CLI,
    DiagnosticsControl,
    DirectoryType,
    NonZeroReturnCode,
    ServiceManagerMixin,
    UserGroupControl,
)

from omero.install.config_parser import PropertyParser
from omero.plugins.prefs import \
    WriteableConfigControl, with_config
from omero.install.windows_warning import windows_warning, WINDOWS_WARNING

import portalocker
from omero_ext.path import path
from omero_ext.which import whichall
from omero_ext.argparse import FileType
from omero_version import ice_compatibility

from omero.util._process_defaultxml import _process_xml

try:
    from omero_version import omero_version
    VERSION = omero_version
except ImportError:
    VERSION = "Unknown"  # Usually during testing

try:
    import pywintypes
    import win32service
    import win32evtlogutil
    import win32api
    import win32security
    has_win32 = True
except ImportError:
    has_win32 = False

DEFAULT_WAIT = 300

CHECKUPGRADE_USERAGENT = "test"

HELP = """Administrative tools including starting/stopping OMERO.

Environment variables:
 OMERO_MASTER
 OMERO_NODE

Configuration properties:
 omero.windows.user
 omero.windows.pass
 omero.windows.servicename
 omero.web.application_server.port
 omero.web.server_list

""" + "\n" + "="*50 + "\n"


if platform.system() == 'Windows':
    HELP += ("\n\n%s" % WINDOWS_WARNING)


class AdminControl(DiagnosticsControl,
                   WriteableConfigControl,
                   UserGroupControl,
                   ServiceManagerMixin):

    # Require by omero.cli.ServiceManagerMixin
    SERVICE_MANAGER_KEY = 'server'

    def _complete(self, text, line, begidx, endidx):
        """
        Returns a file after "deploy", "start", or "startasync"
        and otherwise delegates to the BaseControl
        """
        for s in (" deploy ", " start ", " startasync "):
            length = len(s)
            i = line.find(s)
            if i >= 0:
                f = line[i+length:]
                return self._complete_file(f)
        return WriteableConfigControl._complete(
            self, text, line, begidx, endidx)

    def _configure(self, parser):
        sub = parser.sub()
        parser = self._add_diagnostics(parser, sub)
        parser.add_argument(
            "--all-jars", action="store_true",
            help="Show information for all jars")
        self.add_error(
            "NOT_WINDOWS", 123,
            "Not Windows")
        self.add_error(
            "SETUP", 200,
            "Error during service user set up:  (%s) %s")
        self.add_error(
            "RUNNING", 201,
            "%s is already running. Use stop first")
        self.add_error(
            "NO_SERVICE", 202,
            "%s service deleted.")
        self.add_error(
            "BAD_CONFIG", 300,
            "Bad configuration: No IceGrid.Node.Data property")
        self.add_error(
            "WIN_CONFIG", 400, r"""

            %s is not in this directory. Aborting...

            Please see the installation instructions on modifying
            the files for your installation (%s)
            with bin\winconfig.bat

            """)
        self.add_error(
            "NO_WIN32", 666,
            "Could not import win32service and/or win32evtlogutil")
        self.actions = {}

        class Action(object):
            def __init__(this, name, help, wait=False):
                this.parser = sub.add_parser(name, help=help,
                                             description=help)
                this.parser.set_defaults(func=getattr(self, name))
                self.actions[name] = this.parser
                if wait:
                    this.parser.add_argument(
                        "--wait", type=float, default=DEFAULT_WAIT,
                        help="Seconds to wait for operation")

        Action(
            "start",
            """Start icegridnode daemon and waits for required components to \
come up, i.e. status == 0

If the first argument can be found as a file, it will be deployed as the
application descriptor rather than etc/grid/default.xml. All other arguments
will be used as targets to enable optional sections of the descriptor.

On Windows, two arguments (-u and -w) specify the Windows service Log On As
user credentials. If not specified, omero.windows.user and omero.windows.pass
will be used.""",
            wait=True)

        Action("startasync", "The same as start but returns immediately",)

        Action("restart", "stop && start", wait=True)

        Action(
            "restartasync", """The same as restart but returns as soon as \
starting has begun.""",
            wait=True)

        Action("status", """Status of server

Returns with 0 status if a node ping is successful and if some SessionManager
returns an OMERO-specific exception on a bad login. This can be used in shell
scripts, e.g.:

    $ omero admin status && echo "server started"
            """)

        Action(
            "stop",
            """Initiates node shutdown and waits for status to return a \
non-0 value""",
            wait=True)

        Action("stopasync", "The same as stop but returns immediately")

        Action(
            "deploy",
            """Deploy the given deployment descriptor. See etc/grid/*.xml

If the first argument is not a file path, etc/grid/default.xml will be
deployed by default. Same functionality as start, but requires that the node
already be running. This may automatically restart some server components.""")

        Action(
            "ice", "Drop user into icegridadmin console or execute arguments")

        fixpyramids = Action(
            "fixpyramids",
            "Remove empty pyramid pixels files (admins only)").parser

        removepyramids = Action(
            "removepyramids",
            """Remove pyramid pixels files (admins only) according to endianness.
By default pyramids with big and little-endianness will be deleted.
To delete pyramids with little-endianness equals to true use --endian=little.

Examples:
  omero admin removepyramids --dry-run
  omero admin removepyramids --dry-run --endian=little
  omero admin removepyramids --dry-run --imported-after YYYY-mm-dd
            """).parser
        # See cleanse options below

        email = Action(
            "email",
            """Send administrative emails to users.

Administrators can contact OMERO users and groups of
users based on configured email settings. A subject
and some text are required. If no text is passed on
the command-line or if "-" is passed, then text will
be read from the standard input.

Examples:

  # Send the contents of a file to everyone
  # except inactive users.
  omero admin email --everyone Subject < some_file.text

  # Include inactive users in the email
  omero admin email --everyone --inactive ...

  # Contact a single group
  omero admin email --group-name system \\
                        Subject short message

  # Contact a list of users
  omero admin email --user-id 10 \\
                        --user-name ralph \\
                        Subject ...

            """).parser
        email.add_argument(
            "subject",
            help="Required subject for the mail")
        email.add_argument(
            "text", nargs="*",
            help=("All further arguments are combined "
                  "to form the body. stdin if none or '-' "
                  "is given."))
        email.add_argument(
            "--everyone", action="store_true",
            help=("Contact everyone in the system regardless "
                  "of other arguments."))
        email.add_argument(
            "--inactive", action="store_true",
            help="Do not filter inactive users.")
        self._add_wait(email)
        self.add_user_and_group_arguments(email,
                                          action="append",
                                          exclusive=False)

        Action(
            "rewrite",
            """Regenerate the IceGrid configuration files

Regenerates the IceGrid configuration files from the template files and the
current configuration properties. Recalculates the JVM configuration settings
and replaces the memory settings as well as the port and host properties under
the corresponding application descriptors.
            """)

        Action(
            "jvmcfg",
            "Display JVM configuration settings based on the current system")

        Action(
            "waitup",
            "Used by start after calling startasync to wait on status==0",
            wait=True)

        Action(
            "waitdown",
            "Used by stop after calling stopasync to wait on status!=0",
            wait=True)

        reindex = Action(
            "reindex",
            """Re-index the Lucene index

Command-line tool for re-indexing the database. This command must be run on the
machine where the FullText directory is located. In most cases, you will want
to disable the background indexer before running most of these commands.

See https://docs.openmicroscopy.org/latest/omero/sysadmins/search.html
for more information.

Examples:

  # 1. Reset the 'last indexed' counter. Defaults to 0
  omero admin reindex --reset 0

  # 2. Delete the contents of a corrupt FullText directory
  omero admin reindex --wipe

  # 3. Run indexer in the foreground. Disable the background first
  omero admin reindex --foreground
  # Foreground indexing is NOT currently working.

Other commands (usually unnecessary):

  # Index all objects in the database.
  omero admin reindex --full

  # Index one specific class
  omero admin reindex --class ome.model.core.Image

  # Passing arguments to Java
  JAVA_OPTS="-Dlogback.configurationFile=stderr.xml" \\
  omero admin reindex --foreground

  JAVA_OPTS="-Xdebug -Xrunjdwp:server=y,transport=\
dt_socket,address=8787,suspend=y" \\
  omero admin reindex --foreground

""").parser

        reindex.add_argument(
            "--jdwp", action="store_true",
            help="Activate remote debugging")
        reindex.add_argument(
            "--mem", default="1024m",
            help="Heap size to use")
        reindex.add_argument(
            "--batch", default="500",
            help="Number of items to index before reporting status")
        reindex.add_argument(
            "--merge-factor", "--merge_factor", default="100",
            help=("Higher means merge less frequently. "
                  "Faster but needs more RAM"))
        reindex.add_argument(
            "--ram-buffer-size", "--ram_buffer_size", default="1000",
            help=("Number of MBs to use for the indexing. "
                  "Higher is faster."))
        reindex.add_argument(
            "--lock-factory", "--lock_factory", default="native",
            help=("Choose Lucene lock factory by class or "
                  "'native', 'simple', 'none'"))

        group = reindex.add_mutually_exclusive_group()
        group.add_argument(
            "--prepare", action="store_true",
            help="Disables the background indexer in preparation for indexing")
        group.add_argument(
            "--full", action="store_true",
            help="Reindexes all non-excluded tables sequentially")
        group.add_argument(
            "--events", action="store_true",
            help="Reindexes all non-excluded event logs chronologically")
        group.add_argument(
            "--reset", default=None,
            help="Reset the index counter")
        group.add_argument(
            "--dryrun", action="store_true",
            help=("Run through all events, incrementing the counter. "
                  "NO INDEXING OCCURS"))
        group.add_argument(
            "--foreground", action="store_true",
            help=("Run indexer in the foreground"))
        group.add_argument(
            "--class", nargs="+",
            help="Reindexes the given classes sequentially")
        group.add_argument(
            "--wipe", action="store_true",
            help="Delete the existing index files")
        group.add_argument(
            "--finish", action="store_true",
            help="Re-enables the background indexer after for indexing")

        sessionlist = Action(
            "sessionlist", "List currently running sessions (deprecated)") \
            .parser
        sessionlist.add_login_arguments()

        cleanse = Action("cleanse", """Remove binary data files from OMERO  (admins only)

Deleting an object from OMERO currently may not remove all the binary data.
Use this command either manually or in a cron job periodically to remove
Pixels, empty directories, and other data.

This is done by checking that for all the files in the given directory, a
matching entry exists on the server. THE /OMERO DIRECTORY MUST MATCH THE
DATABASE YOU ARE RUNNING AGAINST.

This command must be run on the machine where, for example, /OMERO/ is
located.

Examples:
  omero admin cleanse --dry-run /OMERO      # Lists files that will be \
deleted
  omero admin cleanse /OMERO                # Actually delete them.
  omero admin cleanse /volumes/data/OMERO   # Delete from a standard \
location.

""").parser

        for x in (cleanse, fixpyramids):
            x.add_argument(
                "--dry-run", action="store_true",
                help="Print out which files would be deleted")
            x.add_argument(
                "data_dir", type=DirectoryType(),
                help="omero.data.dir directory value e.g. /OMERO")
            x.add_login_arguments()

        removepyramids.add_argument(
            "--dry-run", action="store_true",
            help="Print out which files would be deleted")
        removepyramids.add_argument(
            "--endian", choices=("little", "big", "both"), default="both",
            help="Delete pyramid with given endianness. "
            "If not specified, all will be removed.")
        removepyramids.add_argument(
            "--imported-after", metavar="DATE",
            help="Delete pyramid imported after a given date. "
            "Expected format YYYY-mm-dd")
        removepyramids.add_argument(
            "--limit", metavar="MAX_NUMBER",
            help="Set the limit of pyramids to remove in one call. "
            "Values greater than 500 (default) are not supported")
        removepyramids.add_login_arguments()
        self._add_wait(removepyramids)

        Action("checkwindows", "Run simple check of the local installation "
               "(Windows-only)")
        Action("checkice", "Run simple check of the Ice installation")

        Action("events", "Print event log (Windows-only)")

        Action(
            "checkupgrade", "Check whether a server upgrade is available")

        log = Action("log", "Add a custom log message to "
                            "the server log").parser
        log.add_argument(
            "--level",
            help="The log level: trace, debug, info, warn or error "
                 "(default: info)", default="info")
        log.add_argument(
            "repo",
            help="The repo uuid (e.g. ScriptRepo)")
        log.add_argument(
            "message",
            help="The log message to add")
        log.add_login_arguments()

        self.actions["ice"].add_argument(
            "argument", nargs="*",
            help="""Arguments joined together to make an Ice command. If not \
present, the user will enter a console""")

        self.actions["status"].add_argument(
            "node", nargs="?", default="master")
        self.actions["status"].add_argument(
            "--nodeonly", action="store_true",
            help="If set, then only tests if the icegridnode is running")

        for name in ("start", "startasync", "restart", "restartasync", "stop",
                     "stopasync"):
            self.actions[name].add_argument(
                "--force-rewrite", action="store_true",
                help="Force the configuration to be rewritten before checking"
                " the server status")

        for name in ("start", "restart"):
            self.actions[name].add_argument(
                "--foreground", action="store_true",
                help="Start server in foreground mode (no daemon/service)")

        for name in ("start", "startasync", "restart", "restartasync"):
            self.actions[name].add_argument(
                "-u", "--user",
                help="Windows Service Log On As user name.")
            self.actions[name].add_argument(
                "-w", "--password", metavar="PW",
                help="Windows Service Log On As user password.")

        for name in ("start", "startasync", "deploy", "restart",
                     "restartasync"):
            self.actions[name].add_argument(
                "file", nargs="?",
                help="Application descriptor. If not provided, a default"
                " will be used")
            self.actions[name].add_argument(
                "targets", nargs="*",
                help="Targets within the application descriptor which "
                " should  be activated. Common values are: \"debug\", "
                "\"trace\" ")

        # DISABLED = """ see: http://www.zeroc.com/forums/bug-reports/\
        # 4237-sporadic-freeze-errors-concurrent-icegridnode-access.html
        #   restart [filename] [targets]      : Calls stop followed by start \
        #   args
        #   restartasync [filename] [targets] : Calls stop followed by \
        #   startasync args
        # """

    #
    # Windows utility methods
    #
    if has_win32:
        def _get_service_name(unused, config):
            try:
                return config.as_map()["omero.windows.servicename"]
            except KeyError:
                return 'OMERO'

        def _query_service(unused, svc_name):
            hscm = win32service.OpenSCManager(
                None, None, win32service.SC_MANAGER_ALL_ACCESS)
            try:
                try:
                    hs = win32service.OpenService(
                        hscm, svc_name, win32service.SERVICE_ALL_ACCESS)
                except Exception:
                    return "DOESNOTEXIST"
                try:
                    q = win32service.QueryServiceStatus(hs)
                    type, state, ctrl, err, svcerr, svccp, svcwh = q
                    if state == win32service.SERVICE_STOPPED:
                        return "STOPPED"
                    else:
                        return "unknown"
                finally:
                    win32service.CloseServiceHandle(hs)
            finally:
                win32service.CloseServiceHandle(hscm)

        def _stop_service(self, svc_name):
            hscm = win32service.OpenSCManager(
                None, None, win32service.SC_MANAGER_ALL_ACCESS)
            try:
                hs = win32service.OpenService(
                    hscm, svc_name, win32service.SC_MANAGER_ALL_ACCESS)
                win32service.ControlService(
                    hs, win32service.SERVICE_CONTROL_STOP)
                win32service.DeleteService(hs)
                self.ctx.out("%s service deleted." % svc_name)
            finally:
                win32service.CloseServiceHandle(hs)
                win32service.CloseServiceHandle(hscm)

        def _start_service(self, config, descript, svc_name, pasw, user):
            output = self._query_service(svc_name)
            # Now check if the server exists
            if 0 <= output.find("DOESNOTEXIST"):
                binpath = """icegridnode.exe "%s" --deploy "%s" --service\
                    %s""" % (self._icecfg(), descript, svc_name)

                # By default: "NT Authority\Local System"
                if not user:
                    try:
                        user = config.as_map()["omero.windows.user"]
                    except KeyError:
                        user = None
                if user is not None and len(user) > 0:
                    if "\\" not in user:
                        computername = win32api.GetComputerName()
                        user = "\\".join([computername, user])
                    try:
                        # See #9967, code based on http://mail.python.org/\
                        # pipermail/python-win32/2010-October/010791.html
                        self.ctx.out("Granting SeServiceLogonRight to service"
                                     " user \"%s\"" % user)
                        policy_handle = win32security.LsaOpenPolicy(
                            None, win32security.POLICY_ALL_ACCESS)
                        sid_obj, domain, tmp = \
                            win32security.LookupAccountName(None, user)
                        win32security.LsaAddAccountRights(
                            policy_handle, sid_obj, ('SeServiceLogonRight',))
                        win32security.LsaClose(policy_handle)
                    except pywintypes.error as details:
                        self.raise_error("SETUP", details[0], details[2])
                    if not pasw:
                        try:
                            pasw = config.as_map()["omero.windows.pass"]
                        except KeyError:
                            pasw = self._ask_for_password(
                                " for service user \"%s\"" % user)
                else:
                    pasw = None

                hscm = win32service.OpenSCManager(
                    None, None, win32service.SC_MANAGER_ALL_ACCESS)
                try:
                    self.ctx.out("Installing %s Windows service." % svc_name)
                    hs = win32service.CreateService(
                        hscm, svc_name, svc_name,
                        win32service.SERVICE_ALL_ACCESS,
                        win32service.SERVICE_WIN32_OWN_PROCESS,
                        win32service.SERVICE_AUTO_START,
                        win32service.SERVICE_ERROR_NORMAL, binpath, None, 0,
                        None, user, pasw)
                    self.ctx.out("Successfully installed %s Windows service."
                                 % svc_name)
                    win32service.CloseServiceHandle(hs)
                finally:
                    win32service.CloseServiceHandle(hscm)

            # Then check if the server is already running
            if 0 <= output.find("RUNNING"):
                self.raise_error("RUNNING", svc_name)

            # Finally, try to start the service - delete if startup fails
            hscm = win32service.OpenSCManager(
                None, None, win32service.SC_MANAGER_ALL_ACCESS)
            try:
                try:
                    hs = win32service.OpenService(
                        hscm, svc_name, win32service.SC_MANAGER_ALL_ACCESS)
                    win32service.StartService(hs, None)
                    self.ctx.out("Starting %s Windows service." % svc_name)
                except pywintypes.error as details:
                    self.ctx.out("%s service startup failed: (%s) %s"
                                 % (svc_name, details[0], details[2]))
                    win32service.DeleteService(hs)
                    self.raise_error("NO_SERVICE", svc_name)
            finally:
                win32service.CloseServiceHandle(hs)
                win32service.CloseServiceHandle(hscm)

        def events(self, svc_name):
            def DumpRecord(record):
                if str(record.SourceName) == svc_name:
                    self.ctx.out("Time: %s" % record.TimeWritten)
                    self.ctx.out("Rec:  %s" % record.RecordNumber)
                    for si in record.StringInserts:
                        self.ctx.out(si)
                    self.ctx.out("="*20)
            win32evtlogutil.FeedEventLogRecords(DumpRecord)

    else:

        def events(self, svc_name):
            self.raise_error("NO_WIN32")

        def _query_service(self, svc_name):
            self.raise_error("NO_WIN32")

        def _start_service(self, config, descript, svc_name, pasw, user):
            self.raise_error("NO_WIN32")

        def _stop_service(self, svc_name):
            self.raise_error("NO_WIN32")

    #
    # End Windows Methods
    #

    def _node(self, omero_node=None):
        """
        Overrides the regular node() logic to return the value of
        OMERO_MASTER or "master"
        """
        if omero_node is not None:
            os.environ["OMERO_MASTER"] = omero_node

        if "OMERO_MASTER" in os.environ:
            return os.environ["OMERO_MASTER"]
        else:
            return "master"

    def _get_etc_dir(self):
        """Return path to directory containing configuration files"""
        return old_div(self.ctx.dir, "etc")

    def _get_grid_dir(self):
        """Return path to directory containing Gridconfiguration files"""
        return old_div(self._get_etc_dir(), "grid")

    def _get_templates_dir(self):
        """Return path to directory containing templates"""
        return self.ctx.dir / "etc" / "templates"

    def _cmd(self, *command_arguments):
        """
        Used to generate an icegridadmin command line argument list
        """
        command = ["icegridadmin", self._intcfg()]
        command.extend(command_arguments)
        return command

    def _descript(self, args):
        if args.file is not None:
            # Relative to cwd
            descript = path(args.file).abspath()
            if not descript.exists():
                self.ctx.dbg("No such file: %s -- Using as target" % descript)
                args.targets.insert(0, args.file)
                descript = None
        else:
            descript = None

        if descript is None:
            __d__ = "default.xml"
            if self._isWindows():
                __d__ = "windefault.xml"
            descript = old_div(self._get_grid_dir(), __d__)
            self.ctx.err("No descriptor given. Using %s"
                         % os.path.sep.join(["etc", "grid", __d__]))
        return descript

    def checkwindows(self, args):
        r"""
        Checks that the templates file as defined in etc\Windows.cfg
        can be found.
        """
        self.check_access(os.R_OK)
        if not self._isWindows():
            self.raise_error("NOT_WINDOWS")

        import Ice
        key = "IceGrid.Node.Data"
        properties = Ice.createProperties([self._icecfg()])
        nodedata = properties.getProperty(key)
        if not nodedata:
            self.raise_error("BAD_CONFIG")
        nodepath = path(nodedata)
        pp = nodepath.parpath(self.ctx.dir)
        if pp:
            return
        if nodepath == r"c:\omero_dist\var\master":
            self.ctx.out("Found default value: %s" % nodepath)
            self.ctx.out("Attempting to correct...")
            from omero.install.win_set_path import win_set_path
            count = win_set_path(dir=self.ctx.dir)
            if count:
                return
        self.raise_error("WIN_CONFIG", nodedata, self.ctx.dir)

    ##############################################
    #
    # Commands
    #

    @with_config
    def startasync(self, args, config):
        """
        First checks for a valid installation, then checks the grid,
        then registers the action: "node HOST start"
        """
        self.requires_service_manager(config)
        self.check_access(mask=os.R_OK, config=config)
        self.checkice()
        self.check_node(args)

        if not self.check_internal_cfg() or args.force_rewrite:
            self.rewrite(args, config, force=True)

        if 0 == self.status(args, node_only=True, can_force_rewrite=True):
            self.ctx.die(876, "Server already running")

        if not args.force_rewrite:
            self.rewrite(args, config)

        if self._isWindows():
            self.checkwindows(args)
        self.check_lock(config)

        try:
            config['omero.db.poolsize']
        except KeyError:
            self.ctx.out(
                "WARNING: Your server has not been configured for production "
                "use.\nSee https://docs.openmicroscopy.org/omero/latest/"
                "sysadmins/server-performance.html?highlight=poolsize\n"
                "for more information.")

        # Warn if deprecated TLS 1.0 and 1.1 protocols are allowed
        # See https://datatracker.ietf.org/doc/html/rfc8996
        # Both protocols are included in the default value of IceSSL.Protocols
        # https://doc.zeroc.com/ice/3.6/property-reference/icessl#id-.IceSSL.*v3.6-IceSSL.Protocols
        DEPRECATED_TLS_MESSAGE = (
            "Your server is configured to allow a deprecated TLS protocol."
            "\n\nPlease refer to https://omero.readthedocs.io/en/stable/"
            "sysadmins/server-upgrade.html#server-certificates for "
            "instructions on how to upgrade your configuration.")
        try:
            ssl_protocols = config["omero.glacier2.IceSSL.Protocols"]
            if ("TLS1_0" in ssl_protocols or "TLS1_1" in ssl_protocols):
                self.ctx.out("WARNING: " + DEPRECATED_TLS_MESSAGE)
        except KeyError:
            self.ctx.out("WARNING: " + DEPRECATED_TLS_MESSAGE)

        self._initDir()
        # Do a check to see if we've started before.
        self._regdata()
        self.check([])

        command = None
        descript = self._descript(args)
        foreground = hasattr(args, "foreground") and args.foreground

        if self._isWindows():
            if foreground:
                command = """icegridnode.exe "%s" --deploy "%s" %s\
                """ % (self._icecfg(), descript, args.targets)
            else:
                user = args.user
                pasw = args.password
                svc_name = "%s.%s" % (
                    self._get_service_name(config), args.node)
                self._start_service(config, descript, svc_name, pasw, user)
        else:
            if foreground:
                command = ["icegridnode", "--nochdir", self._icecfg(),
                           "--deploy", str(descript)] + args.targets
            else:
                command = ["icegridnode", "--daemon", "--pidfile",
                           str(self._pid()), "--nochdir", self._icecfg(),
                           "--deploy", str(descript)] + args.targets

        if command is not None:
            self.ctx.rv = self.ctx.call(command)

    @with_config
    def start(self, args, config):
        self.startasync(args, config)
        try:
            self.waitup(args)
        except NonZeroReturnCode as nzrc:
            # stop() may itself throw,
            # if it does not, then we rethrow
            # the original
            self.ctx.err('Calling "stop" on remaining components')
            self.stop(args, config)
            raise nzrc

    @with_config
    def deploy(self, args, config):
        self.rewrite(args, config)
        self.check_access()
        self.checkice()
        descript = self._descript(args)

        # TODO : Doesn't properly handle whitespace
        # Though users can workaround with something like:
        # omero admin deploy etc/grid/a\\\\ b.xml
        command = ["icegridadmin", self._intcfg(), "-e",
                   " ".join(["application", "update", str(descript)] +
                            args.targets)]
        self.ctx.call(command)

    def check_internal_cfg(self):
        internal_cfg = self._cfglist()[0]
        return os.path.exists(internal_cfg)

    @windows_warning
    def status(self, args, node_only=False, can_force_rewrite=False):
        self.check_node(args)
        if not self.check_internal_cfg():
            error_msg = 'Missing internal configuration.'
            if can_force_rewrite:
                error_msg += ' Pass --force-rewrite to the command.'
            else:
                error_msg += ' Run `omero admin rewrite`.'
            self.ctx.die(574, error_msg)
        command = self._cmd("-e", "node ping %s" % self._node())
        self.ctx.rv = self.ctx.popen(command).wait()  # popen

        # node_only implies that "up" need not check for all
        # of blitz to be accessible but just that if the node
        # is running.
        if not node_only:
            node_only = getattr(args, "nodeonly", False)

        if self.ctx.rv == 0 and not node_only:
            try:
                import Ice
                ic = Ice.initialize([self._intcfg()])
                try:
                    sm = self.session_manager(ic)
                    try:
                        sm.create("####### STATUS CHECK ########", None)
                        # Not adding "omero.client.uuid"
                    except omero.WrappedCreateSessionException:
                        # Only the server will throw one of these
                        self.ctx.dbg("Server reachable")
                        self.ctx.rv = 0
                finally:
                    ic.destroy()
            except Exception as exc:
                self.ctx.rv = 1
                self.ctx.dbg("Server not reachable: "+str(exc))

        return self.ctx.rv

    def wait_for_icedb(self, args, config):
        """
        Since the stop and start are separately protected by
        the lock on config.xml, we need to wait for some time
        to hopefully let the icegridnode process release the
        file locks.
        """
        self.ctx.sleep(1)  # put in sleep to try to prevent "db locked" (#7325)

    @with_config
    def restart(self, args, config):
        if not self.stop(args, config):
            self.ctx.die(54, "Failed to shutdown")
        self.wait_for_icedb(args, config)
        self.start(args, config)

    @with_config
    def restartasync(self, args, config):
        if not self.stop(args, config):
            self.ctx.die(54, "Failed to shutdown")
        self.wait_for_icedb(args, config)
        self.startasync(args, config)

    def waitup(self, args):
        """
        Loops 30 times with 10 second pauses waiting for status()
        to return 0. If it does not, then ctx.die() is called.
        """
        self.check_access(os.R_OK)
        self.ctx.out("Waiting on startup. Use CTRL-C to exit")
        count, loop_secs, time_msg = self.loops_and_wait(args)
        while True:
            count = count - 1
            if count == 0:
                self.ctx.die(43, "\nFailed to startup some components after"
                             " %s" % time_msg)
            elif 0 == self.status(args, node_only=False):
                break
            else:
                self.ctx.out(".", newline=False)
                self.ctx.sleep(loop_secs)

    def waitdown(self, args):
        """
        Returns true if the server went down
        """
        self.check_access(os.R_OK)
        self.ctx.out("Waiting on shutdown. Use CTRL-C to exit")
        count, loop_secs, time_msg = self.loops_and_wait(args)
        while True:
            count = count - 1
            if count == 0:
                self.ctx.die(44, "\nFailed to shutdown some components after"
                             " %s" % time_msg)
                return False
            elif 0 != self.status(args, node_only=True):
                break
            else:
                self.ctx.out(".", newline=False)
                self.ctx.sleep(loop_secs)
        self.ctx.rv = 0
        return True

    def loops_and_wait(self, args):
        """
        If present, get the wait time from the args argument
        and calculate the number of loops and the wait time
        needed. If not present in args, use a default value.
        """

        if not hasattr(args, "wait"):
            # This might happen if a new command starts using
            # waitup/waitdown without setting wait=True for
            # Action()
            args.wait = DEFAULT_WAIT

        total_secs = args.wait
        loop_secs = old_div(total_secs, 30.0)
        return 30, loop_secs, "%s seconds" % total_secs

    @with_config
    def stopasync(self, args, config):
        """
        Returns true if the server was already stopped
        """
        self.requires_service_manager(config)
        self.check_node(args)
        if args.force_rewrite:
            self.rewrite(args, config, force=True)
        if 0 != self.status(args, node_only=True, can_force_rewrite=True):
            self.ctx.err("Server not running")
            return True
        elif self._isWindows():
            svc_name = "%s.%s" % (self._get_service_name(config), args.node)
            output = self._query_service(svc_name)
            if 0 <= output.find("DOESNOTEXIST"):
                self.ctx.die(203, "%s does not exist. Use 'start' first."
                             % svc_name)
            self._stop_service(svc_name)
        else:
            command = self._cmd("-e", "node shutdown %s" % self._node())
            try:
                self.ctx.call(command)
            except NonZeroReturnCode as nzrc:
                self.ctx.rv = nzrc.rv
                self.ctx.out("Was the server already stopped?")

    @with_config
    def stop(self, args, config):
        if not self.stopasync(args, config):
            return self.waitdown(args)
        return True

    def check(self, args):
        # print "Check db. Have a way to load the db control"
        pass

    def ice(self, args):
        self.check_access()
        command = self._cmd()
        if len(args.argument) > 0:
            command.extend(["-e", " ".join(args.argument)])
            return self.ctx.call(command)
        else:
            self.ctx.call(command)

    @admin_only(full_admin=True)
    @with_config
    def fixpyramids(self, args, config):
        self.check_access()
        from omero.util.cleanse import fixpyramids
        client = self.ctx.conn(args)
        client.getSessionId()
        fixpyramids(data_dir=args.data_dir, dry_run=args.dry_run,
                    admin_service=client.sf.getAdminService(),
                    query_service=client.sf.getQueryService(),
                    config_service=client.sf.getConfigService())

    @admin_only(full_admin=True)
    def removepyramids(self, args):
        from omero.util.cleanse import removepyramids
        client = self.ctx.conn(args)
        client.getSessionId()
        if args.wait is not None and int(args.wait) > 0:
            wait = args.wait
        else:
            wait = 25
        if args.limit is not None and int(args.limit) > 0:
            limit = args.limit
        else:
            limit = 500
        if args.endian == "both":
            little = None
        elif args.endian == "little":
            little = omero.rtypes.rbool(True)
        else:
            little = omero.rtypes.rbool(False)
        # check time
        value = None
        if args.imported_after is not None:
            date = time.strptime(args.imported_after, "%Y-%m-%d")
            value = omero.rtypes.rtime(time.mktime(date)*1000)
        removepyramids(client=client,
                       little_endian=little,
                       dry_run=args.dry_run,
                       imported_after=value,
                       limit=limit, wait=wait)

    @with_config
    def jvmcfg(self, args, config):
        """Display JVM settings from the current configuration"""

        from xml.etree.ElementTree import XML
        from omero.install.jvmcfg import adjust_settings

        # JVM configuration regeneration
        templates = self._get_templates_dir() / "grid" / "templates.xml"
        template_xml = XML(templates.text())
        try:
            rv = adjust_settings(config, template_xml)
        except Exception as e:
            self.ctx.die(11, 'Cannot adjust memory settings in %s.\n%s'
                         % (templates, e))

        self.ctx.out("JVM Settings:")
        self.ctx.out("============")
        for k, v in sorted(rv.items()):
            settings = v.pop(0)
            sb = " ".join([str(x) for x in v])
            if str(settings) != "Settings()":
                sb += " # %s" % settings
            self.ctx.out("%s=%s" % (k, sb))

    def _get_omero_properties(self):
        omero_props_file = old_div(self._get_etc_dir(), "omero.properties")
        pp = PropertyParser()
        omero_props = dict(
            (p.key, p.val) for p in pp.parse_file(omero_props_file))
        if sys.platform == "darwin":
            # Override xxx.yyy properties with value of _xxx.yyy.darwin
            for key in omero_props:
                if key.startswith('_') and key.endswith('.darwin'):
                    omero_props[key[1:-7]] = omero_props[key]
        return omero_props

    def _glacier2_icessl_xml(self, config_props):
        # Convert omero.glacier2.IceSSL.* properties to IceSSL.*
        items = list(config_props.items())
        glacier2_icessl = dict((k[15:], v) for (k, v) in items
                               if k.startswith('omero.glacier2.IceSSL.'))
        return ['<property name="%s" value="%s"/>' % kv
                for kv in list(glacier2_icessl.items())]

    def get_omero_server_version(self):
        """Returns the value of omero.version stored in omero.properties"""
        omero_props_file = os.path.join(self._get_etc_dir(), "omero.properties")
        with open(omero_props_file, 'r') as f:
            for line in f.readlines():
                if line.startswith("omero.version"):
                    idx = line.index("=")
                    return line[idx + 1:].rstrip()
        return "unknown"

    @with_config
    def rewrite(self, args, config, force=False):
        """
        Regenerate all the template files under the configuration directory
        """

        from xml.etree.ElementTree import XML
        from omero.install.jvmcfg import adjust_settings

        if self.check_internal_cfg() and not force:
            if 0 == self.status(args, node_only=True):
                self.ctx.die(
                    100, "Can't regenerate templates the server is running!")
            # Reset return value
            self.ctx.rv = 0

        # JVM configuration regeneration
        templates = self._get_templates_dir()/"grid"/"templates.xml"

        # Get some defaults from omero.properties
        config_props = self._get_omero_properties()

        generated = old_div(self._get_grid_dir(), "templates.xml")
        if generated.exists():
            generated.remove()
        config2 = omero.config.ConfigXml(str(generated))

        config_props.update(config.as_map())
        template_xml_text = templates.text().replace(
            '@omero.glacier2.icessl@',
            '\n'.join(self._glacier2_icessl_xml(config_props)))
        template_xml = XML(template_xml_text)

        try:
            rv = adjust_settings(config, template_xml)
        except Exception as e:
            self.ctx.die(11, 'Cannot adjust memory settings in %s.\n%s'
                         % (templates, e))

        def clear_tail(elem):
            elem.tail = ""
            if elem.text is not None and not elem.text.strip():
                elem.text = ""
            for child in list(elem):
                clear_tail(child)

        clear_tail(template_xml)
        config2.write_element(template_xml)
        config2.XML = None  # Prevent re-saving
        config2.close()
        config.save()

        # Define substitution dictionary for template files
        config = config.as_map()
        substitutions = {
            '@omero.ports.prefix@': config.get('omero.ports.prefix', ''),
            '@omero.ports.ssl@': config.get('omero.ports.ssl', '4064'),
            '@omero.ports.tcp@': config.get('omero.ports.tcp', '4063'),
            '@omero.ports.wss@': config.get('omero.ports.wss', '4066'),
            '@omero.ports.ws@': config.get('omero.ports.ws', '4065'),
            '@omero.ports.registry@': config.get(
                'omero.ports.registry', '4061'),
            '@omero.master.host@': config.get('omero.master.host', config.get(
                'Ice.Default.Host', '127.0.0.1'))
            }

        client_transports = config.get('omero.client.icetransports', 'ssl,tcp')
        client_endpoints = [
            '{tp} -p {prefix}{port}'.format(
                tp=clienttp,
                prefix=substitutions['@omero.ports.prefix@'],
                port=substitutions['@omero.ports.{tp}@'.format(tp=clienttp)],
            )
            for clienttp in client_transports.split(',')]
        substitutions['@omero.client.endpoints@'] = ':'.join(client_endpoints)

        node_descriptors = config.get('omero.server.nodedescriptors', '')

        def copy_template(input_file, output_dir, post_process=None):
            """Replace templates"""
            with open(input_file) as template:
                data = template.read()
            output_file = path(old_div(output_dir,
                               os.path.basename(input_file)))
            if output_file.exists():
                output_file.remove()
            with open(output_file, 'w') as f:
                for key, value in substitutions.items():
                    data = re.sub(key, value, data)
                if post_process:
                    data = post_process(data)
                f.write(data)

        # Regenerate various configuration files from templates
        for cfg_file in glob(old_div(self._get_templates_dir(), "*.cfg")):
            copy_template(cfg_file, self._get_etc_dir())
        for xml_file in glob(
                self._get_templates_dir() / "grid" / "*default.xml"):
            copy_template(
                xml_file, old_div(self._get_etc_dir(), "grid"),
                lambda xml: _process_xml(xml, node_descriptors))
        ice_config = old_div(self._get_templates_dir(), "ice.config")
        substitutions['@omero.master.host@'] = config.get(
            'omero.master.host', config.get('Ice.Default.Host', 'localhost'))
        copy_template(ice_config, self._get_etc_dir())

        return rv

    @windows_warning
    @with_config
    def diagnostics(self, args, config):

        self._diagnostics_banner("admin")

        from xml.etree.ElementTree import XML
        from omero.install.jvmcfg import read_settings

        self.check_access(os.R_OK)
        templates = old_div(self._get_grid_dir(), "templates.xml")
        if templates.exists():
            template_xml = XML(templates.text())
            try:
                memory = read_settings(template_xml)
            except Exception as e:
                self.ctx.die(11, 'Cannot read memory settings in %s.\n%s'
                             % (templates, e))
        else:
            memory = None
        omero_data_dir = self._get_data_dir(config)

        from omero.util.temp_files import gettempdir
        # gettempdir returns ~/omero/tmp/omero_%NAME/%PROCESS
        # To find something more generally useful for calculating
        # size, we go up two directories
        omero_temp_dir = gettempdir()
        omero_temp_dir = os.path.abspath(
            os.path.join(omero_temp_dir, os.path.pardir, os.path.pardir))

        def version(cmd):
            """
            Returns a true response only
            if a valid version was found.
            """
            self._item("Commands", "%s" % " ".join(cmd))
            try:
                p = self.ctx.popen(cmd)
            except OSError:
                self.ctx.err("not found")
                return False

            p.wait()
            io = list(map(bytes_to_native_str, p.communicate()))
            try:
                v = io[0].split()
                v.extend(io[1].split())
                v = "".join(v)
                m = re.match(r"^\D*(\d[.\d]+\d)\D?.*$", v)
                v = "%-10s" % m.group(1)
                self.ctx.out(v, False)
                try:
                    where = whichall(cmd[0])
                    sz = len(where)
                    if sz == 0:
                        where = "unknown"
                    else:
                        where = where[0]
                        if sz > 1:
                            where += " -- %s others" % sz
                except Exception:
                    where = "unknown"
                self.ctx.out("(%s)" % where)
                return True
            except Exception as e:
                self.ctx.err("error:%s" % e)
                return False

        import logging
        logging.basicConfig()
        check = self.run_upgrade_check(config, "diagnostics")
        if check.isUpgradeNeeded():
            self.ctx.out("")

        version(["java",         "-version"])
        version(["python",       "-V"])
        version(["icegridnode",  "--version"])
        iga = version(["icegridadmin", "--version"])
        version(["psql",         "--version"])
        version(["openssl",      "version"])

        self.ctx.out("")
        self._item("Component", "OMERO.py")
        self.ctx.out(VERSION)
        self._item("Component", "OMERO.server")
        self.ctx.out(self.get_omero_server_version())

        def get_ports(input):
            router_lines = [line for line in input.split("\n")
                            if line.find("ROUTER") >= 0]

            ssl_port = None
            tcp_port = None
            for line in router_lines:
                if not ssl_port and line.find("ROUTERPORT") >= 0:
                    m = re.match(r".*?(\d+).*?$", line)
                    if m:
                        ssl_port = m.group(1)

                if not tcp_port and line.find("INSECUREROUTER") >= 0:
                    m = re.match(r"^.*?-p (\d+).*?$", line)
                    if m:
                        tcp_port = m.group(1)
            return ssl_port, tcp_port

        self.ctx.out("")
        if not iga:
            self.ctx.out(
                "No icegridadmin available: Cannot check server list")
        else:
            self._item("Server", "icegridnode")
            p = self.ctx.popen(self._cmd("-e", "server list"))  # popen
            rv = p.wait()
            io = p.communicate()
            if rv != 0:
                self.ctx.out("not started")
                self.ctx.dbg("""
                Stdout:\n%s
                Stderr:\n%s
                """ % io)
            else:
                self.ctx.out("running")
                servers = io[0].split()
                servers.sort()
                for s in servers:
                    decoded = s.decode("utf-8")
                    self._item("Server", "%s" % decoded)
                    p2 = self.ctx.popen(
                        self._cmd("-e", "server state %s" % decoded))  # popen
                    p2.wait()
                    io2 = p2.communicate()
                    if io2[1]:
                        self.ctx.err(io2[1].strip().decode("utf-8"))
                    elif io2[0]:
                        self.ctx.out(io2[0].strip().decode("utf-8"))
                    else:
                        self.ctx.err("UNKNOWN!")
            if self._isWindows():
                # Print the OMERO server Windows service details
                hscm = win32service.OpenSCManager(
                    None, None, win32service.SC_MANAGER_ALL_ACCESS)
                services = win32service.EnumServicesStatus(hscm)
                omesvcs = tuple((sname, fname) for sname, fname, status
                                in services if "OMERO" in fname)
                for sname, fname in omesvcs:
                    self._item("Server", fname)
                    hsc = win32service.OpenService(
                        hscm, sname, win32service.SC_MANAGER_ALL_ACCESS)
                    logonuser = win32service.QueryServiceConfig(hsc)[7]
                    if win32service.QueryServiceStatus(hsc)[1] == \
                            win32service.SERVICE_RUNNING:
                        self.ctx.out("active (running as %s)" % logonuser)
                    else:
                        self.ctx.out("inactive")
                    win32service.CloseServiceHandle(hsc)
                win32service.CloseServiceHandle(hscm)

        def parse_logs():

            log_dir = self.ctx.dir / "var" / "log"
            self.ctx.out("")
            self._item("Log dir", "%s" % log_dir.abspath())
            if not log_dir.exists():
                self.ctx.out("")
                self.ctx.out("No logs available")
                return
            else:
                self._exists(log_dir)

            known_log_files = [
                "Blitz-0.log", "Tables-0.log", "Processor-0.log",
                "Indexer-0.log", "FileServer.log", "MonitorServer.log",
                "DropBox.log", "TestDropBox.log"]
            files = log_dir.files()
            files = set([x.basename() for x in files])
            # Adding known names just in case
            for x in known_log_files:
                files.add(x)
            files = list(files)
            files.sort()
            for x in files:
                self._item("Log files", x)
                self._exists(old_div(log_dir, x))
            self._item("Log files", "Total size")
            sz = 0
            for x in log_dir.walkfiles():
                sz += x.size
            self.ctx.out("%-.2f MB" % (old_div(float(sz), 1000000.0)))
            self.ctx.out("")

            # Parsing well known issues
            ready = re.compile(r".*?ome.services.util.ServerVersionCheck\
            .*OMERO.Version.*Ready..*?")
            db_ready = re.compile(r".*?Did.you.create.your.database[?].*?")
            data_dir = re.compile(r".*?Unable.to.initialize:.FullText.*?")
            pg_password = re.compile(r".*?org.postgresql.util.PSQLException:\
            .FATAL:.password.*?authentication.failed.for.user.*?")
            pg_user = re.compile(r""".*?org.postgresql.util.PSQLException:\
            .FATAL:.role.".*?".does.not.exist.*?""")
            pg_conn = re.compile(r""".*?org.postgresql.util.PSQLException:\
            .Connection.refused.""")

            issues = {
                ready: "=> Server restarted <=",
                db_ready: "Your database configuration is invalid",
                data_dir: "Did you create your omero.data.dir? E.g. /OMERO",
                pg_password: "Your postgres password seems to be invalid",
                pg_user: "Your postgres user is invalid",
                pg_conn: "Your postgres hostname and/or port is invalid"
            }

            try:
                for file in ('Blitz-0.log',):

                    p = self.ctx.dir / "var" / "log" / file
                    import fileinput
                    for line in fileinput.input([str(p)]):
                        lno = fileinput.filelineno()
                        for k, v in list(issues.items()):
                            if k.match(line):
                                self._item('Parsing %s' % file,
                                           "[line:%s] %s" % (lno, v))
                                self.ctx.out("")
                                break
            except Exception:
                self.ctx.err("Error while parsing logs")

        if not args.no_logs:
            parse_logs()

        self.ctx.out("")

        def env_val(val):
            self._item("Environment", "%s=%s"
                       % (val, os.environ.get(val, "(unset)")))
            self.ctx.out("")
        env_val("OMERO_HOME")
        env_val("OMERODIR")
        env_val("OMERO_NODE")
        env_val("OMERO_MASTER")
        env_val("OMERO_USERDIR")
        env_val("OMERO_TMPDIR")
        env_val("PATH")
        env_val("PYTHONPATH")
        env_val("ICE_HOME")
        env_val("LD_LIBRARY_PATH")
        env_val("DYLD_LIBRARY_PATH")

        # List SSL & TCP ports of deployed applications
        self.ctx.out("")
        p = self.ctx.popen(self._cmd("-e", "application list"))  # popen
        rv = p.wait()
        io = p.communicate()
        if rv != 0:
            self.ctx.out("Cannot list deployed applications.")
            self.ctx.dbg("""
            Stdout:\n%s
            Stderr:\n%s
            """ % io)
        else:
            applications = io[0].split()
            applications.sort()
            for s in applications:
                def port_val(port_type, value):
                    self._item("%s %s port" % (s, port_type),
                               "%s" % value or "Not found")
                    self.ctx.out("")
                s = s.decode("utf-8")
                p2 = self.ctx.popen(
                    self._cmd("-e", "application describe %s" % s))
                io2 = p2.communicate()
                if io2[1]:
                    self.ctx.err(io2[1].strip().decode("utf-8"))
                elif io2[0]:
                    ssl_port, tcp_port = get_ports(io2[0].decode("utf-8"))
                    port_val("SSL", ssl_port)
                    port_val("TCP", tcp_port)
                else:
                    self.ctx.err("UNKNOWN!")

        for dir_name, dir_path, dir_size in (
                ("data", omero_data_dir, ""),
                ("temp", omero_temp_dir, True)):
            dir_path_exists = os.path.exists(dir_path)
            is_writable = os.access(dir_path, os.R_OK | os.W_OK)
            if dir_size and dir_path_exists:
                dir_size = self.getdirsize(omero_temp_dir, False)
                dir_size = "   (Size: %s)" % dir_size
            self._item("OMERO %s dir" % dir_name, "'%s'" % dir_path)
            self.ctx.out("Exists? %s\tIs writable? %s%s" %
                         (dir_path_exists, is_writable,
                          dir_size))

        # JVM settings
        self.ctx.out("")
        if memory:
            for k, v in sorted(memory.items()):
                sb = " ".join([str(x) for x in v])
                self._item("JVM settings", " %s" % (k[0].upper() + k[1:]))
                self.ctx.out("%s" % sb)

        def jar_manifest(jar):
            manifest = {}
            error = ''
            try:
                with ZipFile(jar) as z:
                    current = ''
                    for line in z.read('META-INF/MANIFEST.MF').splitlines():
                        line = line.decode()
                        if line and line[0] == ' ':
                            current += line[1:]
                        else:
                            if current:
                                manifest.update([current.split(': ', 1)])
                            current = line
                    if current:
                        manifest.update([current.split(': ', 1)])
            except Exception as e:
                error = str(e)
            return manifest, error

        # Jar versions
        jars = sorted(
            os.path.join(os.path.relpath(root, self.ctx.dir), filename)
            for root, dirnames, filenames in os.walk(self.ctx.dir)
            for filename in filenames
            if filename.endswith('.jar')
        )
        if args.all_jars:
            jar_re = r'.*'
        else:
            jar_re = r'lib/server/(formats|ome|omero)-.*.jar'
        manifest_keys = (
            'Implementation-Title',
            'Implementation-Version',
            'Implementation-Date',
            'Implementation-Build',
        )
        self.ctx.out("")
        for jar in jars:
            if not re.match(jar_re, jar):
                continue
            manifest, error = jar_manifest(self.ctx.dir / jar)
            if error:
                info = [error]
            else:
                info = [manifest.get(key, '') for key in manifest_keys]
            self._item("Jar", jar)
            self.ctx.out('\t'.join(info))



    def email(self, args):
        client = self.ctx.conn(args)
        iadmin = client.sf.getAdminService()
        users, groups = self.get_users_groups(args, iadmin)

        if not args.text:
            args.text = ("-")

        text = " ".join(args.text)
        if text == "-":
            stdin = FileType("r")("-")
            text = stdin.read()

        if args.everyone:
            if users or groups:
                self.ctx.err("Warning: users and groups ignored")

        req = omero.cmd.SendEmailRequest(
            subject=args.subject,
            body=text,
            userIds=users,
            groupIds=groups,
            everyone=args.everyone,
            inactive=args.inactive)

        ms = 500
        wait = args.wait if args.wait > 0 else 25
        loops = ceil(wait * 1000.0 / ms)

        try:
            cb = client.submit(
                req, loops=loops, ms=ms,
                failonerror=True, failontimeout=True)
        except omero.CmdError as ce:
            err = ce.err
            if err.name == "no-body" and err.parameters:
                sb = list(err.parameters.items())
                sb = ["%s:%s" % (k, v) for k, v in sb]
                sb = "\n".join(sb)
                self.ctx.die(12, sb)
            self.ctx.die(13, "Failed to send emails:\n%s" % err)

        try:
            rsp = cb.getResponse()
            self.ctx.out(
                "Successfully sent %s of %s emails" % (
                    rsp.success, rsp.total
                ))
            if rsp.invalidusers:
                self.ctx.out(
                    "%s users had no email address" % len(
                        rsp.invalidusers)
                )
            if rsp.invalidemails:
                self.ctx.out(
                    "%s email addresses were invalid" % len(
                        rsp.invalidemails)
                )
        finally:
            cb.close(True)

    def getdirsize(self, directory, strict=True):
        """
        Uses os.walk to calculate the deep size of the given
        directory. If strict is set, an exception will be thrown
        if a directory disappears during calculation. Otherwise,
        an error will be printed to stderr, but the calculation
        will continue.
        """
        total = 0
        for values in os.walk(directory):
            for filename in values[2]:
                fullpath = os.path.join(values[0], filename)
                try:
                    total += os.path.getsize(fullpath)
                except Exception:
                    self.ctx.err("Failed to get size of: %s" % fullpath)
                    if strict:
                        raise
        return total

    def session_manager(self, communicator):
        import IceGrid
        import Glacier2
        iq = communicator.stringToProxy("IceGrid/Query")
        iq = IceGrid.QueryPrx.checkedCast(iq)
        sm = iq.findAllObjectsByType("::Glacier2::SessionManager")[0]
        sm = Glacier2.SessionManagerPrx.checkedCast(sm)
        return sm

    def can_access(self, filepath, mask=os.R_OK | os.W_OK):
        """
        Check that the given path belongs to
        or is accessible by the current user
        on Linux systems.
        """

        if "Windows" == platform.system():
            return

        pathobj = path(filepath)

        if not pathobj.exists():
            self.ctx.die(8, "FATAL: OMERO directory does not exist: %s"
                         % pathobj)

        if not os.access(filepath, mask):
            self.ctx.die(10, "FATAL: Cannot access %s, a required"
                         " file/directory for OMERO" % filepath)

    def check_access(self, mask=os.R_OK | os.W_OK, config=None):
        """Check that 'var' is accessible by the current user."""

        var = old_div(self.ctx.dir, 'var')
        if not os.path.exists(var):
            self.ctx.out("Creating directory %s" % var)
            os.makedirs(var)
        else:
            self.can_access(var, mask)

        if config is not None:
            omero_data_dir = self._get_data_dir(config)
            self.can_access(omero_data_dir, mask)

        for p in os.listdir(var):
            subpath = os.path.join(var, p)
            if os.path.isdir(subpath):
                self.can_access(subpath, mask)

    def check_lock(self, config):
        """
        Issue a warning if any of the top ".omero" directories
        contain a lock file. This isn't a conclusive test this
        we don't have access to the DB to get the UUID
        for this instance. Usually there should only be one
        though.
        """
        omero_data_dir = self._get_data_dir(config)
        lock_files = os.path.join(
            omero_data_dir, ".omero", "repository",
            "*", ".lock")
        lock_files = glob(lock_files)
        if lock_files:
            self.ctx.err("WARNING: lock files in %s" %
                         omero_data_dir)
            self.ctx.err("-"*40)
            for lock_file in lock_files:
                self.ctx.err(lock_file)
            self.ctx.err("-"*40)
            self.ctx.err((
                "\n"
                "You may want to stop all server processes and remove\n"
                "these files manually. Lock files can remain after an\n"
                "abrupt server outage and are especially frequent on\n"
                "remotely mounted filesystems like NFS.\n"))

    def check_node(self, args):
        """
        If the args argparse.Namespace argument has no "node" attribute,
        then assign one.
        """
        if not hasattr(args, "node"):
            args.node = self._node()

    def checkice(self, args=None):
        """
        Checks for Ice version compatibility.

        See ticket:2514, ticket:1260
        """

        def _check(msg, vers):
            compat = ice_compatibility.split(".")
            if isbytes(vers):
                vers = bytes_to_native_str(vers)
            vers = vers.split(".")
            if compat[0:2] != vers[0:2]:
                self.ctx.die(164, "%s is not compatible with %s: %s"
                             % (msg, ".".join(compat), ".".join(vers)))

        import Ice
        vers = Ice.stringVersion()
        _check("IcePy version", vers)

        # See ticket #10051
        popen = self.ctx.popen(["icegridnode", "--version"])
        env = self.ctx._env()
        # Unclear how this could have been set with the call to unsetenv
        ice_config = env.get("ICE_CONFIG")
        if ice_config is not None and not os.path.exists(ice_config):
            popen = self.ctx.popen(["icegridnode", "--version"],
                                   **{'ICE_CONFIG': ''})

        vers = popen.communicate()[1]
        _check("icegridnode version", vers)

    def open_config(self, unused=None):
        """
        Callers are responsible for closing the
        returned ConfigXml object.
        """
        cfg_xml = old_div(self._get_grid_dir(), "config.xml")
        cfg_tmp = old_div(self._get_grid_dir(), "config.xml.tmp")
        grid_dir = self._get_grid_dir()
        if not cfg_xml.exists() and self.can_access(grid_dir):
            if cfg_tmp.exists() and self.can_access(cfg_tmp):
                self.ctx.dbg("Removing old config.xml.tmp")
                cfg_tmp.remove()
            config = omero.config.ConfigXml(str(cfg_tmp))
            try:
                self.ctx.controls["config"].upgrade(None, config)
            finally:
                config.close()
            self.ctx.err("Creating %s" % cfg_xml)
            cfg_tmp.rename(str(cfg_xml))

        try:
            try:
                config = omero.config.ConfigXml(str(cfg_xml))
            except Exception as e:
                self.ctx.die(577, str(e))
            if config.save_on_close:
                config.save()
            else:
                self.ctx.err("%s read-only" % cfg_xml)
        except portalocker.LockException:
            try:
                config.close()
            except Exception:
                pass
            self.ctx.die(111, "Could not acquire lock on %s" % cfg_xml)

        return config

    @with_config
    def reindex(self, args, config):

        self.check_access(config=config)
        import omero.java
        server_dir = self.ctx.dir / "lib" / "server"
        log_config_file = self.ctx.dir / "etc" / "logback-indexing-cli.xml"
        logback = "-Dlogback.configurationFile=%s" % log_config_file
        classpath = [file.abspath() for file in server_dir.files("*.jar")]
        xargs = [logback, "-cp", os.pathsep.join(classpath)]
        # See etc/grid/templates.xml
        for v in (("warn", "3600000"), ("error", "86400000")):
            xargs.append("-Domero.throttling.method_time.%s=%s" % v)

        cfg = config.as_map()
        omero_data_dir = self._get_data_dir(config)
        config.close()  # Early close. See #9800
        for x in ("name", "user", "host", "port", "properties", "url"):
            # NOT passing password on command-line
            k = "omero.db.%s" % x
            if k in cfg:
                v = cfg[k]
                xargs.append("-D%s=%s" % (k, v))

        xargs.append("-Domero.data.dir=%s" % omero_data_dir)
        for k, v in list(cfg.items()):
            if k.startswith("omero.search"):
                xargs.append("-D%s=%s" % (k, cfg[k]))

        locks = {"native": "org.apache.lucene.store.NativeFSLockFactory",
                 "simple": "org.apache.lucene.store.SimpleFSLockFactory",
                 "none": "org.apache.lucene.store.NoLockFactory"}
        lock = locks.get(args.lock_factory, args.lock_factory)

        year2 = datetime.datetime.now().year + 2
        factory_class = "org.apache.lucene.store.FSDirectoryLockFactoryClass"
        xargs2 = ["-Xmx%s" % args.mem,
                  "-Domero.search.cron=1 1 1 1 1 ? %s" % year2,
                  "-Domero.search.batch=%s" % args.batch,
                  "-Domero.search.merge_factor=%s" % args.merge_factor,
                  "-Domero.search.ram_buffer_size=%s" % args.ram_buffer_size,
                  "-D%s=%s" % (factory_class, lock)]
        xargs.extend(xargs2)

        cmd = ["ome.services.fulltext.Main"]

        # Python actions
        early_exit = False
        if args.wipe:
            early_exit = True
            self.can_access(omero_data_dir)
            from os.path import sep
            pattern = sep.join([omero_data_dir, "FullText", "*"])
            files = glob(pattern)
            total = 0
            self.ctx.err("Wiping %s files matching %s" % (len(files), pattern))
            for file in files:
                size = os.path.getsize(file)
                total += 0
                print(file, size)
            print("Total:", size)
            yes = self.ctx.input("Enter 'y' to continue:")
            if not yes.lower().startswith("y"):
                return
            else:
                for file in files:
                    try:
                        os.remove(file)
                    except Exception:
                        self.ctx.err("Failed to remove: %s", file)

        elif args.prepare:
            early_exit = True
            self.stop_service("Indexer-0")
        elif args.finish:
            early_exit = True
            self.start_service("Indexer-0")

        if early_exit:
            return  # Early exit!

        if self.check_service("Indexer-0") and not args.prepare:
            self.ctx.die(578, "Indexer-0 is running")

        # Java actions
        if args.full:
            cmd.append("full")
        elif args.dryrun:
            cmd.append("dryrun")
        elif args.foreground:
            cmd.append("foreground")
            self.ctx.die(877, "foreground indexing is not available")
        elif args.reset is not None:
            cmd.append("reset")
            cmd.append(args.reset)
        elif args.events:
            cmd.append("events")
        elif getattr(args, "class"):
            cmd.append("reindex")
            cmd.extend(getattr(args, "class"))
        else:
            self.ctx.die(502, "No valid action: %s" % args)

        debug = False
        if getattr(args, "jdwp"):
            debug = True

        # Pass omero.db.pass using JAVA_OPTS environment variable
        if "omero.db.pass" in cfg:
            dbpassargs = "-Domero.db.pass=%s" % cfg["omero.db.pass"]
            if "JAVA_OPTS" not in os.environ:
                os.environ['JAVA_OPTS'] = dbpassargs
            else:
                os.environ['JAVA_OPTS'] = "%s %s" % (
                    os.environ.get('JAVA_OPTS'), dbpassargs)

        self.ctx.dbg(
            "Launching Java: %s, debug=%s, xargs=%s" % (cmd, debug, xargs))
        p = omero.java.run(cmd,
                           use_exec=True, debug=debug, xargs=xargs,
                           stdout=sys.stdout, stderr=sys.stderr)
        self.ctx.rv = p.wait()

    @admin_only(full_admin=True)
    def cleanse(self, args):
        self.check_access()
        from omero.util.cleanse import cleanse
        cleanse(data_dir=args.data_dir, client=self.ctx.conn(args),
                dry_run=args.dry_run)

    @admin_only(full_admin=False)
    def log(self, args):
        client = self.ctx.conn(args)
        req = RawAccessRequest(command='log', path=args.level,
                               repoUuid=args.repo, args=[args.message])
        client.submit(req).loop(100, 100)

    def sessionlist(self, args):
        self.ctx.err('WARNING: "admin sessionlist" is deprecated, '
                     'use "sessions who" instead')
        client = self.ctx.conn(args)
        service = client.sf.getQueryService()
        params = omero.sys.ParametersI()
        query = "select s from Session s join fetch s.node n join fetch"\
            " s.owner o where s.closed is null and n.id != 0"
        results = service.findAllByQuery(query, params)
        mapped = list()
        for s in results:
            rv = list()
            mapped.append(rv)
            if not s.isLoaded():
                rv.append("")
                rv.append("id=%s" % s.id.val)
                rv.append("")
                rv.append("")
                rv.append("")
                rv.append("insufficient privileges")
            else:
                rv.append(s.node.id)
                rv.append(s.uuid)
                rv.append(s.started)
                rv.append(s.owner.omeName)
                if s.userAgent is None:
                    rv.append("")
                else:
                    rv.append(s.userAgent)
                if client.getSessionId() == s.uuid.val:
                    rv.append("current session")
                else:
                    rv.append("")
        self.ctx.controls["hql"].display(
            mapped, ("node", "session", "started", "owner", "agent", "notes"))

    def check_service(self, name):
        command = self._cmd()
        command.extend(["-e", "server pid %s" % name])
        p = self.ctx.popen(command)  # popen
        rc = p.wait()
        return rc == 0

    def start_service(self, name):
        command = self._cmd()
        command.extend(["-e", "server enable %s" % name])
        rc = self.ctx.call(command)
        if rc != 0:
            self.ctx.err("%s could not be enabled" % name)
        else:
            self.ctx.err("%s restarted" % name)

    def stop_service(self, name):
        command = self._cmd()
        command.extend(["-e", "server disable %s" % name])
        rc = self.ctx.call(command)
        if rc != 0:
            self.ctx.err("%s may already be disabled" % name)
        else:
            command = self._cmd()
            command.extend(["-e", "server stop %s" % name])
            rc = self.ctx.call(command)
            if rc != 0:
                self.ctx.err("'server stop %s' failed" % name)
            else:
                self.ctx.err("%s stopped" % name)

    def _get_data_dir(self, config):
        config = config.as_map()
        return config.get("omero.data.dir", "/OMERO")

    @with_config
    def checkupgrade(self, args, config):
        """
        Checks whether a server upgrade is available, exits with return code
        0: this is the latest version
        1: an upgrade is available
        2: an error occurred whilst checking
        """

        uc = self.run_upgrade_check(config, CHECKUPGRADE_USERAGENT)
        if uc.isUpgradeNeeded():
            self.ctx.die(1, uc.getUpgradeUrl())
        if uc.isExceptionThrown():
            self.ctx.die(2, uc.getExceptionThrown())


try:
    register("admin", AdminControl, HELP)
except NameError:
    if __name__ == "__main__":
        cli = CLI()
        cli.register("admin", AdminControl, HELP)
        cli.invoke(sys.argv[1:])
