Example #1
0
    def __executeMethod(self):
        """
      Execute the method called, this method is ran in an executor
      We have several try except to catch the different problem which can occur

      - First, the method does not exist => Attribute error, return an error to client
      - second, anything happend during execution => General Exception, send error to client

      .. warning::
        This method is called in an executor, and so cannot use methods like self.write
        See https://www.tornadoweb.org/en/branch5.1/web.html#thread-safety-notes
    """

        # getting method
        try:
            # For compatibility reasons with DISET, the methods are still called ``export_*``
            method = getattr(self, 'export_%s' % self.method)
        except AttributeError as e:
            sLog.error("Invalid method", self.method)
            raise HTTPError(status_code=http_client.NOT_IMPLEMENTED)

        # Decode args
        args_encoded = self.get_body_argument('args', default=encode([]))

        args = decode(args_encoded)[0]
        # Execute
        try:
            self.initializeRequest()
            retVal = method(*args)
        except Exception as e:  # pylint: disable=broad-except
            sLog.exception("Exception serving request",
                           "%s:%s" % (str(e), repr(e)))
            raise HTTPError(http_client.INTERNAL_SERVER_ERROR)

        return retVal
Example #2
0
    def persistOperation(self, opObj, **kwargs):
        """Persist (insert/update) an FTS3Operation object into the db

        :param opObj: instance of FTS3Operation
        """

        # In case someone manually set sourceSEs as a list:
        if isinstance(opObj.sourceSEs, list):
            opObj.sourceSEs = ",".join(opObj.sourceSEs)

        opJSON = encode(opObj)
        return self._getRPC(**kwargs).persistOperation(opJSON)
Example #3
0
    def executeRPC(self, method, *args):
        """
      Calls a remote service

      :param str method: remote procedure name
      :param args: list of arguments
      :returns: decoded response from server, server may return S_OK or S_ERROR
    """
        rpcCall = {'method': method, 'args': encode(args)}
        # Start request
        retVal = self._request(**rpcCall)
        retVal['rpcStub'] = (self._getBaseStub(), method, list(args))
        return retVal
Example #4
0
    def receiveFile(self, destFile, *args):
        """
      Equivalent of :py:meth:`~DIRAC.Core.DISET.TransferClient.TransferClient.receiveFile`

      In practice, it calls the remote method `streamToClient` and stores the raw result in a file

      :param str destFile: path where to store the result
      :param args: list of arguments
      :returns: S_OK/S_ERROR
    """
        rpcCall = {'method': 'streamToClient', 'args': encode(args)}
        # Start request
        retVal = self._request(outputFile=destFile, **rpcCall)
        return retVal
Example #5
0
    def export_getOperation(cls, operationID):
        """Get the FTS3Operation from the database

        :param operationID: ID of the operation

        :return: the FTS3Operation JSON string matching
        """
        getOperation = cls.fts3db.getOperation(operationID)
        if not getOperation["OK"]:
            gLogger.error("getOperation:", getOperation["Message"])
            return getOperation

        getOperation = getOperation["Value"]
        opJSON = encode(getOperation)
        return S_OK(opJSON)
Example #6
0
    def export_getOperationsFromRMSOpID(cls, rmsOpID):
        """Get the FTS3Operation associated to a given rmsOpID

        :param rmsOpID: ID of the operation in the RMS

        :return: JSON encoded list of FTS3Operations
        """
        res = cls.fts3db.getOperationsFromRMSOpID(rmsOpID)
        if not res["OK"]:
            gLogger.error("getOperationsFromRMSOpID", res["Message"])
            return res

        operations = res["Value"]
        opsJSON = encode(operations)

        return S_OK(opsJSON)
Example #7
0
    def export_getNonFinishedOperations(cls, limit, operationAssignmentTag):
        """Get all the FTS3Operations that are missing a callback, i.e.
        in 'Processed' state

        :param limit: max number of operations to retrieve
        :return: json list of FTS3Operation
        """

        res = cls.fts3db.getNonFinishedOperations(limit=limit, operationAssignmentTag=operationAssignmentTag)
        if not res["OK"]:
            return res

        nonFinishedOperations = res["Value"]
        nonFinishedOperationsJSON = encode(nonFinishedOperations)

        return S_OK(nonFinishedOperationsJSON)
Example #8
0
def test_complexBodyPlugin(taskDict, pluginFactor):
    """This test makes sure that we can load the BodyPlugin objects"""

    transBody = DummyBody(factor=pluginFactor)

    # keep the number of tasks for later
    originalNbOfTasks = len(taskDict)

    # Make up the DN and the group
    ownerDN = "DN_owner"
    ownerGroup = "group_owner"

    res = reqTasks.prepareTransformationTasks(encode(transBody),
                                              taskDict,
                                              owner="owner",
                                              ownerGroup=ownerGroup,
                                              ownerDN=ownerDN)

    assert res["OK"], res

    # prepareTransformationTasks can pop tasks if a problem occurs,
    # so check that this did not happen
    assert len(res["Value"]) == originalNbOfTasks

    for _taskID, task in taskDict.items():

        req = task.get("TaskObject")

        # Checks whether we got a Request assigned
        assert req

        # Check that the attributes of the request are what
        # we expect them to be
        assert req.OwnerDN == ownerDN
        assert req.OwnerGroup == ownerGroup

        # DummyBody only creates a single operation.
        # It should be a forward diset, and the
        # argument should be the number of files in the task
        # multiplied by the pluginParam

        assert len(req) == 1

        ops = req[0]
        assert ops.Type == "ForwardDISET"
        assert json.loads(
            ops.Arguments) == pluginFactor * len(task["InputData"])
Example #9
0
    def export_getActiveJobs(cls, limit, lastMonitor, jobAssignmentTag):
        """Get all the FTSJobs that are not in a final state

        :param limit: max number of jobs to retrieve
        :param jobAssignmentTag: tag to put in the DB
        :param lastMonitor: jobs monitored earlier than the given date
        :return: json list of FTS3Job
        """

        res = cls.fts3db.getActiveJobs(limit=limit, lastMonitor=lastMonitor, jobAssignmentTag=jobAssignmentTag)
        if not res["OK"]:
            return res

        activeJobs = res["Value"]
        activeJobsJSON = encode(activeJobs)

        return S_OK(activeJobsJSON)
Example #10
0
    def _request(self, retry=0, outputFile=None, **kwargs):
        """
        Sends the request to server

        :param retry: internal parameters for recursive call. TODO: remove ?
        :param outputFile: (default None) path to a file where to store the received data.
                          If set, the server response will be streamed for optimization
                          purposes, and the response data will not go through the
                          JDecode process
        :param **kwargs: Any argument there is used as a post parameter. They are detailed bellow.
        :param method: (mandatory) name of the distant method
        :param args: (mandatory) json serialized list of argument for the procedure



        :returns: The received data. If outputFile is set, return always S_OK

        """

        # Adding some informations to send
        if self.__extraCredentials:
            kwargs[self.KW_EXTRA_CREDENTIALS] = encode(self.__extraCredentials)
        kwargs["clientVO"] = self.vo
        kwargs["clientSetup"] = self.setup

        # Getting URL
        url = self.__findServiceURL()
        if not url["OK"]:
            return url
        url = url["Value"]

        # Getting CA file (or skip verification)
        verify = not self.kwargs.get(self.KW_SKIP_CA_CHECK)
        if verify:
            if not self.__ca_location:
                self.__ca_location = Locations.getCAsLocation()
                if not self.__ca_location:
                    gLogger.error("No CAs found!")
                    return S_ERROR("No CAs found!")

            verify = self.__ca_location

        # getting certificate
        # Do we use the server certificate ?
        if self.kwargs[self.KW_USE_CERTIFICATES]:
            cert = Locations.getHostCertificateAndKeyLocation()
        elif self.kwargs.get(self.KW_PROXY_STRING):
            tmpHandle, cert = tempfile.mkstemp()
            fp = os.fdopen(tmpHandle, "wb")
            fp.write(self.kwargs[self.KW_PROXY_STRING])
            fp.close()

        # CHRIS 04.02.21
        # TODO: add proxyLocation check ?
        else:
            cert = Locations.getProxyLocation()
            if not cert:
                gLogger.error("No proxy found")
                return S_ERROR("No proxy found")

        # We have a try/except for all the exceptions
        # whose default behavior is to try again,
        # maybe to different server
        try:
            # And we have a second block to handle specific exceptions
            # which makes it not worth retrying
            try:
                rawText = None

                # Default case, just return the result
                if not outputFile:
                    call = requests.post(url, data=kwargs, timeout=self.timeout, verify=verify, cert=cert)
                    # raising the exception for status here
                    # means essentialy that we are losing here the information of what is returned by the server
                    # as error message, since it is not passed to the exception
                    # However, we can store the text and return it raw as an error,
                    # since there is no guarantee that it is any JEncoded text
                    # Note that we would get an exception only if there is an exception on the server side which
                    # is not handled.
                    # Any standard S_ERROR will be transfered as an S_ERROR with a correct code.
                    rawText = call.text
                    call.raise_for_status()
                    return decode(rawText)[0]
                else:
                    # Instruct the server not to encode the response
                    kwargs["rawContent"] = True

                    rawText = None
                    # Stream download
                    # https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow
                    with requests.post(
                        url, data=kwargs, timeout=self.timeout, verify=verify, cert=cert, stream=True
                    ) as r:
                        rawText = r.text
                        r.raise_for_status()

                        with open(outputFile, "wb") as f:
                            for chunk in r.iter_content(4096):
                                # if chunk:  # filter out keep-alive new chuncks
                                f.write(chunk)

                        return S_OK()

            # Some HTTPError are not worth retrying
            except requests.exceptions.HTTPError as e:
                status_code = e.response.status_code
                if status_code == http_client.NOT_IMPLEMENTED:
                    return S_ERROR(errno.ENOSYS, "%s is not implemented" % kwargs.get("method"))
                elif status_code in (http_client.FORBIDDEN, http_client.UNAUTHORIZED):
                    return S_ERROR(errno.EACCES, "No access to %s" % url)

                # if it is something else, retry
                raise

        # Whatever exception we have here, we deem worth retrying
        except Exception as e:
            # CHRIS TODO review this part: retry logic is fishy
            # self.__bannedUrls is emptied in findServiceURLs
            if url not in self.__bannedUrls:
                self.__bannedUrls += [url]
            if retry < self.__nbOfUrls - 1:
                self._request(retry=retry + 1, outputFile=outputFile, **kwargs)

            errStr = "%s: %s" % (str(e), rawText)
            return S_ERROR(errStr)
Example #11
0
 def _getMethodArgs(self, args: tuple, kwargs: dict) -> tuple:
     """Decode target function arguments."""
     args_encoded = self.get_body_argument("args", default=encode([]))
     return (decode(args_encoded)[0], {})
Example #12
0
    def setBody(self, body):
        """check that the body is a string, or using the proper syntax for multiple operations,
        or is a BodyPlugin object

        :param body: transformation body, for example

          .. code :: python

            body = [ ( "ReplicateAndRegister", { "SourceSE":"FOO-SRM", "TargetSE":"BAR-SRM" }),
                     ( "RemoveReplica", { "TargetSE":"FOO-SRM" } ),
                   ]

        :type body: string or list of tuples (or lists) of string and dictionaries or a Body plugin (:py:class:`DIRAC.TransformationSystem.Client.BodyPlugin.BaseBody.BaseBody`)
        :raises TypeError: If the structure is not as expected
        :raises ValueError: If unknown attribute for the :class:`~DIRAC.RequestManagementSystem.Client.Operation.Operation`
                            is used
        :returns: S_OK, S_ERROR
        """
        self.item_called = "Body"

        # Simple single operation body case
        if isinstance(body, six.string_types):
            return self.__setParam(body)

        # BodyPlugin case
        elif isinstance(body, BaseBody):
            return self.__setParam(encode(body))

        if not isinstance(body, (list, tuple)):
            raise TypeError("Expected list or string, but %r is %s" %
                            (body, type(body)))

        # MultiOperation body case
        for tup in body:
            if not isinstance(tup, (tuple, list)):
                raise TypeError("Expected tuple or list, but %r is %s" %
                                (tup, type(tup)))
            if len(tup) != 2:
                raise TypeError("Expected 2-tuple, but %r is length %d" %
                                (tup, len(tup)))
            if not isinstance(tup[0], six.string_types):
                raise TypeError(
                    "Expected string, but first entry in tuple %r is %s" %
                    (tup, type(tup[0])))
            if not isinstance(tup[1], dict):
                raise TypeError(
                    "Expected dictionary, but second entry in tuple %r is %s" %
                    (tup, type(tup[0])))
            for par, val in tup[1].items():
                if not isinstance(par, six.string_types):
                    raise TypeError(
                        "Expected string, but key in dictionary %r is %s" %
                        (par, type(par)))
                if par not in Operation.ATTRIBUTE_NAMES:
                    raise ValueError("Unknown attribute for Operation: %s" %
                                     par)
                if not isinstance(
                        val, six.string_types + six.integer_types +
                    (float, list, tuple, dict)):
                    raise TypeError("Cannot encode %r, in json" % (val))
        return self.__setParam(json.dumps(body))
Example #13
0
    def post(self):  # pylint: disable=arguments-differ
        """
      Method to handle incoming ``POST`` requests.
      Note that all the arguments are already prepared in the :py:meth:`.prepare`
      method.

      The ``POST`` arguments expected are:

      * ``method``: name of the method to call
      * ``args``: JSON encoded arguments for the method
      * ``extraCredentials``: (optional) Extra informations to authenticate client
      * ``rawContent``: (optionnal, default False) If set to True, return the raw output
        of the method called.

      If ``rawContent`` was requested by the client, the ``Content-Type``
      is ``application/octet-stream``, otherwise we set it to ``application/json``
      and JEncode retVal.

      If ``retVal`` is a dictionary that contains a ``Callstack`` item,
      it is removed, not to leak internal information.


      Example of call using ``requests``::

        In [20]: url = 'https://*****:*****@cern.ch',
            u'group': u'dirac_user',
            u'identity': u'/C=ch/O=DIRAC/OU=DIRAC CI/CN=ciuser/[email protected]',
            u'isLimitedProxy': False,
            u'isProxy': True,
            u'issuer': u'/C=ch/O=DIRAC/OU=DIRAC CI/CN=ciuser/[email protected]',
            u'properties': [u'NormalUser'],
            u'secondsLeft': 85441,
            u'subject': u'/C=ch/O=DIRAC/OU=DIRAC CI/CN=ciuser/[email protected]/CN=2409820262',
            u'username': u'adminusername',
            u'validDN': False,
            u'validGroup': False}}
    """

        sLog.notice(
            "Incoming request",
            "%s /%s: %s" % (self.srv_getFormattedRemoteCredentials(),
                            self._serviceName, self.method))

        # Execute the method in an executor (basically a separate thread)
        # Because of that, we cannot calls certain methods like `self.write`
        # in __executeMethod. This is because these methods are not threadsafe
        # https://www.tornadoweb.org/en/branch5.1/web.html#thread-safety-notes
        # However, we can still rely on instance attributes to store what should
        # be sent back (reminder: there is an instance
        # of this class created for each request)
        retVal = yield IOLoop.current().run_in_executor(
            None, self.__executeMethod)

        # retVal is :py:class:`tornado.concurrent.Future`
        self.result = retVal.result()

        # Here it is safe to write back to the client, because we are not
        # in a thread anymore

        # If set to true, do not JEncode the return of the RPC call
        # This is basically only used for file download through
        # the 'streamToClient' method.
        rawContent = self.get_argument('rawContent', default=False)

        if rawContent:
            # See 4.5.1 http://www.rfc-editor.org/rfc/rfc2046.txt
            self.set_header("Content-Type", "application/octet-stream")
            result = self.result
        else:
            self.set_header("Content-Type", "application/json")
            result = encode(self.result)

        self.write(result)
        self.finish()