Source code for smc.api.web

#  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.
"""
Web actions to SMC

SSL certificates are not verified to the CA authority, need to implement for
urllib3:
https://urllib3.readthedocs.io/en/latest/user-guide.html#ssl
"""
import json
import os.path
import collections
import logging
import requests
from smc.api.exceptions import SMCOperationFailure, SMCConnectionError


logger = logging.getLogger(__name__)


class CacheEncoder(json.JSONEncoder):
    def default(self, o):
        try:
            return o.data
        except AttributeError:
            json.JSONEncoder.default(self, o)


GET = "GET"
PUT = "PUT"
POST = "POST"
DELETE = "DELETE"
OPTIONS = "OPTIONS"


def send_request(user_session, method, request):
    """
    Send request to SMC

    :param Session user_session: session object
    :param str method: method for request
    :param SMCRequest request: request object
    :raises SMCOperationFailure: failure with reason
    :rtype: SMCResult
    """
    if user_session.session:
        session = user_session.session  # requests session
        try:
            method = method.upper() if method else ""

            if method == GET:
                if request.filename:  # File download request
                    return file_download(user_session, request)

                response = session.get(
                    request.href,
                    params=request.params,
                    json=request.json,
                    headers=request.headers,
                    timeout=user_session.timeout,
                )

                response.encoding = "utf-8"

                counters.update(read=1)

                if logger.isEnabledFor(logging.DEBUG):
                    debug(response)

                if response.status_code not in (200, 204, 304):
                    raise SMCOperationFailure(response)

            elif method == POST:
                if request.files:  # File upload request
                    return file_upload(user_session, method, request)

                response = session.post(
                    request.href,
                    data=json.dumps(request.json, cls=CacheEncoder),
                    headers=request.headers,
                    params=request.params,
                )

                response.encoding = "utf-8"

                counters.update(create=1)
                if logger.isEnabledFor(logging.DEBUG):
                    debug(response)

                if response.status_code not in (200, 201, 202, 204):
                    # 202 is asynchronous response with follower link
                    # 204 response contains no data but is OK
                    raise SMCOperationFailure(response)

            elif method == PUT:
                if request.files:  # File upload request
                    return file_upload(user_session, method, request)

                # Etag should be set in request object
                request.headers.update(Etag=request.etag)

                response = session.put(
                    request.href,
                    data=json.dumps(request.json, cls=CacheEncoder),
                    params=request.params,
                    headers=request.headers,
                )

                counters.update(update=1)

                if logger.isEnabledFor(logging.DEBUG):
                    debug(response)

                if response.status_code != 200:
                    raise SMCOperationFailure(response)

            elif method == DELETE:
                response = session.delete(request.href,
                                          params=request.params,
                                          headers=request.headers)

                counters.update(delete=1)

                # Conflict (409) if ETag is not current
                if response.status_code in (409,):
                    req = session.get(request.href)
                    etag = req.headers.get("ETag")
                    response = session.delete(request.href, headers={"if-match": etag})

                response.encoding = "utf-8"

                if logger.isEnabledFor(logging.DEBUG):
                    debug(response)

                if response.status_code not in (200, 204):
                    raise SMCOperationFailure(response)

            elif method == OPTIONS:
                response = session.options(
                    request.href,
                    headers=request.headers,
                    timeout=user_session.timeout,
                )

                response.encoding = "utf-8"

                if logger.isEnabledFor(logging.DEBUG):
                    debug(response)

                if response.status_code not in (200, 204, 304):
                    raise SMCOperationFailure(response)

            else:  # Unsupported method
                return SMCResult(msg="Unsupported method: %s" % method, user_session=user_session)

        except SMCOperationFailure as error:
            if error.code in (401,):
                user_session.refresh()
                return send_request(user_session, method, request)
            raise error
        except requests.exceptions.RequestException as e:
            raise SMCConnectionError(
                "Connection problem to SMC, ensure the API "
                "service is running and host is correct: %s, exiting." % e
            )
        else:
            return SMCResult(response, user_session=user_session)
    else:
        raise SMCConnectionError("No session found. Please login to continue")


def file_download(user_session, request):
    """
    Called when GET request specifies a filename to retrieve.

    :param Session user_session: session object
    :param SMCRequest request: request object
    :raises SMCOperationFailure: failure with reason
    :rtype: SMCResult
    """
    logger.debug("Download file: %s", vars(request))
    response = user_session.session.get(
        request.href, params=request.params, headers=request.headers, stream=True
    )

    if response.status_code == 200:
        logger.debug("Streaming to file... Content length: %s", len(response.content))
        try:
            path = os.path.abspath(request.filename)
            logger.debug("Operation: %s, saving to file: %s", request.href, path)

            with open(path, "wb") as handle:
                for chunk in response.iter_content(chunk_size=1024):
                    if chunk:
                        handle.write(chunk)
                        handle.flush()
        except IOError as e:
            raise IOError("Error attempting to save to file: {}".format(e))

        result = SMCResult(response, user_session=user_session)
        result.content = path
        return result
    else:
        raise SMCOperationFailure(response)


def file_upload(user_session, method, request):
    """
    Perform a file upload PUT/POST to SMC. Request should have the
    files attribute set which will be an open handle to the
    file that will be binary transfer.

    :param Session user_session: session object
    :param str method: method to use, could be put or post
    :param SMCRequest request: request object
    :raises SMCOperationFailure: failure with reason
    :rtype: SMCResult
    """
    logger.debug("Upload: %s", vars(request))
    http_command = getattr(user_session.session, method.lower())

    try:
        response = http_command(request.href, params=request.params, files=request.files)
    except AttributeError:
        raise TypeError("File specified in request was not readable: %s" % request.files)
    else:
        if response.status_code in (200, 201, 202, 204):
            logger.debug("Success sending file in elapsed time: %s", response.elapsed)
            return SMCResult(response, user_session=user_session)

        raise SMCOperationFailure(response)


[docs] class SMCResult(object): """ SMCResult will store the return data for operations performed against the SMC API. If the function returns an SMCResult, the following attributes are set. Note: SMC API will return a list if searches are done and a dict if the attempt is made to get the element directly from href. Instance attributes: :ivar str etag: etag from HTTP GET, representing unique value from server :ivar str href: href of location header if it exists :ivar str content: content if return was application/octet :ivar str msg: error message, if set :ivar int code: http code :ivar dict json: element full json """ def __init__(self, respobj=None, msg=None, user_session=None): self.etag = None self.href = None self.content = None self.msg = msg # Only set in case of error self.code = None self.user_session = user_session self.headers = None self.domain = getattr(user_session, "domain", None) self.json = self._unpack_response(respobj) # list or dict def _unpack_response(self, response): if response: self.code = response.status_code self.href = response.headers.get("location") self.etag = response.headers.get("ETag") self.headers = response.headers content_type = response.headers.get("content-type", "") if content_type == "application/json": try: result = response.json() except ValueError: result = None # Search results return list, direct link fetch # will return a dict or list only if "result" is the unique element returned if result: if len(result) == 1 and "result" in result: self.json = result.get("result") else: self.json = result else: self.json = result # Empty dict return self.json elif "text/plain" in content_type or "application/octet-stream" in content_type: self.content = response.text if response.text else None def __str__(self): sb = [] for key in self.__dict__: sb.append("{key}='{value}'".format(key=key, value=self.__dict__[key])) return ", ".join(sb)
def debug(response): logger.debug("Request method: %s", response.request.method) logger.debug("Request URL: %s", response.url) logger.debug("Request headers:") for k, v in response.request.headers.items(): logger.debug("\t%r: %r", k, v) logger.debug("Request body:") logger.debug("%s", response.request.body) logger.debug("Response status: %s", response.status_code) logger.debug("Response headers:") for k, v in response.headers.items(): logger.debug("\t%r: %r", k, v) logger.debug("Response content:") logger.debug("%s", response.text) counters = collections.Counter({"read": 0, "create": 0, "update": 0, "delete": 0, "cache": 0})