# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (c) 2011 OpenStack, LLC.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""
Manages packages on the Guest VM.
"""
import commands
import os
import pexpect
import re

from trove.common import exception
from trove.common import utils
from trove.common.exception import ProcessExecutionError
from trove.openstack.common import log as logging
from trove.guestagent import system
from trove.openstack.common.gettextutils import _


LOG = logging.getLogger(__name__)
OK = 0
RUN_DPKG_FIRST = 1
REINSTALL_FIRST = 2


class PkgAdminLockError(exception.TroveError):
    pass


class PkgPermissionError(exception.TroveError):
    pass


class PkgPackageStateError(exception.TroveError):
    pass


class PkgNotFoundError(exception.NotFound):
    pass


class PkgTimeout(exception.TroveError):
    pass


class PkgScriptletError(exception.TroveError):
    pass


class PkgTransactionCheckError(exception.TroveError):
    pass


class PkgDownloadError(exception.TroveError):
    pass


class BasePackagerMixin:

    def pexpect_kill_proc(self, child):
        child.delayafterclose = 1
        child.delayafterterminate = 1
        child.close(force=True)

    def pexpect_wait_and_close_proc(self, child):
        child.expect(pexpect.EOF)
        child.close()

    def pexpect_run(self, cmd, output_expects, time_out):
        child = pexpect.spawn(cmd, timeout=time_out)
        try:
            i = child.expect(output_expects)
            self.pexpect_wait_and_close_proc(child)
        except pexpect.TIMEOUT:
            self.pexpect_kill_proc(child)
            raise PkgTimeout("Process timeout after %i seconds." % time_out)
        return i


class RedhatPackagerMixin(BasePackagerMixin):

    def _install(self, package_name, time_out):
        """Attempts to install a package.

        Returns OK if the package installs fine or a result code if a
        recoverable-error occurred.
        Raises an exception if a non-recoverable error or time out occurs.

        """
        cmd = "sudo yum --color=never -y install %s" % package_name
        output_expects = ['\[sudo\] password for .*:',
                          'No package %s available.' % package_name,
                          'Transaction Check Error:',
                          '.*scriptlet failed*',
                          'HTTP Error',
                          'No more mirrors to try.',
                          '.*already installed and latest version',
                          'Updated:',
                          'Installed:']
        i = self.pexpect_run(cmd, output_expects, time_out)
        if i == 0:
            raise PkgPermissionError("Invalid permissions.")
        elif i == 1:
            raise PkgNotFoundError("Could not find pkg %s" % package_name)
        elif i == 2:
            raise PkgTransactionCheckError("Transaction Check Error")
        elif i == 3:
            raise PkgScriptletError("Package scriptlet failed")
        elif i == 4 or i == 5:
            raise PkgDownloadError("Package download problem")
        return OK

    def _remove(self, package_name, time_out):
        """Removes a package.

        Returns OK if the package is removed successfully or a result code if a
        recoverable-error occurs.
        Raises an exception if a non-recoverable error or time out occurs.

        """
        cmd = "sudo yum --color=never -y remove %s" % package_name
        output_expects = ['\[sudo\] password for .*:',
                          'No Packages marked for removal',
                          'Removed:']
        i = self.pexpect_run(cmd, output_expects, time_out)
        if i == 0:
            raise PkgPermissionError("Invalid permissions.")
        elif i == 1:
            raise PkgNotFoundError("Could not find pkg %s" % package_name)
        return OK

    def pkg_install(self, package_name, time_out):
        result = self._install(package_name, time_out)
        if result != OK:
            raise PkgPackageStateError("Package %s is in a bad state."
                                       % package_name)

    def pkg_version(self, package_name):
        cmd_list = ["rpm", "-qa", "--qf", "'%{VERSION}-%{RELEASE}\n'",
                    package_name]
        p = commands.getstatusoutput(' '.join(cmd_list))
        # Need to capture the version string
        # check the command output
        std_out = p[1]
        for line in std_out.split("\n"):
            regex = re.compile("[0-9.]+-.*")
            matches = regex.match(line)
            if matches:
                line = matches.group()
                return line
        msg = _("version() saw unexpected output from rpm!")
        LOG.error(msg)

    def pkg_remove(self, package_name, time_out):
        """Removes a package."""
        if self.pkg_version(package_name) is None:
            return
        result = self._remove(package_name, time_out)
        if result != OK:
            raise PkgPackageStateError("Package %s is in a bad state."
                                       % package_name)


class DebianPackagerMixin(BasePackagerMixin):

    def _fix(self, time_out):
        """Sometimes you have to run this command before a pkg will install."""
        try:
            utils.execute("dpkg", "--configure", "-a", run_as_root=True,
                          root_helper="sudo")
        except ProcessExecutionError as e:
            LOG.error(_("Error fixing dpkg"))

    def _install(self, package_name, time_out):
        """Attempts to install a package.

        Returns OK if the package installs fine or a result code if a
        recoverable-error occurred.
        Raises an exception if a non-recoverable error or time out occurs.

        """
        cmd = "sudo -E DEBIAN_FRONTEND=noninteractive " \
              "apt-get -y --allow-unauthenticated install %s" % package_name
        output_expects = ['.*password*',
                          'E: Unable to locate package %s' % package_name,
                          "Couldn't find package % s" % package_name,
                          ("dpkg was interrupted, you must manually run "
                           "'sudo dpkg --configure -a'"),
                          "Unable to lock the administration directory",
                          "Setting up %s*" % package_name,
                          "is already the newest version"]
        i = self.pexpect_run(cmd, output_expects, time_out)
        if i == 0:
            raise PkgPermissionError("Invalid permissions.")
        elif i == 1 or i == 2:
            raise PkgNotFoundError("Could not find apt %s" % package_name)
        elif i == 3:
            return RUN_DPKG_FIRST
        elif i == 4:
            raise PkgAdminLockError()
        return OK

    def _remove(self, package_name, time_out):
        """Removes a package.

        Returns OK if the package is removed successfully or a result code if a
        recoverable-error occurs.
        Raises an exception if a non-recoverable error or time out occurs.

        """
        cmd = "sudo -E apt-get -y --allow-unauthenticated remove %s" \
              % package_name
        output_expects = ['.*password*',
                          'E: Unable to locate package %s' % package_name,
                          'Package is in a very bad inconsistent state',
                          'Sub-process /usr/bin/dpkg returned an error code',
                          ("dpkg was interrupted, you must manually run "
                           "'sudo dpkg --configure -a'"),
                          "Unable to lock the administration directory",
                          "Removing %s*" % package_name]
        i = self.pexpect_run(cmd, output_expects, time_out)
        if i == 0:
            raise PkgPermissionError("Invalid permissions.")
        elif i == 1:
            raise PkgNotFoundError("Could not find pkg %s" % package_name)
        elif i == 2 or i == 3:
            return REINSTALL_FIRST
        elif i == 4:
            return RUN_DPKG_FIRST
        elif i == 5:
            raise PkgAdminLockError()
        return OK

    def pkg_install(self, package_name, time_out):
        """Installs a package."""
        try:
            utils.execute("apt-get", "update", run_as_root=True,
                          root_helper="sudo")
        except ProcessExecutionError as e:
            LOG.error(_("Error updating the apt sources"))

        result = self._install(package_name, time_out)
        if result != OK:
            if result == RUN_DPKG_FIRST:
                self._fix(time_out)
            result = self._install(package_name, time_out)
            if result != OK:
                raise PkgPackageStateError("Package %s is in a bad state."
                                           % package_name)

    def pkg_version(self, package_name):
        cmd_list = ["dpkg", "-l", package_name]
        p = commands.getstatusoutput(' '.join(cmd_list))
        # check the command status code
        if not p[0] == 0:
            return None
        # Need to capture the version string
        # check the command output
        std_out = p[1]
        patterns = ['.*No packages found matching.*',
                    "\w\w\s+(\S+)\s+(\S+)\s+(.*)$"]
        for line in std_out.split("\n"):
            for p in patterns:
                regex = re.compile(p)
                matches = regex.match(line)
                if matches:
                    line = matches.group()
                    parts = line.split()
                    if not parts:
                        msg = _("returned nothing")
                        LOG.error(msg)
                        raise exception.GuestError(msg)
                    if len(parts) <= 2:
                        msg = _("Unexpected output.")
                        LOG.error(msg)
                        raise exception.GuestError(msg)
                    if parts[1] != package_name:
                        msg = _("Unexpected output:[1] = %s" % str(parts[1]))
                        LOG.error(msg)
                        raise exception.GuestError(msg)
                    if parts[0] == 'un' or parts[2] == '<none>':
                        return None
                    return parts[2]
        msg = _("version() saw unexpected output from dpkg!")
        LOG.error(msg)

    def pkg_remove(self, package_name, time_out):
        """Removes a package."""
        if self.pkg_version(package_name) is None:
            return
        result = self._remove(package_name, time_out)

        if result != OK:
            if result == REINSTALL_FIRST:
                self._install(package_name, time_out)
            elif result == RUN_DPKG_FIRST:
                self._fix(time_out)
            result = self._remove(package_name, time_out)
            if result != OK:
                raise PkgPackageStateError("Package %s is in a bad state."
                                           % package_name)


class BasePackage(type):

    def __new__(meta, name, bases, dct):
        if system.OS == system.REDHAT:
            bases += (RedhatPackagerMixin, )
        else:
            # The default is debian
            bases += (DebianPackagerMixin,)
        return super(BasePackage, meta).__new__(meta, name, bases, dct)


class Package(object):

    __metaclass__ = BasePackage
