def get_access_token(username=None, password=None, ca_certs=None): """Get a goauth access token from nexus. Uses basic auth with the user's username and password to authenticate to nexus. If the username or password are not passed, they are prompted for on stdin. @param username: Globus Online username to authenticate as, or None to prompt on stdin. @param password: Globus Online password to authenticate with, or None to prompt on stdin (with echo disabled for security). @param ca_certs: Path to a ca certificate to verify nexus, or None to use the default CA included in the package. @return: GOAuthResult object. Most applications will only care about the token field, but username/password may be useful for caching authentication information when using the prompt. """ if ca_certs is None: from globusonline.transfer.api_client import get_ca ca_certs = get_ca(HOST) if username is None: print "Globus Online Username: "******"Globus Online Password: "******"%s:%s" % (username, password)) headers = { "Content-type": "application/json; charset=UTF-8", "Hostname": HOST, "Accept": "application/json; charset=UTF-8", "Authorization": "Basic %s" % basic_auth } c = VerifiedHTTPSConnection(HOST, PORT, ca_certs=ca_certs) c.request("GET", GOAUTH_PATH, headers=headers) response = c.getresponse() if response.status == 403: raise GOCredentialsError() elif response.status > 299 or response.status < 200: raise GOAuthError("error response: %d %s" % (response.status, response.reason)) data = json.loads(response.read()) token = data.get("access_token") if token is None: raise GOAuthError("no token in response") return GOAuthResult(username, password, token)
def get_access_token(username=None, password=None, ca_certs=None): """Get a goauth access token from nexus. Uses basic auth with the user's username and password to authenticate to nexus. If the username or password are not passed, they are prompted for on stdin. @param username: Globus Online username to authenticate as, or None to prompt on stdin. @param password: Globus Online password to authenticate with, or None to prompt on stdin (with echo disabled for security). @param ca_certs: Path to a ca certificate to verify nexus, or None to use the default CA included in the package. @return: GOAuthResult object. Most applications will only care about the token field, but username/password may be useful for caching authentication information when using the prompt. """ if ca_certs is None: from globusonline.transfer.api_client import get_ca ca_certs = get_ca(HOST) if username is None: print "Globus Online Username: "******"Globus Online Password: "******"%s:%s" % (username, password)) headers = { "Content-type": "application/json; charset=UTF-8", "Hostname": HOST, "Accept": "application/json; charset=UTF-8", "Authorization": "Basic %s" % basic_auth, } c = VerifiedHTTPSConnection(HOST, PORT, ca_certs=ca_certs) c.request("GET", GOAUTH_PATH, headers=headers) response = c.getresponse() if response.status == 403: raise GOCredentialsError() elif response.status > 299 or response.status < 200: raise GOAuthError("error response: %d %s" % (response.status, response.reason)) data = json.loads(response.read()) token = data.get("access_token") if token is None: raise GOAuthError("no token in response") return GOAuthResult(username, password, token)
def get_go_auth(ca_certs, username=None, password=None): """ POST the login form to www.globusonline.org to get the cookie, prompting for username and password on stdin if they were not passed as parameters. @return: a GOAuthResult instance. The cookie is what most clients will be interested in, but if the username is not passed as a parameter the caller may need that as well, and may want to cache the password. """ if ca_certs is None: from globusonline.transfer.api_client import get_ca ca_certs = get_ca(HOST) if username is None: print "GO Username: "******"GO Password: "******"Content-type": "application/x-www-form-urlencoded", "Hostname": HOST } c = VerifiedHTTPSConnection(HOST, PORT, ca_certs=ca_certs) body = urllib.urlencode(dict(username=username, password=password)) c.request("POST", PATH, body=body, headers=headers) response = c.getresponse() set_cookie_header = response.getheader("set-cookie") if not set_cookie_header: # TODO: more appropriate exc type raise ValueError("No cookies received") cookies = BaseCookie(set_cookie_header) morsel = cookies.get("saml") if not morsel: raise ValueError("No saml cookie received") return GOAuthResult(username, password, morsel.coded_value)
def get_go_auth(ca_certs, username=None, password=None): """ POST the login form to www.globusonline.org to get the cookie, prompting for username and password on stdin if they were not passed as parameters. @return: a GOAuthResult instance. The cookie is what most clients will be interested in, but if the username is not passed as a parameter the caller may need that as well, and may want to cache the password. """ if username is None: print "GO Username: "******"GO Password: "******"Content-type": "application/x-www-form-urlencoded", "Hostname": HOST } c = VerifiedHTTPSConnection(HOST, PORT, ca_certs=ca_certs) body = urllib.urlencode(dict(username=username, password=password)) c.request("POST", PATH, body=body, headers=headers) response = c.getresponse() set_cookie_header = response.getheader("set-cookie") if not set_cookie_header: # TODO: more appropriate exc type raise ValueError("No cookies received") cookies = BaseCookie(set_cookie_header) morsel = cookies.get("saml") if not morsel: raise ValueError("No saml cookie received") return GOAuthResult(username, password, morsel.coded_value)
class TransferAPIClient(object): """ Maintains a connection to the server as a specific user. Not thread safe. Uses the JSON representations. Convenience api methods return a triple: (status_code, status_message, data) data is either the JSON response loaded as a python dictionary, or None if the reponse was empty, or a conveninience wrapper around the JSON data if the data itself is hard to use directly. Endpoint names can be full canonical names of the form ausername#epname, or simply epname, in which case the API looks at the logged in user's endpoints. """ def __init__(self, username, server_ca_file=None, cert_file=None, key_file=None, base_url=DEFAULT_BASE_URL, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, httplib_debuglevel=0, max_attempts=1, header_auth=None, goauth=None): """ Initialize a client with the client credential and optional alternate base URL. For authentication, use either x509 or goauth; header_auth is deprecated, and cookie auth is no longer supported. x509 requires configuring your Globus Online account with the x509 certificate and having the private key available. Requires @cert_file and @key_file, or just one if both are in the same file. goauth requires fetching an access token from nexus, which supports several authentication methods including basic auth - see the goauth module and the nexus documentation. @param username: username to connect to the service with. @param server_ca_file: path to file containing one or more x509 certificates, used to verify the server certificate. If not specified tries to choose the appropriate CA based on the hostname in base_url. @param cert_file: path to file containing the x509 client certificate for authentication. @param key_file: path to file containg the RSA key for client authentication. If blank and cert_file passed, uses cert_file. @param header_auth: contents of the saml cookie, but used for header authentication, not cookie auth. @param goauth: goauth access token retrieved from nexus. @param base_url: optionally specify an alternate base url, if testing out an unreleased or alternatively hosted version of the API. @param timeout: timeout to set on the underlying TCP socket. @param max_attempts: Retry every API call on network errors and ServiceUnavailable up to this many times. Sleeps for 30 seconds between each attempt. Note that a socket timeout will be treated as a network error and retried. When max_attempts is exceeded, the exception from the last attempt will be raised. max_attempts=1 implies no retrying. """ if server_ca_file is None: server_ca_file = get_ca(base_url) if server_ca_file is None: raise InterfaceError("no CA found for base URL '%s'" % base_url) if not os.path.isfile(server_ca_file): raise InterfaceError("server_ca_file not found: '%s'" % server_ca_file) self.headers = {} if header_auth: if goauth or cert_file or key_file: raise InterfaceError("pass only one auth method") elif goauth: if cert_file or key_file: raise InterfaceError("pass only one auth method") elif cert_file or key_file: if not key_file: key_file = cert_file if not cert_file: cert_file = key_file if not os.path.isfile(cert_file): raise InterfaceError("cert_file not found: %s" % cert_file) if not os.path.isfile(key_file): raise InterfaceError("key_file not found: %s" % key_file) self.headers["X-Transfer-API-X509-User"] = username else: raise InterfaceError("pass one auth method") if max_attempts is not None: max_attempts = int(max_attempts) if max_attempts < 1: raise InterfaceError( "max_attempts must be None or a positive integer") self.max_attempts = max_attempts self.goauth = goauth self.cert_file = cert_file self.key_file = key_file self.header_auth = header_auth self.username = username self.server_ca_file = server_ca_file self.httplib_debuglevel = httplib_debuglevel self.base_url = base_url self.host, self.port = _get_host_port(base_url) self.timeout = timeout self.print_request = False self.print_response = False self.c = None self.user_agent = "Python-httplib/%s (%s)" \ % (platform.python_version(), platform.system()) self.client_info = "globusonline.transfer.api_client/%s" % __version__ def connect(self): """ Create an HTTPS connection to the server. Run automatically by request methods. """ kwargs = dict(ca_certs=self.server_ca_file, strict=False, timeout=self.timeout) if self.cert_file: kwargs["cert_file"] = self.cert_file kwargs["key_file"] = self.key_file self.c = VerifiedHTTPSConnection(self.host, self.port, **kwargs) self.c.set_debuglevel(self.httplib_debuglevel) def set_http_connection_debug(self, value): """ Turn debugging of the underlying VerifiedHTTPSConnection on or off. Note: this may print sensative information, like auth tokens, to standard out. """ if value: level = 1 else: level = 0 self.httplib_debuglevel = level if self.c: self.c.set_debuglevel(level) def set_debug_print(self, print_request, print_response): self.print_request = print_request self.print_response = print_response def close(self): """ Close the wrapped VerifiedHTTPSConnection. """ if self.c: self.c.close() self.c = None def _request(self, method, path, body=None, content_type=None): if not path.startswith("/"): path = "/" + path url = self.base_url + path headers = self.headers.copy() if content_type: headers["Content-Type"] = content_type if self.print_request: print print ">>>REQUEST>>>:" print "%s %s" % (method, url) for h in headers.iteritems(): print "%s: %s" % h print if body: print body if self.goauth: headers["Authorization"] = "Globus-Goauthtoken %s" % self.goauth elif self.header_auth: headers["Authorization"] = "Bearer %s" % self.header_auth headers["User-Agent"] = self.user_agent headers["X-Transfer-API-Client"] = self.client_info def do_request(): if self.c is None: self.connect() self.c.request(method, url, body=body, headers=headers) r = self.c.getresponse() response_body = r.read() return r, response_body for attempt in xrange(self.max_attempts): r = None try: try: r, response_body = do_request() except BadStatusLine: # This happens when the connection is closed by the server # in between request, which is very likely when using # interactively, in a client that waits for user input # between requests, or after a retry wait. This does not # count as an attempt - it just means the old connection # has gone stale and we need a new one. # TODO: find a more elegant way to re-use the connection # on closely spaced requests. Can we tell that the # connection is dead without making a request? self.close() r, response_body = do_request() except ssl.SSLError: # This probably has to do with failed authentication, so # retrying is not useful. self.close() raise except socket.error: # Network error. If the last attempt failed, raise, # otherwise do nothing and go on to next attempt. self.close() if attempt == self.max_attempts - 1: raise # Check for 503 ServiceUnavailable, which is treated just like # network errors. if (r is not None and r.status == 503 and attempt < self.max_attempts - 1): # Force sleep below and continue loop, unless we are on # the last attempt in which case skip this and return # the 503 error. self.close() r = None if r is not None: break else: time.sleep(RETRY_WAIT_SECONDS) if self.print_response: print print "<<<RESPONSE<<<:" print r.status, r.reason for h in r.getheaders(): print "%s: %s" % h print print response_body return r, response_body def _request_json(self, method, path, body=None, content_type=None): """ Make a request and load the response body as JSON, if the response is not empty. """ r, response_body = self._request(method, path, body, content_type) if response_body: try: data = json.loads(response_body) except Exception as e: raise InterfaceError( ("Unable to parse JSON in response: err='%s', " +"body len='%d', status='%d %s'") % (e, len(response_body), r.status, r.reason)) else: data = None return api_result(r, data) # Generic API methods: def get(self, path): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self._request_json("GET", path) def put(self, path, body): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self._request_json("PUT", path, body, "application/json") def post(self, path, body): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self._request_json("POST", path, body, "application/json") def _delete(self, path): """ @return: (status_code, status_reason, data) @raise TransferAPIError TODO: this conflicts with the method for submitting delete jobs, so it's named inconsistently from the other HTTP method functions. Maybe they should all be _ prefixed? """ return self._request_json("DELETE", path) # Convenience API methods: def tasksummary(self, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/tasksummary" + encode_qs(kw)) def task_list(self, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/task_list" + encode_qs(kw)) def task(self, task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/task/%s" % task_id + encode_qs(kw)) def task_update(self, task_id, task_data, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.post("/task/%s" % task_id + encode_qs(kw)) def task_cancel(self, task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.post("/task/%s/cancel" % task_id + encode_qs(kw), body=None) def subtask_list(self, parent_task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/task/%s/subtask_list" % parent_task_id + encode_qs(kw)) def subtask(self, task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/subtask/%s" % task_id + encode_qs(kw)) def task_event_list(self, parent_task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/task/%s/event_list" % parent_task_id + encode_qs(kw)) def subtask_event_list(self, task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/subtask/%s/event_list" % task_id + encode_qs(kw)) def endpoint_list(self, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/endpoint_list" + encode_qs(kw)) def endpoint(self, endpoint_name, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get(_endpoint_path(endpoint_name) + encode_qs(kw)) def endpoint_activation_requirements(self, endpoint_name, **kw): """ @return: (code, reason, data), where data is an ActivationRequirements instance instead of a plain dictionary. @raise TransferAPIError """ code, reason, data = self.get(_endpoint_path(endpoint_name, "/activation_requirements") + encode_qs(kw)) if code == 200 and data: data = ActivationRequirementList(data) return code, reason, data def endpoint_activate(self, endpoint_name, filled_requirements, if_expires_in="", timeout=30): """ @param endpoint_name: partial or canonical name of endpoint to activate. @param filled_requirements: ActivationRequirementList instance with required values set for one activation type. @type filled_requirements: ActivationRequirementList @param if_expires_in: don't re-activate endpoint if it doesn't expire for this many minutes. If not passed, always activate, even if already activated. @param timeout: timeout in seconds to attempt contacting external servers to get the credential. @return: (code, reason, data), where data is an ActivationRequirements instance. @raise TransferAPIError """ if filled_requirements: body = json.dumps(filled_requirements.json_data) else: raise InterfaceError("Use autoactivate instead; using activate " "with an empty request body to auto activate is " "deprecated.") # Note: blank query parameters are ignored, so we can pass blank # values to use the default behavior. qs = encode_qs(dict(if_expires_in=str(if_expires_in), timeout=str(timeout))) code, reason, data = self.post( _endpoint_path(endpoint_name, "/activate" + qs), body=body) if code == 200 and data: data = ActivationRequirementList(data) return code, reason, data def endpoint_autoactivate(self, endpoint_name, if_expires_in="", timeout=30): """ @param endpoint_name: partial or canonical name of endpoint to activate. @param if_expires_in: don't re-activate endpoint if it doesn't expire for this many minutes. If not passed, always activate, even if already activated. @param timeout: timeout in seconds to attempt contacting external servers to get the credential. @return: (code, reason, data), where data is an ActivationRequirements instance. @raise TransferAPIError """ # Note: blank query parameters are ignored, so we can pass blank # values to use the default behavior. qs = encode_qs(dict(if_expires_in=str(if_expires_in), timeout=str(timeout))) code, reason, data = self.post( _endpoint_path(endpoint_name, "/autoactivate" + qs), body=None) if code == 200 and data: data = ActivationRequirementList(data) return code, reason, data def endpoint_deactivate(self, endpoint_name, **kw): """ @param endpoint_name: partial or canonical name of endpoint to activate. @return: (code, reason, data) @raise TransferAPIError """ # Note: blank query parameters are ignored, so we can pass blank # values to use the default behavior. code, reason, data = self.post( _endpoint_path(endpoint_name, "/deactivate") + encode_qs(kw), body=None) return code, reason, data def endpoint_ls(self, endpoint_name, path="", **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ kw["path"] = path return self.get(_endpoint_path(endpoint_name, "/ls") + encode_qs(kw)) def endpoint_mkdir(self, endpoint_name, path, **kw): data = dict(path=path, DATA_TYPE="mkdir") return self.post(_endpoint_path(endpoint_name, "/mkdir") + encode_qs(kw), json.dumps(data)) def endpoint_create(self, endpoint_name, hostname=None, description="", scheme="gsiftp", port=2811, subject=None, myproxy_server=None, myproxy_dn=None, public=False, is_globus_connect=False, default_directory=None, oauth_server=None): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ data = { "DATA_TYPE": "endpoint", "myproxy_server": myproxy_server, "myproxy_dn": myproxy_dn, "description": description, "canonical_name": endpoint_name, "public": public, "is_globus_connect": is_globus_connect, "default_directory": default_directory, "oauth_server": oauth_server, } if not is_globus_connect: data["DATA"] = [dict(DATA_TYPE="server", hostname=hostname, scheme=scheme, port=port, subject=subject)] return self.post("/endpoint", json.dumps(data)) def endpoint_update(self, endpoint_name, endpoint_data): """ Call endpoint to get the data, modify as needed, then pass the modified data to this method. @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.put(_endpoint_path(endpoint_name), json.dumps(endpoint_data)) def endpoint_rename(self, endpoint_name, new_endpoint_name): _, _, endpoint_data = self.endpoint(endpoint_name) endpoint_data["canonical_name"] = new_endpoint_name del endpoint_data["name"] return self.endpoint_update(endpoint_name, endpoint_data) def endpoint_delete(self, endpoint_name): """ Delete the specified endpoint. Existing transfers using the endpoint will continue to work, but you will not be able to use the endpoint in any new operations, and it will be gone from the endpoint_list. @return: (status_code, status_reason, data) @raise TransferAPIError """ return self._delete(_endpoint_path(endpoint_name)) def submission_id(self): """ @return: (status_code, status_reason, data) @raise: TransferAPIError """ return self.get("/submission_id") # backward compatibility transfer_submission_id = submission_id def transfer(self, transfer): """ @type transfer: Transfer object @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.post("/transfer", transfer.as_json()) def delete(self, delete): """ @type delete: Delete object @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.post("/delete", delete.as_json())
class TransferAPIClient(object): """ Maintains a connection to the server as a specific users. Not thread safe. Uses the JSON representations. Convenience api methods return a triple: (status_code, status_message, data) data is either the JSON response loaded as a python dictionary, or None if the reponse was empty, or a conveninience wrapper around the JSON data if the data itself is hard to use directly. Endpoint names can be full canonical names of the form ausername#epname, or simply epname, in which case the API looks at the logged in user's endpoints. """ def __init__(self, username, server_ca_file, cert_file=None, key_file=None, saml_cookie=None, base_url=DEFAULT_BASE_URL, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, httplib_debuglevel=0, max_attempts=1): """ Initialize a client with the client credential and optional alternate base URL. The main authentication method is using an x509 certificate, in which case cert_file and key_file are required. A signed cookie can also be used, but that is mainly used for internal testing; however it is possible to copy the contents of the 'saml' cookie from the browser after signing in to www.globusonline.org and use that, until it expires. @param username: username to connect to the service with. @param server_ca_file: path to file containing one or more x509 certificates, used to verify the server certificate. @param cert_file: path to file containing the x509 client certificate for authentication. @param key_file: path to file containg the RSA key for client authentication. If blank and cert_file passed, uses cert_file. @param saml_cookie: contents of 'saml' cookie from www.globusonline.org. @param base_url: optionally specify an alternate base url, if testing out an unreleased or alternatively hosted version of the API. @param timeout: timeout to set on the underlying TCP socket. @param max_attempts: Retry every API call on network errors and ServiceUnavailable up to this many times. Sleeps for 30 seconds between each attempt. Note that a socket timeout will be treated as a network error and retried. When max_attempts is exceeded, the exception from the last attempt will be raised. max_attempts=1 implies no retrying. """ if not os.path.isfile(server_ca_file): raise InterfaceError("server_ca_file not found: %s" % server_ca_file) if saml_cookie and (cert_file or key_file): raise InterfaceError("pass either cooie or cert and key" " files, not both.") if cert_file: if not os.path.isfile(cert_file): raise InterfaceError("cert_file not found: %s" % cert_file) if not key_file: key_file = cert_file else: if not os.path.isfile(key_file): raise InterfaceError("key_file not found: %s" % key_file) if max_attempts is not None: max_attempts = int(max_attempts) if max_attempts < 1: raise InterfaceError( "max_attempts must be None or a positive integer") self.max_attempts = max_attempts self.saml_cookie = saml_cookie self.cert_file = cert_file self.key_file = key_file self.username = username self.server_ca_file = server_ca_file self.httplib_debuglevel = httplib_debuglevel self.base_url = base_url self.host, self.port = _get_host_port(base_url) self.timeout = timeout if saml_cookie: unquoted = urllib.unquote(saml_cookie) if unquoted.find("un=%s|" % username) == -1: raise InterfaceError("saml cookie username does not match " "username argument") self.headers = {} else: self.headers = {"X-Transfer-API-X509-User": username} self.print_request = False self.print_response = False self.c = None self.user_agent = "Python-httplib/%s (%s)" \ % (platform.python_version(), platform.system()) self.client_info = "globusonline.transfer.api_client/%s" % __version__ def connect(self): """ Create an HTTPS connection to the server. Run automatically by request methods. """ kwargs = dict(ca_certs=self.server_ca_file, strict=False, timeout=self.timeout) if self.cert_file: kwargs["cert_file"] = self.cert_file kwargs["key_file"] = self.key_file self.c = VerifiedHTTPSConnection(self.host, self.port, **kwargs) self.c.set_debuglevel(self.httplib_debuglevel) def set_http_connection_debug(self, value): """ Turn debugging of the underlying VerifiedHTTPSConnection on or off. Note: this may print sensative information, like saml cookie, to standard out. """ if value: level = 1 else: level = 0 self.httplib_debuglevel = level if self.c: self.c.set_debuglevel(level) def set_debug_print(self, print_request, print_response): self.print_request = print_request self.print_response = print_response def close(self): """ Close the wrapped VerifiedHTTPSConnection. """ if self.c: self.c.close() self.c = None def _request(self, method, path, body=None, content_type=None): if not path.startswith("/"): path = "/" + path url = self.base_url + path headers = self.headers.copy() if content_type: headers["Content-Type"] = content_type if self.print_request: print print ">>>REQUEST>>>:" print "%s %s" % (method, url) if self.saml_cookie: # Should be enough to show the username and still hide the # signature. headers["Cookie"] = "saml=%s..." % self.saml_cookie[:31] for h in headers.iteritems(): print "%s: %s" % h print if body: print body if self.saml_cookie: headers["Cookie"] = "saml=%s" % self.saml_cookie headers["User-Agent"] = self.user_agent headers["X-Transfer-API-Client"] = self.client_info def do_request(): if self.c is None: self.connect() self.c.request(method, url, body=body, headers=headers) r = self.c.getresponse() response_body = r.read() return r, response_body for attempt in xrange(self.max_attempts): #print "attempt:", attempt r = None try: try: r, response_body = do_request() except BadStatusLine: # This happens when the connection is closed by the server # in between request, which is very likely when using # interactively, in a client that waits for user input # between requests, or after a retry wait. This does not # count as an attempt - it just means the old connection # has gone stale and we need a new one. # TODO: find a more elegant way to re-use the connection # on closely spaced requests. Can we tell that the # connection is dead without making a request? self.close() r, response_body = do_request() except ssl.SSLError: # This probably has to do with failed authentication, so # retrying is not useful. traceback.print_exc() self.close() raise except socket.error: # Network error. If the last attempt failed, raise, # otherwise do nothing and go on to next attempt. traceback.print_exc() self.close() if attempt == self.max_attempts - 1: raise # Check for ServiceUnavailable, which is treated just like # network errors. if r is not None and attempt < self.max_attempts - 1: error_code = r.getheader("X-Transfer-API-Error", None) if error_code is not None \ and error_code.startswith("ServiceUnavailable"): # Force sleep below and continue loop self.close() r = None if r is not None: break else: time.sleep(RETRY_WAIT_SECONDS) if self.print_response: print print "<<<RESPONSE<<<:" print r.status, r.reason for h in r.getheaders(): print "%s: %s" % h print print response_body return r, response_body def _request_json(self, method, path, body=None, content_type=None): """ Make a request and load the response body as JSON, if the response is not empty. """ r, response_body = self._request(method, path, body, content_type) if response_body: try: data = json.loads(response_body) except Exception as e: raise InterfaceError("Unable to parse JSON in response: " + str(e)) else: data = None return api_result(r, data) # Generic API methods: def get(self, path): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self._request_json("GET", path) def put(self, path, body): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self._request_json("PUT", path, body, "application/json") def post(self, path, body): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self._request_json("POST", path, body, "application/json") def _delete(self, path): """ @return: (status_code, status_reason, data) @raise TransferAPIError TODO: this conflicts with the method for submitting delete jobs, so it's named inconsistently from the other HTTP method functions. Maybe they should all be _ prefixed? """ return self._request_json("DELETE", path) # Convenience API methods: def tasksummary(self, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/tasksummary" + encode_qs(kw)) def task_list(self, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/task_list" + encode_qs(kw)) def task(self, task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/task/%s" % task_id + encode_qs(kw)) def task_update(self, task_id, task_data): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.post("/task/%s" % task_id + encode_qs(kw)) def task_cancel(self, task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.post("/task/%s/cancel" % task_id + encode_qs(kw), body=None) def subtask_list(self, parent_task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/task/%s/subtask_list" % parent_task_id + encode_qs(kw)) def subtask(self, task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/subtask/%s" % task_id + encode_qs(kw)) def task_event_list(self, parent_task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/task/%s/event_list" % parent_task_id + encode_qs(kw)) def subtask_event_list(self, task_id, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/subtask/%s/event_list" % task_id + encode_qs(kw)) def endpoint_list(self, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get("/endpoint_list" + encode_qs(kw)) def endpoint(self, endpoint_name, **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.get(_endpoint_path(endpoint_name) + encode_qs(kw)) def endpoint_activation_requirements(self, endpoint_name, **kw): """ @return: (code, reason, data), where data is an ActivationRequirements instance instead of a plain dictionary. @raise TransferAPIError """ code, reason, data = self.get( _endpoint_path(endpoint_name, "/activation_requirements") + encode_qs(kw)) if code == 200 and data: data = ActivationRequirementList(data) return code, reason, data def endpoint_activate(self, endpoint_name, filled_requirements, if_expires_in="", timeout=30): """ @param endpoint_name: partial or canonical name of endpoint to activate. @param filled_requirements: ActivationRequirementList instance, or None to attempt auto-activation. @type filled_requirements: ActivationRequirementList @param if_expires_in: don't re-activate endpoint if it doesn't expire for this many minutes. If not passed, always activate, even if already activated. @param timeout: timeout in seconds to attempt contacting external servers to get the credential. @return: (code, reason, data), where data is an ActivationRequirements instance. @raise TransferAPIError """ if filled_requirements: body = json.dumps(filled_requirements.json_data) else: raise InterfaceError( "Use autoactivate instead; using activate " "with an empty request body to auto activate is " "deprecated.") # Note: blank query parameters are ignored, so we can pass blank # values to use the default behavior. qs = encode_qs( dict(if_expires_in=str(if_expires_in), timeout=str(timeout))) code, reason, data = self.post(_endpoint_path(endpoint_name, "/activate" + qs), body=body) if code == 200 and data: data = ActivationRequirementList(data) return code, reason, data def endpoint_autoactivate(self, endpoint_name, if_expires_in="", timeout=30): """ @param endpoint_name: partial or canonical name of endpoint to activate. @param if_expires_in: don't re-activate endpoint if it doesn't expire for this many minutes. If not passed, always activate, even if already activated. @param timeout: timeout in seconds to attempt contacting external servers to get the credential. @return: (code, reason, data), where data is an ActivationRequirements instance. @raise TransferAPIError """ # Note: blank query parameters are ignored, so we can pass blank # values to use the default behavior. qs = encode_qs( dict(if_expires_in=str(if_expires_in), timeout=str(timeout))) code, reason, data = self.post(_endpoint_path(endpoint_name, "/autoactivate" + qs), body=None) if code == 200 and data: data = ActivationRequirementList(data) return code, reason, data def endpoint_deactivate(self, endpoint_name, **kw): """ @param endpoint_name: partial or canonical name of endpoint to activate. @return: (code, reason, data) @raise TransferAPIError """ # Note: blank query parameters are ignored, so we can pass blank # values to use the default behavior. code, reason, data = self.post( _endpoint_path(endpoint_name, "/deactivate") + encode_qs(kw), body=None) return code, reason, data def endpoint_ls(self, endpoint_name, path="", **kw): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ kw["path"] = path return self.get(_endpoint_path(endpoint_name, "/ls") + encode_qs(kw)) def endpoint_mkdir(self, endpoint_name, path, **kw): data = dict(path=path, DATA_TYPE="mkdir") return self.post( _endpoint_path(endpoint_name, "/mkdir") + encode_qs(kw), json.dumps(data)) def endpoint_create(self, endpoint_name, hostname=None, description="", scheme="gsiftp", port=2811, subject=None, myproxy_server=None, public=False, is_globus_connect=False): """ @return: (status_code, status_reason, data) @raise TransferAPIError """ data = { "DATA_TYPE": "endpoint", "myproxy_server": myproxy_server, "description": description, "canonical_name": endpoint_name, "public": public, "is_globus_connect": is_globus_connect, } if not is_globus_connect: data["DATA"] = [ dict(DATA_TYPE="server", hostname=hostname, scheme=scheme, port=port, subject=subject) ] return self.post("/endpoint", json.dumps(data)) def endpoint_update(self, endpoint_name, endpoint_data): """ Call endpoint to get the data, modify as needed, then pass the modified data to this method. @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.put(_endpoint_path(endpoint_name), json.dumps(endpoint_data)) def endpoint_rename(self, endpoint_name, new_endpoint_name): _, _, endpoint_data = self.endpoint(endpoint_name) endpoint_data["canonical_name"] = new_endpoint_name del endpoint_data["name"] return self.endpoint_update(endpoint_name, endpoint_data) def endpoint_delete(self, endpoint_name): """ Delete the specified endpoint. Existing transfers using the endpoint will continue to work, but you will not be able to use the endpoint in any new operations, and it will be gone from the endpoint_list. @return: (status_code, status_reason, data) @raise TransferAPIError """ return self._delete(_endpoint_path(endpoint_name)) def submission_id(self): """ @return: (status_code, status_reason, data) @raise: TransferAPIError """ return self.get("/submission_id") # backward compatibility transfer_submission_id = submission_id def transfer(self, transfer): """ @type transfer: Transfer object @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.post("/transfer", transfer.as_json()) def delete(self, delete): """ @type delete: Delete object @return: (status_code, status_reason, data) @raise TransferAPIError """ return self.post("/delete", delete.as_json())