#! /usr/bin/env python3
#
#  sddc_mgr_rest.py
#
# Copyright 2020-2023 VMware, Inc.  All rights reserved. -- VMware Confidential
# Description:  Provides class SddcMgrRest.
# Disabled: True
#

import json
import logging
import os
import subprocess
import sys

try:
    import requests
    requestsLibPresent = True
except Exception:
    requestsLibPresent = False


logger = logging.getLogger(__name__)


class SddcMgrRest:
    """
    Helper for accessing (get, delete, CLI) the SDDC Manager.
    """

    remoteSshMessageShown = False

    def _areWeRunningOnSddcManager(self):
        """Return True if we are executing in the SDDC Manager VM, else False.
        """
        onSddcManager = os.path.isdir("/etc/vmware/vcf/sddc-manager-ui-app/")

        return onSddcManager

    def _runSddcMgrCmd(self, cmd):
        """ssh to the SDDC manager VM (as user "vcf") and run the supplied
        command.  This allows things like running a curl command against
        localhost which would likely be prohibited by firewall settings if
        attempted from outside the VM.

        We return the command's raw output text.

        cmd: list
            Command components, starting with the command. We will prepend
            any needed ssh or other commands required to run the supplied
            command inside the SDDC manager VM.
        """
        if self.runningOnSddcManager:
            full_cmd = cmd
        else:
            # if off-VM, must have password-free ssh access as user "vcf"
            full_cmd = ["ssh", "vcf@%s" % self.sddcMgrAddr] + cmd

        return subprocess.check_output(full_cmd)

    def _runLocalRestQuery(self, url, restOp, inputData,
                           useAuth, authUser, authToken,
                           skipRaiseForStatus=False):
        """
        If running locally on SDDC Manager, use python's requests package
        since we don't need to worry about transiting the nginx firewall
        and requests is otherwise more robust.

        Results, if any, are normally in JSON format which we process
        using _outTextToOutDict() and return as a dict.
        If there are no results then we return an empty dict.

        restOp : string
            One of "GET", "POST", "PUT", "PATCH", "DELETE"
        """

        headers = {}

        if inputData:
            if isinstance(inputData, dict):
                inputDataDict = inputData
            else:
                inputDataDict = json.loads(inputData)

            headers['Content-Type'] = 'application/json'
        else:
            inputDataDict = None

        if authToken:
            headers['Authorization'] = 'Bearer ' + authToken
        elif useAuth:
            # useAuth / authUser are obsolete
            pass

        # convenient to bundle up our params in a session and then run
        # our selected op against that session

        with requests.Session() as sess:
            sess.verify = False
            sess.headers = headers

            # One of "GET", "POST", "PUT", "PATCH", "DELETE"
            if restOp == "GET":
                resp = sess.get(url)
            elif restOp == "POST":
                resp = sess.post(url, json=inputDataDict)
            elif restOp == "PUT":
                resp = sess.put(url, json=inputDataDict)
            elif restOp == "PATCH":
                resp = sess.patch(url, json=inputDataDict)
            elif restOp == "DELETE":
                resp = sess.delete(url)
            else:
                logger.error('Unsupported REST op requested: "%s"' % restOp)
                sys.exit(1)

        # raise_for_status() is a somewhat tighter check than we can get with curl,
        # but we need to give our caller the option to request curl-style
        # _outTextToOutDict() instead. The latter is used to find deprecated
        # credential APIs and maybe elsewhere too.
        if not skipRaiseForStatus:
            resp.raise_for_status()

        # decode output text, if any. Our processing here is a little distinctive.
        requestDescr = "%s %s" % (restOp, url)
        outDict = self._outTextToOutDict(resp.text, requestDescr)

        return outDict

    def _runSddcMgrRestQuery(self, url, curlOp, useAuth=False,
                             authUser=None, inputData=None, authToken=None,
                             skipRaiseForStatus=False):
        """Ssh to SDDC Manager and submit a specified REST request type
        against the supplied URL.

        Results, if any, are expected to be in JSON format, which we parse
        and return as a dict. If there are no results then we return an
        empty dict.
        """
        if self.runningOnSddcManager and requestsLibPresent:
            # when possible, use requests for large input payload support
            return self._runLocalRestQuery(url=url, restOp=curlOp,
                                           inputData=inputData,
                                           useAuth=useAuth, authUser=authUser,
                                           authToken=authToken,
                                           skipRaiseForStatus=skipRaiseForStatus)

        cmd = ["curl", "-s", "--insecure",
               "-X", curlOp]

        # $(getconf ARG_MAX) reports a large enough value (2MB) on photon 3
        # that this should work fine for a VDS inventory entry.
        # But per bug 3288130, not so fine when submitting updates for giant
        # known_hosts files. If that is an issue, run locally on SDDC Mgr
        # where the requests package may be used without firewall issues.
        if inputData:
            if isinstance(inputData, dict):
                inputData = json.dumps(inputData)

            cmd += ["--data", "%s" % inputData,
                    "-H", "Content-Type: application/json"]
        if authToken:
            cmd += ["-H", "Authorization: Bearer %s" % authToken]

        elif useAuth:
            cmd += ["-u", authUser]

        cmd += [url]

        if self.runningOnSddcManager:
            quotedCmd = cmd
        else:
            # we need to add a layer of quoting if running remotely
            # (== nested curl requests).
            quotedCmd = []
            for part in cmd:
                if " " in part:
                    # wrap in single quotes and escape any interior
                    # single quotes
                    quotedCmd.append("'%s'" % part.replace("'", r"\'"))
                else:
                    quotedCmd.append(part)

        outText = self._runSddcMgrCmd(quotedCmd)

        requestDescr = " ".join(cmd)
        outDict = self._outTextToOutDict(outText, requestDescr)

        return outDict

    def _outTextToOutDict(self, outText, requestDescr):
        if outText:
            if isinstance(outText, str):
                # assume already converted and use as-is
                outUtf8 = outText
            else:
                outUtf8 = outText.decode('utf8')

            if (outUtf8.startswith("<html>") and
                    ("404 Not Found" in outUtf8)):
                # signal failure to our caller -- exact key value is important
                # to the edge cleaner's _cleanVcfEdgeCreds(), which looks for
                # an error like
                # {"errorCode":"CREDENTIAL_UNSECURE_API_NOT_SUPPORTED","arguments":[],
                #  "message":"Unsecured credential API not supported. Please use /v1/system/credentials",  # noqa
                #  "referenceToken":"UAEDPN"}
                # But what it really checks for is presence of
                #  "errorCode": "xxx".
                outDict = {"errorCode": "404 Not Found"}
            else:
                try:
                    outDict = json.loads(outText)
                except json.JSONDecodeError:
                    logger.error("Non-empty but ill-formed response to %s" %
                                 requestDescr)
                    logger.error("Response: %s" % outText)
                    outDict = {}
        else:
            outDict = {}

        return outDict

    def runSddcMgrRestGet(self, url, useAuth=False, authToken=None,
                          skipRaiseForStatus=False):
        """Ssh to SDDC Manager and submit a GET request against the supplied URL.
        Results are expected to be in JSON format, which we parse and return
        as a dict.
        """
        return self._runSddcMgrRestQuery(url, "GET", useAuth=useAuth,
                                         authToken=authToken,
                                         skipRaiseForStatus=skipRaiseForStatus)

    def runSddcMgrRestPostString(self, url, payloadString, authToken=None):
        """Ssh to SDDC Manager and submit a POST request against the supplied URL.
        Payload data is assumed to be a single text string, in payloadString.
        """
        return self._runSddcMgrRestQuery(url, "POST", inputData=payloadString,
                                         authToken=authToken)

    def runSddcMgrRestPutString(self, url, payloadString, authToken=None):
        """Ssh to SDDC Manager and submit a PUT request against the supplied URL.
        Payload data is assumed to be a single text string, in payloadString.
        """
        return self._runSddcMgrRestQuery(url, "PUT", inputData=payloadString,
                                         authToken=authToken)

    def runSddcMgrRestPatchString(self, url, payloadString, authToken=None):
        """Ssh to SDDC Manager and submit a PATCH request against the supplied
        URL. Payload data is assumed to be a single text string, in
        payloadString.
        """
        return self._runSddcMgrRestQuery(url, "PATCH", inputData=payloadString,
                                         authToken=authToken)

    def runSddcMgrRestDelete(self, url, useAuth=False, authToken=None):
        """Ssh to SDDC Manager and submit a DELETE request against the
        supplied URL.

        Results, if any, are expected to be in JSON format which we parse and
        return as a dict. If there are no results, we return an empty dict.
        """
        return self._runSddcMgrRestQuery(url, "DELETE", useAuth,
                                         authToken=authToken)

    def __init__(self, sddcMgrAddr=None):
        """If we find we're not running in the SDDC Manager VM, we assume it
        is at the supplied sddcMgrAddr location, and that we have password-free
        ssh access to it as user "vcf".
        """

        if not sddcMgrAddr:
            sddcMgrAddr = os.getenv("SDDC_MANAGER_ADDR", default="10.0.0.4")

        self.runningOnSddcManager = self._areWeRunningOnSddcManager()

        if self.runningOnSddcManager:
            self.sddcMgrAddr = "localhost"
        else:
            self.sddcMgrAddr = sddcMgrAddr
            if not SddcMgrRest.remoteSshMessageShown:
                logger.warning("Using password-free ssh to access SDDC "
                               "Manager @ %s" % self.sddcMgrAddr)
                SddcMgrRest.remoteSshMessageShown = True

        if self.runningOnSddcManager and requestsLibPresent:
            logger.debug("* Using requests lib for SDDC Manager APIs")
        else:
            logger.debug("* Using curl for SDDC Manager APIs")
