#!/usr/bin/env python # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects # Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ # # This software is a modified version of the Kestrel interface # package that is provided by NEOS: http://www.neos-server.org # import io import os import re import six import sys import time import socket import base64 import tempfile import logging from pyomo.common.dependencies import attempt_import def _xmlrpclib_importer(): if six.PY2: import xmlrpclib else: import xmlrpc.client as xmlrpclib return xmlrpclib xmlrpclib = attempt_import('xmlrpclib', importer=_xmlrpclib_importer)[0] gzip = attempt_import('gzip')[0] logger = logging.getLogger('pyomo.neos') class NEOS(object): # NEOS currently only supports HTTPS access scheme = 'https' host = 'neos-server.org' port = '3333' # Legacy NEOS HTTP interface #urlscheme = 'http' #port = '3332' def ProxiedTransport(): if six.PY2: from urlparse import urlparse import httplib # ProxiedTransport from Python 2.x documentation # (https://docs.python.org/2/library/xmlrpclib.html) class ProxiedTransport_PY2(xmlrpclib.Transport): def set_proxy(self, proxy): self.proxy = urlparse(proxy) if not self.proxy.hostname: # User omitted scheme from the proxy; assume http self.proxy = urlparse('http://'+proxy) def make_connection(self, host): target = urlparse(host) if target.scheme: self.realhost = target.geturl() else: self.realhost = '%s://%s' % (NEOS.scheme, target.geturl()) # Empirically, the connection class in Python 2.7 needs to # match the PROXY connection scheme, and the final endpoint # scheme needs to be specified in the POST below. if self.proxy.scheme == 'https': connClass = httplib.HTTPSConnection else: connClass = httplib.HTTPConnection return connClass(self.proxy.hostname, self.proxy.port) def send_request(self, connection, handler, request_body): connection.putrequest( "POST", '%s%s' % (self.realhost, handler)) def send_host(self, connection, host): connection.putheader('Host', self.realhost) return ProxiedTransport_PY2() else: # Python 3.x from urllib.parse import urlparse import http.client as httplib # ProxiedTransport from Python 3.x documentation # (https://docs.python.org/3/library/xmlrpc.client.html) class ProxiedTransport_PY3(xmlrpclib.Transport): def set_proxy(self, host): self.proxy = urlparse(host) if not self.proxy.hostname: # User omitted scheme from the proxy; assume http self.proxy = urlparse('http://'+host) def make_connection(self, host): scheme = urlparse(host).scheme if not scheme: scheme = NEOS.scheme # Empirically, the connection class in Python 3.x needs to # match the final endpoint connection scheme, NOT the proxy # scheme. The set_tunnel host then should NOT have a scheme # attached to it. if scheme == 'https': connClass = httplib.HTTPSConnection else: connClass = httplib.HTTPConnection connection = connClass(self.proxy.hostname, self.proxy.port) connection.set_tunnel(host) return connection return ProxiedTransport_PY3() class kestrelAMPL: def __init__(self): self.setup_connection() def setup_connection(self): # on *NIX, the proxy can show up either upper or lowercase. # Prefer lower case, and prefer HTTPS over HTTP if the # NEOS.scheme is https. proxy = os.environ.get( 'http_proxy', os.environ.get( 'HTTP_PROXY', '')) if NEOS.scheme == 'https': proxy = os.environ.get( 'https_proxy', os.environ.get( 'HTTPS_PROXY', proxy)) transport = None if proxy: transport = ProxiedTransport() transport.set_proxy(proxy) self.neos = xmlrpclib.ServerProxy( "%s://%s:%s" % (NEOS.scheme, NEOS.host, NEOS.port), transport=transport) logger.info("Connecting to the NEOS server ... ") try: result = self.neos.ping() logger.info("OK.") except (socket.error, xmlrpclib.ProtocolError, six.moves.http_client.BadStatusLine): e = sys.exc_info()[1] self.neos = None logger.info("Fail.") logger.warning("NEOS is temporarily unavailable.\n") def tempfile(self): return os.path.join(tempfile.gettempdir(),'at%s.jobs' % os.getenv('ampl_id')) def kill(self,jobnumber,password): response = self.neos.killJob(jobNumber,password) logger.info(response) def solvers(self): if self.neos is None: return [] else: attempt = 0 while attempt < 3: try: return self.neos.listSolversInCategory("kestrel") except socket.timeout: attempt += 1 return [] def retrieve(self,stub,jobNumber,password): # NEOS should return results as uu-encoded xmlrpclib.Binary data results = self.neos.getFinalResults(jobNumber,password) if isinstance(results,xmlrpclib.Binary): results = results.data #decode results to kestrel.sol # Well try to anyway, any errors will result in error strings in .sol file # instead of solution. if stub[-4:] == '.sol': stub = stub[:-4] solfile = open(stub + ".sol","wb") solfile.write(results) solfile.close() def submit(self, xml): # LOGNAME and USER should map to the effective user (i.e., the # one sudo-ed to), whereas USERNAME is the original user who ran # sudo. We include USERNAME to cover Windows, where LOGNAME and # USER may not be defined. for _ in ('LOGNAME','USER','USERNAME'): uname = os.getenv(_) if uname is not None: break hostname = socket.getfqdn(socket.gethostname()) user = "%s on %s" % (uname,hostname) (jobNumber,password) = self.neos.submitJob(xml,user,"kestrel") if jobNumber == 0: raise RuntimeError("%s\n\tJob not submitted" % (password,)) logger.info("Job %d submitted to NEOS, password='%s'\n" % (jobNumber,password)) logger.info("Check the following URL for progress report :\n") logger.info( "%s://www.neos-server.org/neos/cgi-bin/nph-neos-solver.cgi" "?admin=results&jobnumber=%d&pass=%s\n" % (NEOS.scheme, jobNumber,password)) return (jobNumber,password) def getJobAndPassword(self): """ If kestrel_options is set to job/password, then return the job and password values """ jobNumber=0 password="" options = os.getenv("kestrel_options") if options is not None: m = re.search(r'job\s*=\s*(\d+)',options,re.IGNORECASE) if m: jobNumber = int(m.groups()[0]) m = re.search(r'password\s*=\s*(\S+)',options,re.IGNORECASE) if m: password = m.groups()[0] return (jobNumber,password) def getSolverName(self): """ Read in the kestrel_options to pick out the solver name. The tricky parts: we don't want to be case sensitive, but NEOS is. we need to read in options variable """ # Get a list of available kestrel solvers from NEOS allKestrelSolvers = self.neos.listSolversInCategory("kestrel") kestrelAmplSolvers = [] for s in allKestrelSolvers: i = s.find(':AMPL') if i > 0: kestrelAmplSolvers.append(s[0:i]) self.options = None # Read kestrel_options to get solver name if "kestrel_options" in os.environ: self.options = os.getenv("kestrel_options") elif "KESTREL_OPTIONS" in os.environ: self.options = os.getenv("KESTREL_OPTIONS") # if self.options is not None: m = re.search('solver\s*=*\s*(\S+)',self.options,re.IGNORECASE) NEOS_solver_name=None if m: solver_name=m.groups()[0] for s in kestrelAmplSolvers: if s.upper() == solver_name.upper(): NEOS_solver_name=s break # if not NEOS_solver_name: raise RuntimeError( "%s is not available on NEOS. Choose from:\n\t%s" % (solver_name, "\n\t".join(kestrelAmplSolvers))) # if self.options is None or m is None: raise RuntimeError( "%s is not available on NEOS. Choose from:\n\t%s" % (solver_name, "\n\t".join(kestrelAmplSolvers))) return NEOS_solver_name def formXML(self,stub): solver = self.getSolverName() zipped_nl_file = io.BytesIO() if os.path.exists(stub) and stub[-3:] == '.nl': stub = stub[:-3] nlfile = open(stub+".nl","rb") zipper = gzip.GzipFile(mode='wb',fileobj=zipped_nl_file) zipper.write(nlfile.read()) zipper.close() nlfile.close() # ampl_files={} for key in ['adj','col','env','fix','spc','row','slc','unv']: if os.access(stub+"."+key,os.R_OK): f = open(stub+"." +key,"r") val="" buf = f.read() while buf: val += buf buf=f.read() f.close() ampl_files[key] = val # Get priority priority = "" m = re.search(r'priority[\s=]+(\S+)',self.options) if m: priority = "%s\n" % (m.groups()[0]) # Add any AMPL-created environment variables to dictionary solver_options = "kestrel_options:solver=%s\n" % solver.lower() solver_options_key = "%s_options" % solver # solver_options_value = "" if solver_options_key in os.environ: solver_options_value = os.getenv(solver_options_key) elif solver_options_key.lower() in os.environ: solver_options_value = os.getenv(solver_options_key.lower()) elif solver_options_key.upper() in os.environ: solver_options_value = os.getenv(solver_options_key.upper()) if not solver_options_value == "": solver_options += "%s_options:%s\n" % (solver.lower(), solver_options_value) # if six.PY2: nl_string = base64.encodestring(zipped_nl_file.getvalue()) else: nl_string = (base64.encodebytes(zipped_nl_file.getvalue())).decode('utf-8') xml = """ kestrel %s AMPL %s %s %s\n""" %\ (solver,priority, solver_options, nl_string) # for key in ampl_files: xml += "<%s>\n" % (key,ampl_files[key],key) # for option in ["kestrel_auxfiles","mip_priorities","objective_precision"]: if option in os.environ: xml += "<%s>\n" % (option,os.getenv(option),option) # xml += "" return xml if __name__=="__main__": #pragma:nocover if len(sys.argv) < 2: sys.stdout.write("kestrel should be called from inside AMPL.\n") sys.exit(1) kestrel = kestrelAMPL() if sys.argv[1] == "solvers": for s in sorted(kestrel.neos.listSolversInCategory("kestrel")): print(" "+s) sys.exit(0) elif sys.argv[1] == "submit": xml = kestrel.formXML("kestproblem") (jobNumber,password) = kestrel.submit(xml) # Add the job,pass to the stack jobfile = open(kestrel.tempfile(),'a') jobfile.write("%d %s\n" % (jobNumber,password)) jobfile.close() elif sys.argv[1] == "retrieve": # Pop job,pass from the stack try: jobfile = open(kestrel.tempfile(),'r') except IOError: e = sys.exc_info()[1] sys.stdout.write("Error, could not open file %s.\n") sys.stdout.write("Did you use kestrelsub?\n") sys.exit(1) m = re.match(r'(\d+) ([a-zA-Z]+)',jobfile.readline()) if m: jobNumber = int(m.groups()[0]) password = m.groups()[1] restofstack = jobfile.read() jobfile.close() kestrel.retrieve('kestresult',jobNumber,password) if restofstack: sys.stdout.write("restofstack: %s\n" % restofstack) jobfile = open(kestrel.tempfile(),'w') jobfile.write(restofstack) jobfile.close() else: os.unlink(kestrel.tempfile()) elif sys.argv[1] == "kill": (jobNumber,password) = kestrel.getJobAndPassword() if jobNumber: kestrel.kill(jobNumber,password) else: sys.stdout.write("To kill a NEOS job, first set kestrel_options variable:\n") sys.stdout.write('\tampl: option kestrel_options "job=#### password=xxxx";\n') else: try: stub = sys.argv[1] # See if kestrel_options has job=.. password=.. (jobNumber,password) = kestrel.getJobAndPassword() # otherwise, submit current problem to NEOS if not jobNumber: xml = kestrel.formXML(stub) (jobNumber,password) = kestrel.submit(xml) except KeyboardInterrupt: e = sys.exc_info()[1] sys.stdout.write("Keyboard Interrupt while submitting problem.\n") sys.exit(1) try: # Get intermediate results time.sleep(1) status = "Running" offset = 0 while status == "Running" or status == "Waiting": (output,offset) = kestrel.neos.getIntermediateResults(jobNumber, password,offset) if isinstance(output,xmlrpclib.Binary): output = output.data sys.stdout.write(output) status = kestrel.neos.getJobStatus(jobNumber,password) time.sleep(5) # Get final results kestrel.retrieve(stub,jobNumber,password) sys.exit(0) except KeyboardInterrupt: e = sys.exc_info()[1] msg = ''' Keyboard Interrupt\n\ Job is still running on remote machine\n\ To stop job:\n\ \tampl: option kestrel_options "job=%d password=%s";\n\ \tampl: commands kestrelkill;\n\ To retrieve results:\n\ \tampl: option kestrel_options "job=%d password=%s";\n\ \tampl: solve;\n''' % (jobNumber,password,jobNumber,password) sys.stdout.write(msg) sys.exit(1)