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
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)
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
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
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)
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)
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)
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"])
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)
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)
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], {})
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))
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()