# This file is part of BOINC. # http://boinc.berkeley.edu # Copyright (C) 2020 University of California # # BOINC is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License # as published by the Free Software Foundation, # either version 3 of the License, or (at your option) any later version. # # BOINC is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with BOINC. If not, see . # Python bindings of the remote job submission and file management APIs # See https://boinc.berkeley.edu/trac/wiki/RemoteJobs#Pythonbinding import urllib import urllib2 import copy import xml.etree.ElementTree as ET import requests # you'll need to "pip install requests" import hashlib # describes an input file # class FILE_DESC: def __init__(self): return def to_xml(self): xml = ('\n' '%s\n' ) %(self.mode) if self.mode == 'remote': xml += ('%s\n' '%f\n' '%s\n' ) %(self.url, self.nbytes, self.md5) else: xml += '%s\n' %(self.source) xml += '\n' return xml # describes a job # class JOB_DESC: def __init__(self): return def to_xml(self): xml = '\n' if hasattr(self, 'name'): xml += '%s\n'%self.name if hasattr(self, 'rsc_fpops_est'): xml += '%f\n'%self.rsc_fpops_est if hasattr(self, 'command_line'): xml += '%s\n'%self.command_line if hasattr(self, 'input_template'): xml += '%s\n'%self.input_template if hasattr(self, 'output_template'): xml += '%s\n'%self.output_template if hasattr(self, 'priority'): xml += '%d\n'%(self.priority) if hasattr(self, 'files'): for file in self.files: xml += file.to_xml() xml += '\n' return xml # describes a batch for submit() or estimate() # class BATCH_DESC: def __init__(self): return def to_xml(self, op): xml = ('<%s>\n' '%s\n' '\n' '%s\n' ) %(op, self.authenticator, self.app_name) if hasattr(self, 'batch_id'): xml += '%s\n'%(self.batch_id) elif hasattr(self, 'batch_name'): xml += '%s\n'%(self.batch_name) if hasattr(self, 'app_version_num'): xml += '%d\n'%(self.app_version_num) if hasattr(self, 'allocation_priority'): if self.allocation_priority: xml += '\n' if hasattr(self, 'priority'): xml += '%d\n'%(self.priority) for job in self.jobs: xml += job.to_xml() xml += '\n\n' %(op) return xml class CREATE_BATCH_REQ: def __init__(self): return def to_xml(self): xml = ('\n' '%s\n' '%s\n' '%s\n' '%f\n' '\n') %(self.authenticator, self.app_name, self.batch_name, self.expire_time) return xml # a generic request # class REQUEST: def __init__(self): return rpc_timeout = 0 def do_http_post(req, project_url, handler='submit_rpc_handler.php'): #print(req) url = project_url + handler params = urllib.urlencode({'request': req}) if rpc_timeout>0: f = urllib2.urlopen(url, params, rpc_timeout) else: f = urllib2.urlopen(url, params) reply = f.read() #print("REPLY:", reply) return ET.fromstring(reply) ########### API FUNCTIONS START HERE ############### def set_timeout(x): global rpc_timeout rpc_timeout = x def abort_batch(req): req_xml = ('\n' '%s\n' '%s\n' '\n' ) %(req.authenticator, req.batch_id) return do_http_post(req_xml, req.project) def abort_jobs(req): req_xml = ('\n' '%s\n' ) %(req.authenticator) for job in req.jobs: req_xml += '%s\n'%(job) req_xml += '\n' return do_http_post(req_xml, req.project) # req is a CREATE_BATCH_REQ # def create_batch(req): return do_http_post(req.to_xml(), req.project) def estimate_batch(req): return do_http_post(req.to_xml('estimate_batch'), req.project) def query_batch(req): req_xml = ('\n' '%s\n' '%s\n' '%d\n' '%d\n' '\n' ) %(req.authenticator, req.batch_id, 1 if req.get_cpu_time else 0, 1 if req.get_job_details else 0) return do_http_post(req_xml, req.project) def query_batches(req): req_xml = ('\n' '%s\n' '%d\n' '\n' ) %(req.authenticator, 1 if req.get_cpu_time else 0) return do_http_post(req_xml, req.project) def query_completed_job(req): req_xml = ('\n' '%s\n' '%s\n' '\n' ) %(req.authenticator, req.job_name) return do_http_post(req_xml, req.project) def query_job(req): req_xml = ('\n' '%s\n' '%s\n' '\n' ) %(req.authenticator, req.job_id) return do_http_post(req_xml, req.project) def get_output_file(req): auth_str = hashlib.md5(req.authenticator+req.instance_name).hexdigest() name = req.instance_name file_num = req.file_num return req.project+"/get_output.php?cmd=result_file&result_name=%s&file_num=%s&auth_str=%s"%(name, file_num, auth_str) def get_output_files(req): auth_str = hashlib.md5(req.authenticator+str(req.batch_id)).hexdigest() return req.project+"/get_output.php?cmd=batch_files&batch_id=%s&auth_str=%s"%(req.batch_id, auth_str) def retire_batch(req): req_xml = ('\n' '%s\n' '%s\n' '\n' ) %(req.authenticator, req.batch_id) return do_http_post(req_xml, req.project) def submit_batch(req): return do_http_post(req.to_xml('submit_batch'), req.project) # see if reply is error. # if so print the message and return True # def check_error(response): if response.find('error') is not None: print('BOINC server error: ', response.find('error').find('error_msg').text) return True ############ FILE MANAGEMENT API ############## class QUERY_FILES_REQ: def __init__(self): return def to_xml(self): xml = ('\n' '%s\n' '%d\n') %(self.authenticator, self.batch_id) for name in self.boinc_names: xml += '%s\n' %(name) xml += '\n' return xml class UPLOAD_FILES_REQ: def __init__(self): return def to_xml(self): xml = ('\n' '%s\n' '%d\n') %(self.authenticator, self.batch_id) for name in self.boinc_names: xml += '%s\n' %(name) xml += '\n' return xml def query_files(query_req): reply = do_http_post(query_req.to_xml(), query_req.project, 'job_file.php') return reply # This actually does two RPCs: # query_files() to find what files aren't already on server # upload_files() to upload them # def upload_files(upload_files_req): query_req = QUERY_FILES_REQ() query_req.authenticator = upload_files_req.authenticator query_req.batch_id = upload_files_req.batch_id query_req.boinc_names = upload_files_req.boinc_names query_req_xml = query_req.to_xml() reply = do_http_post(query_req_xml, upload_files_req.project, 'job_file.php') if reply[0].tag == 'error': return reply absent = reply.find('absent_files').findall('file') #print('query files succeeded; ',len(absent), ' files need upload') boinc_names = [] local_names = [] for n in absent: ind = int(n.text) boinc_names.append(upload_files_req.boinc_names[ind]) local_names.append(upload_files_req.local_names[ind]) upload_files_req.boinc_names = boinc_names upload_files_req.local_names = local_names # make a description of upoad files for "requests" # files = [] for i in range(len(boinc_names)): bn = boinc_names[i] ln = local_names[i] upload_name = 'file_%d'%(i) files.append((upload_name, (bn, open(ln, 'rb'), 'application/octet-stream'))) url = upload_files_req.project + '/job_file.php' req_xml = upload_files_req.to_xml() #print(req_xml) req = {'request': req_xml} reply = requests.post(url, data=req, files=files) #print("reply text: ", reply.text) return ET.fromstring(reply.text) # returns an XML object with various job counts # results_ready_to_send # results_in_progress # results_need_file_delete # wus_need_validate # wus_need_assimilate # wus_need_file_delete # see tools/submit_api_test.py # def get_job_counts(req): return do_http_post('', req.project, 'server_status.php?counts=1')