# Copyright 2015 VMware, Inc.  All rights reserved. -- VMware Confidential
#
"""
Module contains contract specifications about the patching extensibility.
"""

import logging
import sys

from status_reporting_sdk.json_utils import Serializable
from l10n import LocalizableMessage

logger = logging.getLogger(__name__)

PY2 = sys.version_info[0] == 2
if not PY2:
    basestring = str  # pylint: disable=W0622

def validateObject(obj, expectedType, expectedSubtype=None, allowedValues=None):
    '''Validates the object type.

    @param obj: Arbitrary object
    @type obj: object

    @param expectedType: Expected object type or tuple of types
    @type expectedType: type

    @param expectedSubtype: Expected list item type of tuple of types.
    @type expectedSubtype: type

    @param allowedValues: The definite combination of allowed element values.
      If None is passed then any the parameter value will be allowed. If the
      parameter value is not allowed then exception will be thrown.

    @raise ValueError: If object or subobjects has invalid type.
    '''
    if not isinstance(obj, expectedType):
        raise ValueError('%s has type %s, expected: %s' % \
                         (obj, type(obj), expectedType))

    if expectedType == list:
        for index, subObj in enumerate(obj):
            if not isinstance(subObj, expectedSubtype):
                raise ValueError('obj %s index %s has type %s, expected %s' % \
                                  (obj, index, type(obj), expectedSubtype))

    # Check if the value is in the allowed range
    if allowedValues and obj not in allowedValues:
        errorMsg = 'Object equal to %s, expected to be any of %s'\
                    % (obj, ", ".join(str(x) for x in allowedValues))
        logger.error(errorMsg)
        raise ValueError(errorMsg)

class PatchContext(Serializable):
    # The disruptive upgrade is an in-place upgrade.
    DISRUPTIVE_UPGRADE = "disruptive"
    # The non-disruptive upgrade uses a spare appliance to avoid the downtime.
    NONDISRUPTIVE_UPGRADE = "nondisruptive"

    def __init__(self, stageDirectory, locale="en",
                 userData=None, skipPrechecksIds=None,
                 upgradeType=DISRUPTIVE_UPGRADE):
        '''Patch context is the input parameter passed to all patch hooks.

        @param stageDirectory: A working directory dedicated to that component.
          This is the directory which component can use, and the framework should
          be responsible to clean it up on patch completion(either success or
          failure). Stage directory should be also used for files contract
          between framework and a hook, e.g. path to EULAs files.
        @type stageDirectory: String

        @param locale: Locale at which all customer-facing messages should
          be translated to. This property is also useful if a component is using
          3rd party libraries on different language and want to pass the locale
          to them.
        @type locale: String

        @param userData: Lookup of question ID to an answer of that question.
          It contains answers of all questions raised during Discovery patching
          hook. Hereby, it does not make sense to be used in Discovery patching
          hook, because it will be still empty.
        @type userData: dict

        @param skipPrechecksIds: List of ids of prechecks that should not be executed
            during any of the executions.
        @type skipPrechecksIds: list(str)
        @param upgradeType: The type of the upgrade being performed. The allowed values
          are either PatchContext.DISRUPTIVE_UPGRADE or PatchContext.NONDISRUPTIVE_UPGRADE.
        @type upgradeType: str
        '''
        userData = userData or dict()
        skipPrechecksIds = skipPrechecksIds or []

        validateObject(stageDirectory, basestring)
        validateObject(locale, basestring)
        validateObject(userData, dict)
        validateObject(skipPrechecksIds, list, basestring)
        validateObject(upgradeType, basestring, allowedValues=[self.DISRUPTIVE_UPGRADE, self.NONDISRUPTIVE_UPGRADE])

        self.stageDirectory = stageDirectory
        self.locale = locale
        self.userData = userData
        self.skipPrechecksIds = skipPrechecksIds
        self.upgradeType = upgradeType

class DiscoveryResult(Serializable):
    def __init__(self, displayName, patchServices=list(), dependentServices=list(),  # pylint: disable=W0102
                 ignoreRpms=list(), deprecatedProducts=list(), patchable=True,
                 componentId=None, dependentComponents=list(), replicationConfig={}):
        '''The specification returned in case a component want to take part of
        the patch process.

        @param displayName: Customer-aware display name. On every operation about
          current component, the displayName will be used in order to notify
          the user what the system is currently doing.
        @type displayName: LocalizableMessage

        @param patchServices: Those are the services which the component is
          taking care of and going to patch. If another component has a
          dependency to some of current component patch services, then that
          dependency will be respected during execution of @Patch hook.
        @type patchServices: list of strings

        @param dependentServices: The services which current component is
          dependent on. That dependency will be respected during execution of
          @Patch hook.
        @type dependentServices: list of strings
        Allowed values:
           - [] - No dependencies
           - ['specific-svc', 'specific-svc2'] - enumerate the services which
             component is depending on. Those services should be patched and
             started at the time when @Patch hook of this component is called out.

        @param ignoreRpms: List of rpms which should not be downloaded and
          respectively patched. Used when patch payload provides more rpms
          than actually necessary in certain deplopyment
        @type ignoreRpms: list of strings

        @param deprecatedProducts: The products which are no longer available,
          and should be un-installed on patch successful completion.
        @type deprecatedProducts: list of string

        @param componentId: Unique component name, which other components can
          use to indicate that they depend on it.
        @type componentId: str

        @param dependentComponents: The components which current component is
          depending on. That dependency will be respected during execution of
          @Patch hook.
        @type dependentComponents: list of strings
        Allowed values:
           - [] - No dependencies
           - ['specific-svc', 'specific-svc2'] - enumerate the components which
             component is depending on. Those components should be patched and
             started at the time when @Patch hook of this component is called out.

        @param patchable: The property specifies if the rest of the component
          hooks has to be called out. If a product is deprecated it may decide
          to not take part of the patch process anymore and leave the framework
          to uninstall it on patch process completion.
        @type patchable: boolean

        @param replicationConfig: Specifies the files to be replicated from the source
          to the target appliance during nondisruptive upgrade, that are not included
          as part of the default replication configuration. The default replication
          configuration is built on the Backup & Restore component's configuration
          present in the target version. The key is the source path of a file or
          directory to be replicated and the value is the path where it to be placed on
          the target appliance. To remove the file from the default configuration, map
          that file or a directory to None.
        @type replicationConfig: dict
        '''
        validateObject(displayName, LocalizableMessage)
        validateObject(patchServices, list, basestring)
        validateObject(dependentServices, list, basestring)
        validateObject(ignoreRpms, list, basestring)
        validateObject(deprecatedProducts, list, basestring)
        validateObject(patchable, bool)
        validateObject(componentId, basestring)
        validateObject(dependentComponents, list, basestring)
        validateObject(replicationConfig, dict)

        self.displayName = displayName
        self.patchServices = patchServices
        self.dependentServices = dependentServices
        self.ignoreRpms = ignoreRpms
        self.deprecatedProducts = deprecatedProducts
        self.patchable = patchable
        self.componentId = componentId
        self.dependentComponents = dependentComponents
        self.replicationConfig = replicationConfig

class Question(Serializable):
    # The customer input is in plain format
    PLAIN_KIND = "plain"
    # The customer input should be asked for yes/no question
    BINARY_KIND = "binary"
    # The customer input should be pruned
    PASSWORD_KIND = "password"

    def __init__(self, userDataId, text, description=None, kind=PLAIN_KIND,  # pylint: disable=W0102
                 allowedValues=list(), defaultAnswer=None):
        '''A specification about a question which should be popped up to the
        customer before patch process is triggered.
        NOTE: The expected answer is a single result, i.e. multiple-choice
        answer is not expected/supported

        @param userDataId: Unique question identifier. It will be used later to
          map the answer to raised question. The answers will come as part of
          PatchContext.userData
        @type userDataId: string

        @param text: Summary question text.
        @type text: LocalizableMessage

        @param description: Detailed explanation about that question.
        @type description: LocalizableMessag

        @param kind: Question kind.
        @type kind: string

        @param allowedAnswers: Used in cases when the user should choose between
          multiple options.
        @type allowedAnswers: list of LocalizableMessage

        @param defaultAnswer: Used in cases when the user should choose between
          multiple options, and one of them should be preselected.
        @type defaultAnswer: LocalizableMessage, or bool for BINARY_KIND
        '''
        validateObject(userDataId, basestring)
        validateObject(text, LocalizableMessage)
        validateObject(description, (LocalizableMessage, type(None)))
        validateObject(kind, basestring, allowedValues=[self.PLAIN_KIND, self.PASSWORD_KIND, self.BINARY_KIND])

        if kind == self.BINARY_KIND:
            validateObject(defaultAnswer, (bool, type(None)))
            # Binary questions should not have allowed values
            # This ensures that none are passed
            validateObject(allowedValues, list, basestring, allowedValues=[list()])
        else:
            validateObject(defaultAnswer, (basestring, type(None)))
            validateObject(allowedValues, list, basestring)


        self.userDataId = userDataId
        self.text = text
        self.description = description
        self.kind = kind
        self.allowedValues = allowedValues
        self.defaultAnswer = defaultAnswer

    def __eq__(self, other):
        if not isinstance(other, Question):
            return False

        return self.userDataId == other.userDataId and \
                self.text == other.text and \
                self.description == other.description and \
                self.kind == other.kind and \
                self.allowedValues == other.allowedValues and \
                self.defaultAnswer == other.defaultAnswer

    def __ne__(self, other):
        return not self.__eq__(other)

class Requirements(Serializable):
    def __init__(self, requiredDiskSpace=dict(), questions=list(),  # pylint: disable=W0102
                 rebootRequired=False):
        '''Structure expressing the requirements which should take in
        consideration when the patch is applied. The framework should aggregate
        the requirements data returned from each validation hook and made a
        final validation check to see if all requirements are met and the
        patch is likely to succeed.

        @param requiredDiskSpace: Lookup of vmdk to required extra disk space
          needed on that vmdk for applying the patch. The size of the rpm is not
          included in this size. This property expresses the extra size required,
          e.g. if a component patches the product via export/import functionality
          then it needs double product size in order to export the data. The
          size is in MBs.
        @type requiredDiskSpace: dictionary of string to int

        @param questions: Questions which should be popped up to the customer
          and answered before the actual patch process begins.
        @type questions: list of Question

        @param rebootRequired: Specifies if a reboot is required after the patch
          process complete.
        @type rebootRequired: boolean
        '''
        validateObject(requiredDiskSpace, dict)
        validateObject(questions, list, Question)
        validateObject(rebootRequired, bool)

        self.requiredDiskSpace = requiredDiskSpace
        self.questions = questions
        self.rebootRequired = rebootRequired

class PatchInfo(Serializable):
    def __init__(self, timeToInstall=0, summary=None):
        '''Patch information which is supposed to be shown to the customer
        before patch is applied.

        @param timeToInstall: Time needed to execute @Prepatch the @Patch hooks.
          The time does not include the time needed for extracting the rpm.
          The time is in minutes.
        @type timeToInstall: int

        @param summary: Text expressing what has been changed in latest
          version. The product could make a diff between currently installed
          version and version coming with the rpm and briefly explains what
          are the benefits of applying the latest rpm.
        @type summary: LocalizableMessage
        '''
        validateObject(timeToInstall, int)
        validateObject(summary, (LocalizableMessage, type(None)))

        self.timeToInstall = timeToInstall
        self.summary = summary

class Mismatch(Serializable):
    WARNING = "warning"
    ERROR = "error"

    def __init__(self, text, description=None, resolution=None, problemId=None,
                 severity=ERROR, relatedUserDataId=None):
        '''Describes a mismatch which could be either warning or error. If an
        error then the customer needs to fix the mismatch and re-validate the
        environment.

        @param text: Mismatch summary
        @type text: LocalizableMessage

        @param description: Detailed text about the mismatch. If omitted, then
          the text will be filled out from text field.
        @type description: LocalizableMessage

        @param resolution: Suggest the user how to fix the problem.
        @type resolution: LocalizableMessage

        @param problemId: Unique problem identificator which is mapped to
          external VMware article explaining the problem.
        @type problemId: str

        @param severity: Mismatch severity. Warning will notify the user about
          a mismatch, but will allow him to proceed further with the patching
          process, while Error will forbid the user to proceed further.
        @type severity: string

        @param relatedUserDataId: Used in case when the mismatch is related
          to the customer input.
        @type relatedUserDataId: string
        '''
        validateObject(text, LocalizableMessage)
        validateObject(description, (LocalizableMessage, type(None)))
        validateObject(resolution, (LocalizableMessage, type(None)))
        validateObject(problemId, (basestring, type(None)))
        validateObject(severity, basestring, allowedValues=[self.WARNING, self.ERROR])
        validateObject(relatedUserDataId, (basestring, type(None)))

        self.text = text
        self.description = description
        self.resolution = resolution
        self.problemId = problemId
        self.severity = severity
        self.relatedUserDataId = relatedUserDataId

class RequirementsResult(Serializable):
    def __init__(self, requirements=Requirements(), patchInfo=PatchInfo(),  # pylint: disable=W0102
                 mismatches=list()):
        '''Holds the data about the validation hook. If mismatches are not
        specified, their length is 0 or all of them are warnings, then the
        validation has succeeded and the customer will be allowed to proceed
        further with the patching process. If even one error is returned, or
        some other component hook return an error, then the customer will
        be prompted to fix either the environment or his/her input and
        re-try, which will cause the validation hook to be executed again.
        The requirements and patch information structures will be used only in
        case the validation has succeeded.

        @param requirements: Structure holding information about the
          requirements of coming patching process.
        @type requirements: Summary

        @param requirements: Structure holding information about the results of
          applying of patching process.
        @type requirements: PatchInfo

        @param mismatches: List of mismatches found during execution of
          validation hook
        @type mismatches: List of Mismatch
        '''
        validateObject(requirements, Requirements)
        validateObject(patchInfo, PatchInfo)
        validateObject(mismatches, list, Mismatch)

        self.requirements = requirements
        self.patchInfo = patchInfo
        self.mismatches = mismatches

class ValidationResult(Serializable):
    def __init__(self, mismatches=list()):  # pylint: disable=W0102
        '''Holds the data about the validation hook. If mismatches are not
        specified, their length is 0 or all of them are warnings, then the
        validation has succeeded and the customer will be allowed to proceed
        further with the patching process. If even one error is returned, or
        some other component hook return an error, then the customer will
        be prompted to fix either the environment or his/her input and
        re-try, which will cause the validation hook to be executed again.

        @param mismatches: List of mismatches found during execution of
          validation hook
        @type mismatches: List of Mismatch
        '''
        validateObject(mismatches, list, Mismatch)

        self.mismatches = mismatches
