Add multimaster support to slapproxy.

parent 955bc036
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
import logging import logging
from slapos.proxy.views import app
def _generateSoftwareProductListFromString(software_product_list_string): def _generateSoftwareProductListFromString(software_product_list_string):
""" """
...@@ -52,33 +53,48 @@ def _generateSoftwareProductListFromString(software_product_list_string): ...@@ -52,33 +53,48 @@ def _generateSoftwareProductListFromString(software_product_list_string):
class ProxyConfig(object): class ProxyConfig(object):
def __init__(self, logger): def __init__(self, logger):
self.logger = logger self.logger = logger
self.multimaster = {}
self.software_product_list = []
def mergeConfig(self, args, configp): def mergeConfig(self, args, configp):
# Set options parameters # Set arguments parameters (from CLI) as members of self
for option, value in args.__dict__.items(): for option, value in args.__dict__.items():
setattr(self, option, value) setattr(self, option, value)
# Merge the arguments and configuration for section in configp.sections():
for section in ("slapproxy", "slapos"):
configuration_dict = dict(configp.items(section)) configuration_dict = dict(configp.items(section))
if section in ("slapproxy", "slapos"):
# Merge the arguments and configuration as member of self
for key in configuration_dict: for key in configuration_dict:
if not getattr(self, key, None): if not getattr(self, key, None):
setattr(self, key, configuration_dict[key]) setattr(self, key, configuration_dict[key])
elif section.startswith('multimaster/'):
# Merge multimaster configuration if any
# XXX: check for duplicate SR entries
for key, value in configuration_dict.iteritems():
if key == 'software_release_list':
# Split multi-lines values
configuration_dict[key] = [line.strip() for line in value.strip().split('\n')]
self.multimaster[section.split('multimaster/')[1]] = configuration_dict
def setConfig(self): def setConfig(self):
if not self.database_uri: if not self.database_uri:
raise ValueError('database-uri is required.') raise ValueError('database-uri is required.')
# XXX: check for duplicate SR entries.
self.software_product_list = _generateSoftwareProductListFromString(
getattr(self, 'software_product_list', ''))
def setupFlaskConfiguration(conf):
app.config['computer_id'] = conf.computer_id
app.config['DATABASE_URI'] = conf.database_uri
app.config['software_product_list'] = conf.software_product_list
app.config['multimaster'] = conf.multimaster
def do_proxy(conf): def do_proxy(conf):
from slapos.proxy.views import app
for handler in conf.logger.handlers: for handler in conf.logger.handlers:
app.logger.addHandler(handler) app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO) app.logger.setLevel(logging.INFO)
app.config['computer_id'] = conf.computer_id setupFlaskConfiguration(conf)
app.config['DATABASE_URI'] = conf.database_uri app.run(host=conf.host, port=int(conf.port), threaded=True)
app.config['software_product_list'] = \
_generateSoftwareProductListFromString(
getattr(conf, 'software_product_list', ""))
app.run(host=conf.host, port=int(conf.port))
...@@ -45,3 +45,7 @@ CREATE TABLE IF NOT EXISTS partition_network%(version)s ( ...@@ -45,3 +45,7 @@ CREATE TABLE IF NOT EXISTS partition_network%(version)s (
netmask VARCHAR(255) netmask VARCHAR(255)
); );
CREATE TABLE IF NOT EXISTS forwarded_partition_request%(version)s (
partition_reference VARCHAR(255), -- a.k.a source_instance_id
master_url VARCHAR(255)
);
...@@ -29,10 +29,13 @@ ...@@ -29,10 +29,13 @@
############################################################################## ##############################################################################
from lxml import etree from lxml import etree
import random
import sqlite3 import sqlite3
import string
from slapos.slap.slap import Computer, ComputerPartition, \ from slapos.slap.slap import Computer, ComputerPartition, \
SoftwareRelease, SoftwareInstance, NotFoundError SoftwareRelease, SoftwareInstance, NotFoundError
from slapos.proxy.db_version import DB_VERSION from slapos.proxy.db_version import DB_VERSION
import slapos.slap
from flask import g, Flask, request, abort from flask import g, Flask, request, abort
import xml_marshaller import xml_marshaller
...@@ -112,14 +115,16 @@ def partitiondict2partition(partition): ...@@ -112,14 +115,16 @@ def partitiondict2partition(partition):
return slap_partition return slap_partition
def execute_db(table, query, args=(), one=False, db_version=None, log=False): def execute_db(table, query, args=(), one=False, db_version=None, log=False, db=None):
if not db:
db = g.db
if not db_version: if not db_version:
db_version = DB_VERSION db_version = DB_VERSION
query = query % (table + db_version,) query = query % (table + db_version,)
if log: if log:
print query print query
try: try:
cur = g.db.execute(query, args) cur = db.execute(query, args)
except: except:
app.logger.error('There was some issue during processing query %r on table %r with args %r' % (query, table, args)) app.logger.error('There was some issue during processing query %r on table %r with args %r' % (query, table, args))
raise raise
...@@ -330,9 +335,7 @@ def supplySupply(): ...@@ -330,9 +335,7 @@ def supplySupply():
@app.route('/requestComputerPartition', methods=['POST']) @app.route('/requestComputerPartition', methods=['POST'])
def requestComputerPartition(): def requestComputerPartition():
parsed_form_dict = parseRequestComputerPartitionForm(request.form) parsed_request_dict = parseRequestComputerPartitionForm(request.form)
# By default, ALWAYS request instance on default computer
parsed_form_dict['filter_kw'].setdefault('computer_guid', app.config['computer_id'])
# Is it a slave instance? # Is it a slave instance?
slave = loads(request.form.get('shared_xml', EMPTY_DICT_XML).encode()) slave = loads(request.form.get('shared_xml', EMPTY_DICT_XML).encode())
...@@ -341,29 +344,36 @@ def requestComputerPartition(): ...@@ -341,29 +344,36 @@ def requestComputerPartition():
if slave: if slave:
# XXX: change schema to include a simple "partition_reference" which # XXX: change schema to include a simple "partition_reference" which
# is name of the instance. Then, no need to do complex search here. # is name of the instance. Then, no need to do complex search here.
slave_reference = parsed_form_dict['partition_id'] + '_' + parsed_form_dict['partition_reference'] slave_reference = parsed_request_dict['partition_id'] + '_' + parsed_request_dict['partition_reference']
requested_computer_id = parsed_form_dict['filter_kw']['computer_guid'] requested_computer_id = parsed_request_dict['filter_kw'].get('computer_guid', app.config['computer_id'])
matching_partition = getAllocatedSlaveInstance(slave_reference, requested_computer_id) matching_partition = getAllocatedSlaveInstance(slave_reference, requested_computer_id)
else: else:
matching_partition = getAllocatedInstance(parsed_form_dict['partition_reference']) matching_partition = getAllocatedInstance(parsed_request_dict['partition_reference'])
if matching_partition: if matching_partition:
# Then the instance is already allocated, just update it # Then the instance is already allocated, just update it
# XXX: split request and request slave into different update/allocate functions and simplify. # XXX: split request and request slave into different update/allocate functions and simplify.
# By default, ALWAYS request instance on default computer
parsed_request_dict['filter_kw'].setdefault('computer_guid', app.config['computer_id'])
if slave: if slave:
software_instance = requestSlave(**parsed_form_dict) software_instance = requestSlave(**parsed_request_dict)
else: else:
software_instance = requestNotSlave(**parsed_form_dict) software_instance = requestNotSlave(**parsed_request_dict)
else: else:
# Instance is not yet allocated: try to do it. # Instance is not yet allocated: try to do it.
# XXX Insert here multimaster allocation external_master_url = isRequestToBeForwardedToExternalMaster(parsed_request_dict)
if external_master_url:
return forwardRequestToExternalMaster(external_master_url, request.form)
# XXX add support for automatic deployment on specific node depending on available SR and partitions on each Node. # XXX add support for automatic deployment on specific node depending on available SR and partitions on each Node.
# Note: only deploy on default node if SLA not specified # Note: It only deploys on default node if SLA not specified
# XXX: split request and request slave into different update/allocate functions and simplify. # XXX: split request and request slave into different update/allocate functions and simplify.
# By default, ALWAYS request instance on default computer
parsed_request_dict['filter_kw'].setdefault('computer_guid', app.config['computer_id'])
if slave: if slave:
software_instance = requestSlave(**parsed_form_dict) software_instance = requestSlave(**parsed_request_dict)
else: else:
software_instance = requestNotSlave(**parsed_form_dict) software_instance = requestNotSlave(**parsed_request_dict)
return dumps(software_instance) return dumps(software_instance)
...@@ -378,12 +388,120 @@ def parseRequestComputerPartitionForm(form): ...@@ -378,12 +388,120 @@ def parseRequestComputerPartitionForm(form):
parsed_dict['partition_id'] = form.get('computer_partition_id', '').encode() parsed_dict['partition_id'] = form.get('computer_partition_id', '').encode()
parsed_dict['partition_parameter_kw'] = loads(form.get('partition_parameter_xml', EMPTY_DICT_XML).encode()) parsed_dict['partition_parameter_kw'] = loads(form.get('partition_parameter_xml', EMPTY_DICT_XML).encode())
parsed_dict['filter_kw'] = loads(form.get('filter_xml', EMPTY_DICT_XML).encode()) parsed_dict['filter_kw'] = loads(form.get('filter_xml', EMPTY_DICT_XML).encode())
# Note: currently ignored on for slave instance (slave instances # Note: currently ignored for slave instance (slave instances
# are always started). # are always started).
parsed_dict['requested_state'] = loads(form.get('state').encode()) parsed_dict['requested_state'] = loads(form.get('state').encode())
return parsed_dict return parsed_dict
run_id = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(32)])
def checkIfMasterIsCurrentMaster(master_url):
"""
Because there are several ways to contact this server, we can't easily check
in a request() if master_url is ourself or not. So we contact master_url,
and if it returns an ID we know: it is ourself
"""
# Dumb way: compare with listening host/port
host = request.host
port = request.environ['SERVER_PORT']
if master_url == 'http://%s:%s/' % (host, port):
return True
# Hack way: call ourself
slap = slapos.slap.slap()
slap.initializeConnection(master_url)
try:
master_run_id = slap._connection_helper.GET('/getRunId')
except:
return False
if master_run_id == run_id:
return True
return False
@app.route('/getRunId', methods=['GET'])
def getRunId():
return run_id
def checkMasterUrl(master_url):
"""
Check if master_url doesn't represent ourself, and check if it is whitelisted
in multimaster configuration.
"""
if not master_url:
return False
if checkIfMasterIsCurrentMaster(master_url):
# master_url is current server: don't forward
return False
master_entry = app.config.get('multimaster').get(master_url, None)
# Check if this master is known
if not master_entry:
# Check if it is ourself
if not master_url.startswith('https') and checkIfMasterIsCurrentMaster(master_url):
return False
app.logger.warning('External SlapOS Master URL %s is not listed in multimaster list.' % master_url)
abort(404)
return True
def isRequestToBeForwardedToExternalMaster(parsed_request_dict):
"""
Check if we HAVE TO forward the request.
Several cases:
* The request specifies a master_url in filter_kw
* The software_release of the request is in a automatic forward list
"""
master_url = parsed_request_dict['filter_kw'].get('master_url')
if checkMasterUrl(master_url):
# Don't allocate the instance locally, but forward to specified master
return master_url
software_release = parsed_request_dict['software_release']
for mutimaster_url, mutimaster_entry in app.config.get('multimaster').iteritems():
if software_release in mutimaster_entry['software_release_list']:
# Don't allocate the instance locally, but forward to specified master
return mutimaster_url
return None
def forwardRequestToExternalMaster(master_url, request_form):
"""
Forward instance request to external SlapOS Master.
"""
master_entry = app.config.get('multimaster').get(master_url, {})
key_file = master_entry.get('key')
cert_file = master_entry.get('cert')
if master_url.startswith('https') and (not key_file or not cert_file):
app.logger.warning('External master %s configuration did not specify key or certificate.' % master_url)
abort(404)
if master_url.startswith('https') and not master_url.startswith('https') and (key_file or cert_file):
app.logger.warning('External master %s configurqtion specifies key or certificate but is using plain http.' % master_url)
abort(404)
slap = slapos.slap.slap()
if key_file:
slap.initializeConnection(master_url, key_file=key_file, cert_file=cert_file)
else:
slap.initializeConnection(master_url)
partition_reference = request_form['partition_reference'].encode()
# Store in database
execute_db('forwarded_partition_request', 'INSERT OR REPLACE INTO %s values(:partition_reference, :master_url)',
{'partition_reference':partition_reference, 'master_url': master_url})
new_request_form = request_form.copy()
filter_kw = loads(new_request_form['filter_xml'].encode())
filter_kw['source_instance_id'] = partition_reference
new_request_form['filter_xml'] = dumps(filter_kw)
partition = loads(slap._connection_helper.POST('/requestComputerPartition', new_request_form))
# XXX move to other end
partition._master_url = master_url
return dumps(partition)
def getAllocatedInstance(partition_reference): def getAllocatedInstance(partition_reference):
""" """
Look for existence of instance, if so return the Look for existence of instance, if so return the
......
This diff is collapsed.
[slapos]
software_root = %(tempdir)s/opt/slapgrid
instance_root = %(tempdir)s/srv/slapgrid
master_url = %(proxyaddr)s
computer_id = computer
[slapproxy]
host = 127.0.0.1
port = 8080
database_uri = %(tempdir)s/lib/proxy.db
# Here goes the list of slapos masters that slapproxy can contact
# Each section beginning by multimaster is a different SlapOS Master, represented by arbitrary name.
# For each section, you need to specify the URL of the SlapOS Master.
# For each section, you can specify if needed the location of key/certificate used to authenticate to this slapOS Master.
# For each section, you can specify a list of Software Releases. Any instance request matching this Softwrare Release will be automatically forwarded to this SlapOS Master and will not be allocated locally.
[multimaster/https://slap.vifib.com]
key = /path/to/cert.key
cert = /path/to/cert.cert
# XXX add wildcard support for SR list.
software_release_list =
http://something.io/software.cfg
/some/arbitrary/local/unix/path
[multimaster/http://%(external_proxy_host)s:%(external_proxy_port)s]
# No certificate here: it is http.
software_release_list =
http://mywebsite.me/exteral_software_release.cfg
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment