From: Marta Carbone Date: Thu, 21 Feb 2008 14:10:56 +0000 (+0000) Subject: Add the Emulation Component support to the PLCAPI. X-Git-Tag: PLCAPI-dummynet-1~9 X-Git-Url: http://git.onelab.eu/?a=commitdiff_plain;h=5d1b60d75b9342476ca1ceff8e161b4e21e63077;p=plcapi.git Add the Emulation Component support to the PLCAPI. The Emulation Component is a part of the work done by UniPi in the context of the EU OneLab project. The purpose of this component is to emulate the behaviour of a wired or a wireless link. It is based on an external device, called Dummynet box, sitting on the link between Nodes and the rest of the Internet. The PLCAPI extension involves: - modification to the planetlab database to store DummynetBoxes; - a python `DummyBoxes' class and Methods to manipulate these additional information. In order to generate a new Dummynet box image, an additional package is needed, containing the picobsd.bin base image. The roles check is still in development. More information regarding the main architectural of the Emulation Component could be found on the 2nd and 3rd deliverables(*) provided as part of the OneLab project. (*) http://www.one-lab.org/pub/OneLab/PublicDeliverables/OneLab-deliverable-D4E-2-Integrated-dummynet-and-PlanetLab.pdf the 3rd will be available soon. --- diff --git a/PLC/Auth.py b/PLC/Auth.py index f62b5119..26dc9105 100644 --- a/PLC/Auth.py +++ b/PLC/Auth.py @@ -17,6 +17,7 @@ from PLC.Parameter import Parameter, Mixed from PLC.Persons import Persons from PLC.Nodes import Node, Nodes from PLC.NodeNetworks import NodeNetwork, NodeNetworks +from PLC.DummyBoxes import DummyBox, DummyBoxes from PLC.Sessions import Session, Sessions from PLC.Peers import Peer, Peers from PLC.Boot import notify_owners @@ -45,12 +46,13 @@ class Auth(Parameter): expected = PasswordAuth() elif auth['AuthMethod'] == "gpg": expected = GPGAuth() - elif auth['AuthMethod'] == "hmac": + elif auth['AuthMethod'] == "hmac" or \ + auth['AuthMethod'] == "hmac_dummybox": expected = BootAuth() elif auth['AuthMethod'] == "anonymous": expected = AnonymousAuth() else: - raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', or 'anonymous'", "AuthMethod") + raise PLCInvalidArgument("must be 'session', 'password', 'gpg', 'hmac', 'hmac_dummybox', or 'anonymous'", "AuthMethod") # Re-check using the specified authentication method method.type_check("auth", auth, expected, (auth,) + args) @@ -151,6 +153,19 @@ class SessionAuth(Auth): method.caller = persons[0] + # XXX Don't know why with session authentication will have no success. + elif session['dummybox_id'] is not None: # and session['expires'] > time.time(): + dummyboxes = DummyBoxes(method.api, {'dummybox_id': session['dummybox_id']}) + if not dummyboxes: + raise PLCAuthenticationFailure, "No such dummybox" + dummybox = dummyboxes[0] + + # XXX enable this check when the `dummynet' role will be added + #if 'node' not in method.roles: + #raise PLCAuthenticationFailure, "Not allowed to call method" + + method.caller = dummybox + else: raise PLCAuthenticationFailure, "Invalid session" @@ -158,6 +173,12 @@ class SessionAuth(Auth): session.delete() raise fault +# The authentication method used by the DummynetBox +# it's the same used by nodes, so I try to use the same code. +# To do this I need to know if the request comes from a node or +# from a DummynetBox, so the `AuthMethod' parameter is overloaded +# and is values is `hmac' to authenticate a node, +# or `hmac_dummynet' to authenticate a DummynetBox. class BootAuth(Auth): """ PlanetLab version 3.x node authentication structure. Used by the @@ -173,9 +194,9 @@ class BootAuth(Auth): def __init__(self): Auth.__init__(self, { - 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac'", optional = False), - 'node_id': Parameter(int, "Node identifier", optional = False), - 'value': Parameter(str, "HMAC of node key and method call", optional = False) + 'AuthMethod': Parameter(str, "Authentication method to use, always 'hmac' or 'hmac_dummybox'", optional = False), + 'node_id': Parameter(int, "Node or DummynetBox identifier", optional = False), + 'value': Parameter(str, "HMAC of node key or dummynet box and method call", optional = False) }) def canonicalize(self, args): @@ -205,62 +226,92 @@ class BootAuth(Auth): if 'node' not in method.roles: raise PLCAuthenticationFailure, "Not allowed to call method" - try: - nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None}) - if not nodes: - raise PLCAuthenticationFailure, "No such node" - node = nodes[0] - - if node['key']: - key = node['key'] - elif node['boot_nonce']: - # Allow very old nodes that do not have a node key in - # their configuration files to use their "boot nonce" - # instead. The boot nonce is a random value generated - # by the node itself and POSTed by the Boot CD when it - # requests the Boot Manager. This is obviously not - # very secure, so we only allow it to be used if the - # requestor IP is the same as the IP address we have - # on record for the node. - key = node['boot_nonce'] - - nodenetwork = None - if node['nodenetwork_ids']: - nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids']) - for nodenetwork in nodenetworks: - if nodenetwork['is_primary']: - break - - if not nodenetwork or not nodenetwork['is_primary']: - raise PLCAuthenticationFailure, "No primary network interface on record" - - if method.source is None: - raise PLCAuthenticationFailure, "Cannot determine IP address of requestor" - - if nodenetwork['ip'] != method.source[0]: - raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \ - (method.source[0], nodenetwork['ip']) - else: - raise PLCAuthenticationFailure, "No node key or boot nonce" + # check dummynetbox authentication + if auth['AuthMethod'] == 'hmac_dummybox': + try: + dummyboxes = DummyBoxes(method.api, {'dummybox_id': auth['node_id']}) + if not dummyboxes: + raise PLCAuthenticationFailure, "No such dummybox" + dummybox = dummyboxes[0] - # Yes, this is the "canonicalization" method used. - args = self.canonicalize(args) - args.sort() - msg = "[" + "".join(args) + "]" + if dummybox['key']: + key = dummybox['key'] + else: + raise PLCAuthenticationFailure, "No dummybox key" - # We encode in UTF-8 before calculating the HMAC, which is - # an 8-bit algorithm. - digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest() + args = self.canonicalize(args) + args.sort() + msg = "[" + "".join(args) + "]" - if digest != auth['value']: - raise PLCAuthenticationFailure, "Call could not be authenticated" + # We encode in UTF-8 before calculating the HMAC, which is + # an 8-bit algorithm. + digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest() - method.caller = node + if digest != auth['value']: + raise PLCAuthenticationFailure, "Call could not be authenticated" - except PLCAuthenticationFailure, fault: - if nodes: - notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault) - raise fault + method.caller = dummybox + + except PLCAuthenticationFailure, fault: + # XXX add notification + raise fault + else: + try: + nodes = Nodes(method.api, {'node_id': auth['node_id'], 'peer_id': None}) + if not nodes: + raise PLCAuthenticationFailure, "No such node" + node = nodes[0] + + if node['key']: + key = node['key'] + elif node['boot_nonce']: + # Allow very old nodes that do not have a node key in + # their configuration files to use their "boot nonce" + # instead. The boot nonce is a random value generated + # by the node itself and POSTed by the Boot CD when it + # requests the Boot Manager. This is obviously not + # very secure, so we only allow it to be used if the + # requestor IP is the same as the IP address we have + # on record for the node. + key = node['boot_nonce'] + + nodenetwork = None + if node['nodenetwork_ids']: + nodenetworks = NodeNetworks(method.api, node['nodenetwork_ids']) + for nodenetwork in nodenetworks: + if nodenetwork['is_primary']: + break + + if not nodenetwork or not nodenetwork['is_primary']: + raise PLCAuthenticationFailure, "No primary network interface on record" + + if method.source is None: + raise PLCAuthenticationFailure, "Cannot determine IP address of requestor" + + if nodenetwork['ip'] != method.source[0]: + raise PLCAuthenticationFailure, "Requestor IP %s does not match node IP %s" % \ + (method.source[0], nodenetwork['ip']) + else: + raise PLCAuthenticationFailure, "No node key or boot nonce" + + # Yes, this is the "canonicalization" method used. + args = self.canonicalize(args) + args.sort() + msg = "[" + "".join(args) + "]" + + # We encode in UTF-8 before calculating the HMAC, which is + # an 8-bit algorithm. + digest = hmac.new(key, msg.encode('utf-8'), sha).hexdigest() + + if digest != auth['value']: + raise PLCAuthenticationFailure, "Call could not be authenticated" + + method.caller = node + + except PLCAuthenticationFailure, fault: + if nodes: + notify_owners(method, node, 'authfail', include_pis = True, include_techs = True, fault = fault) + raise fault class AnonymousAuth(Auth): """ diff --git a/PLC/DummyBoxes.py b/PLC/DummyBoxes.py new file mode 100644 index 00000000..57288433 --- /dev/null +++ b/PLC/DummyBoxes.py @@ -0,0 +1,134 @@ +# +# DummyBoxes class +# +# Derived class from the `Row' base class. +# This class contains structure and functions used to +# interact with the dummyboxes table in the planetlab database +# +# Marta Carbone +# Copyright (C) 2007 UniPi +# $Id:$ +# + +import socket # check ip address + +from PLC.Faults import * # manage faults +from PLC.Parameter import Parameter # use the Parameter wrapper +from PLC.Filter import Filter # filter fields +from PLC.Table import Row, Table # base class to manipulate a row +from PLC.Sites import Site, Sites # used to check if a site_id exist +from PLC.Nodes import valid_hostname # validate hostname function +from PLC.NodeNetworks import valid_ip # validate ip address + +class DummyBox(Row): + """ + Definition of a row in the dummyboxes table. + Here we define which fields related to the + ``dummyboxes'' table we want to manipulate. + More information are in the base class `Row' + defined in the `Tables' module. + """ + + table_name = 'dummyboxes' + primary_key = 'dummybox_id' + join_tables = ['dummybox_session'] + + # These are the fields we want to be manipulated + # from the python interface. + fields = { + 'dummybox_id': Parameter(int, "DummyBox identifier"), + 'hostname': Parameter(str, "Fully qualified hostname"), + 'site_id': Parameter(int, "Site at which this DummyBox is located"), + 'ip': Parameter(str, "DummyBox IP address"), + 'key': Parameter(str, "(Admin only) DummyBox key", max=256), + 'netmask': Parameter(str, "DummyBox netmask number"), + 'gateway': Parameter(str, "IP address of primary gateway"), + 'dns1': Parameter(str, "IP address of primary DNS server"), + 'dns2': Parameter(str, "IP address of secondary DNS server", nullok = True), + 'node_ids': Parameter([int], "List of nodes that a DummyBox manage"), + } + + # Start to define 'validate_[key]' functions. + # These will be called from the base class. + + # Validate the hostname + def validate_hostname(self, hostname): + if hostname and not valid_hostname(hostname): + raise PLCInvalidArgument, "Invalid hostname %s" % hostname + return hostname + + # Check for site existence in the database + def validate_site_id(self, site_id): + sites = [row['site_id'] for row in Sites(self.api)] + if site_id not in sites: + raise PLCInvalidArgument, "Site %s not present" % site_id + return site_id + + def validate_ip(self, ip): + if ip and not valid_ip(ip): + raise PLCInvalidArgument, "Invalid IP address %s"%ip + return ip + + # Check ips. At the moment we only check that the address is syntactically + # correct. Other pieces of onelab code also check for duplicate IPs, but + # only look in their own table. I am not sure the duplicate check is useful, + # but if we want to do it, we need to look in all tables (nodes, pcus, dummyboxes...) + validate_ip = validate_ip + validate_netmask = validate_ip + validate_gateway = validate_ip + validate_dns1 = validate_ip + validate_dns2 = validate_ip + + def delete(self, commit = True): + """ + Delete existing dummybox. + """ + assert 'dummybox_id' in self + + # when a dummybox is deleted we need to zero + # the `dummybox_id' entry in `Nodes' table + sql_update = "UPDATE Nodes SET dummybox_id = 0 WHERE dummybox_id = %d" % \ + (self['dummybox_id']) + self.api.db.do(sql_update) + + # mark the dummybox as deleted + # We set the `deleted' field to `true', then + # the sync function update the row in the table. + self['deleted'] = True + self.sync(commit) + +class DummyBoxes(Table): + """ + In this class we specify how to select a row in the `DummyBoxes' table + (also see the documentation there). The "filter" used in the select query + is specified as follows: + - using an int we refer the dummybox_id field + - using a list of int we refer rows with specified dummybox_ids + - we can access others fields using a dict + - more data access methods will be added (i.e. selection by hostname) + + For internals see the base class `Table' in `Table.py' + """ + + def __init__(self, api, d_filter = None, columns = None): + Table.__init__(self, api, DummyBox, columns) + + sql = "SELECT %s FROM view_dummyboxes WHERE deleted IS False" % \ + ", ".join(self.columns) + + if d_filter is not None: + if isinstance(d_filter, int): + d_filter = Filter(DummyBox.fields, {'dummybox_id':d_filter}) + elif isinstance(d_filter, list): + # extract int numbers, ignore other elements + ints = filter(lambda x: isinstance(x, (int, long)), d_filter) + # build a suitable dict + d_filter = Filter(DummyBox.fields, {'dummybox_id': ints}) + elif isinstance(d_filter, dict): + d_filter = Filter(DummyBox.fields, d_filter) + else: + raise PLCInvalidArgument, "Wrong dummybox filter %r" % d_filter + + sql += " AND (%s) %s" % d_filter.sql(api, "AND") + + self.selectall(sql) diff --git a/PLC/Methods/AddDummyBox.py b/PLC/Methods/AddDummyBox.py new file mode 100644 index 00000000..21f364a4 --- /dev/null +++ b/PLC/Methods/AddDummyBox.py @@ -0,0 +1,82 @@ +# +# Marta Carbone - UniPi +# $Id:$ +# + +from PLC.Faults import * # faults library +from PLC.Method import Method # base class used for methods +from PLC.Parameter import Parameter, Mixed # define input parameters +from PLC.DummyBoxes import DummyBox, DummyBoxes # main class for a DummyBox +from PLC.Sites import Site, Sites # manage authentication +from PLC.Auth import Auth # import Auth parameter + +# define a lambda function to filter fields +# that can not be modified by this method +can_update = lambda (field, value): field not in \ + ['dummybox_id', 'site_id'] + +class AddDummyBox(Method): + """ + Adds a new DummyBox. + Takes as input the site_id or the login_base of the site + and a dict parameter with DummyBox fields. + + This operation is restricted to PIs and techs owner of the site. + Admins may add DummyBoxes to any site. + + Returns the new dummybox_id (> 0) if successful, faults otherwise. + """ + + # Values required for the base class `Method' + roles = ['admin', 'pi', 'tech'] + + dummybox_fields = dict(filter(can_update, DummyBox.fields.items())) + # This value is used by the `Method' class to check for + # parameter correctness when calling the method. (`call' function) + # Auth() is an authentication structure, derived from the `Parameter' class + accepts = [ + Auth(), + Mixed(Site.fields['site_id'], + Site.fields['login_base']), + dummybox_fields + ] + returns = Parameter(int, 'New dummybox_id (> 0) if successful') + + def call(self, auth, site_id_or_login_base, dummybox_fields): + + # check if selected fields are writable + dummybox_fields = dict(filter(can_update, dummybox_fields.items())) + + # check for site existence + sites = Sites(self.api, [site_id_or_login_base]) + if not sites: + raise PLCInvalidArgument, "No such site %s" % site_id_or_login_base + + site = sites[0] # there is at most one matching entry + + # this is not an anonymous method, + # we need to have a caller + assert self.caller is not None + + # admin can do all on any site, + # pi and tech can do this only on their site. + if 'admin' not in self.caller['roles']: + if site['site_id'] not in self.caller['site_ids']: + assert self.caller['person_id'] not in site['person_ids'] + raise PLCPermissionDenied, \ + "Not allowed to add DummyBoxes to the specified site" + else: + assert self.caller['person_id'] in site['person_ids'] + + # create a DummyBox object with the values to insert, + # put the site_id value, and sync to the database + dummybox = DummyBox(self.api, dummybox_fields) + dummybox['site_id'] = site['site_id'] + dummybox.sync() + + # log the creation of this DummyBox + self.event_objects = {'Site': [site['site_id']], + 'DummyBox': [dummybox['dummybox_id']]} + self.message = "DummyBox %s created" % dummybox['dummybox_id'] + + return dummybox['dummybox_id'] diff --git a/PLC/Methods/DeleteDummyBox.py b/PLC/Methods/DeleteDummyBox.py new file mode 100644 index 00000000..2d5c0547 --- /dev/null +++ b/PLC/Methods/DeleteDummyBox.py @@ -0,0 +1,58 @@ +# +# Marta Carbone - UniPi +# $Id:$ +# + +from PLC.Faults import * # faults library +from PLC.Filter import Filter # Filter fields +from PLC.Method import Method # base class used to derive methods +from PLC.Parameter import Parameter, Mixed # used to define input parameters +from PLC.Auth import Auth # import the Auth parameter +from PLC.DummyBoxes import DummyBox, DummyBoxes # main class for a DummyBox +#from PLC.Sites import Site, Sites # used to manage authentication + +class DeleteDummyBox(Method): + """ + Mark an existing DummyBox as deleted. + Takes as input the dummybox_id value of the DummyBox. + If presents, clean the dummmybox_id in the Nodes table too. + + This operation is restricted to PIs and techs owner of the site. + Admins may delete DummyBoxes to any site. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepts = [ + Auth(), + Mixed( Parameter(int, "dummybox_id"), + Filter(DummyBox.fields) ), + ] + + returns = Parameter(int, '1 if successful, faults otherwise.') + + def call(self, auth, dummybox_id): + + # check for DummyBox existence + dummyboxes = DummyBoxes(self.api, dummybox_id) + if not dummyboxes: + raise PLCInvalidArgument, "No such dummybox %s" % dummybox_id + + dummybox = dummyboxes[0] + + assert self.caller is not None + + # check if we have rights to do this operation + if 'admin' not in self.caller['roles']: + if dummyboxes['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to delete this DummyBoxes" + + dummybox.delete() + + # Logging variables + self.event_objects = {'DummyBox': [dummybox['dummybox_id']]} + self.message = "DummyBox %d deleted" % dummybox['dummybox_id'] + + return 1 diff --git a/PLC/Methods/GetBootMedium.py b/PLC/Methods/GetBootMedium.py index 708b8514..0d5a361b 100644 --- a/PLC/Methods/GetBootMedium.py +++ b/PLC/Methods/GetBootMedium.py @@ -43,6 +43,16 @@ boot_medium_actions = [ 'node-preview', 'generic-usb', ] +# compute a new key +def compute_key(): + # Generate 32 random bytes + bytes = random.sample(xrange(0, 256), 32) + # Base64 encode their string representation + key = base64.b64encode("".join(map(chr, bytes))) + # XXX Boot Manager cannot handle = in the key + key = key.replace("=", "") + return key + class GetBootMedium(Method): """ This method is a redesign based on former, supposedly dedicated, @@ -165,12 +175,7 @@ class GetBootMedium(Method): ( host, domain ) = self.split_hostname (node) if renew_key: - # Generate 32 random bytes - bytes = random.sample(xrange(0, 256), 32) - # Base64 encode their string representation - node['key'] = base64.b64encode("".join(map(chr, bytes))) - # XXX Boot Manager cannot handle = in the key - node['key'] = node['key'].replace("=", "") + node['key'] = compute_key() # Save it node.sync() diff --git a/PLC/Methods/GetDummyBoxMedium.py b/PLC/Methods/GetDummyBoxMedium.py new file mode 100644 index 00000000..bf531b98 --- /dev/null +++ b/PLC/Methods/GetDummyBoxMedium.py @@ -0,0 +1,144 @@ +# +# Marta Carbone - UniPi +# $Id:$ +# +# This class requires the rpm package containing +# the picobsd image to be installed +# on the Central Site system. +# + +import base64 +import os +import datetime + +from PLC.Faults import * # faults library +from PLC.Method import Method # base class for methods +from PLC.Parameter import Parameter # use the Parameter wrapper +from PLC.Auth import Auth # import the Auth parameter +from PLC.DummyBoxes import DummyBox, DummyBoxes # main class for a DummyBox +from PLC.Methods.GetBootMedium import compute_key # key generation function + +WORK_DIR = "/tmp/DummynetBoxMedium" + +class GetDummyBoxMedium(Method): + """ + This method is used to get a boot image of the DummyNetBox + equipped with the configuration file. + + We need to provide the dummybox_id of the DummyNetBox + we want to generate. + Since every time a new configuration file will be generater, + THIS OPERATION WILL INVALIDATE ANY PREVIOUSLY DUMMYNETBOX BOOT MEDIUM. + # XXX add checks for picobsd.bin existence + + Returns the iso image customized for the DummyNetBox with the new + key integrated in the image, and update the key fields in the database. + """ + # I added the session role, because this method should be called from the web + roles = ['admin', 'pi', 'tech', 'session'] + + accepts = [ + Auth(), + Parameter(int, "A dummybox_id") + ] + + returns = Parameter(str, "DummynetBox boot medium") + + # Generate a new configuration file in the working directory + # input parameters follows: + # self is used to access instance data, + # dummybox is the dummybox_id, + # new_key is the new generated key, + # configfile is the output file of the configuration. + def generate_conf_file(self, dummybox, new_key, configfile): + + # Generate the dummynet box configuration file + today = datetime.date.today() + file = "" + file += "# This is the dummynetbox configuration file\n" + file += "# and was generated the %s\n" % str(today) + + host_domain = dummybox['hostname'] + host_domain = host_domain.split('.', 1) + file += 'HOST_NAME="%s"\n' % host_domain[0] + file += 'DOMAIN_NAME="%s"\n' % host_domain[1] + + file += 'IP_ADDRESS="%s"\n' % dummybox['ip'] + file += 'IP_NETMASK="%s"\n' % dummybox['netmask'] + file += 'IP_GATEWAY="%s"\n' % dummybox['gateway'] + file += 'IP_DNS1="%s"\n' % dummybox['dns1'] + file += 'IP_DNS2="%s"\n' % dummybox['dns2'] + file += 'DUMMYBOX_ID=%s\n' % dummybox['dummybox_id'] + file += 'DUMMYBOX_KEY=%s\n' % new_key + + file += 'CS_IP=%s\n' % self.api.config.PLC_API_HOST + + # write the configuration file + # WORK_DIR must be writable + FILE = open(configfile, "w") + FILE.write(file) + FILE.close() + + return + + # Here starts the execution of the call + def call(self, auth, dummybox_id): + + # set file names + BASE_IMAGE = WORK_DIR + '/picobsd.bin' + IMAGE_NAME = str(WORK_DIR) + "/dummybox_" + str(dummybox_id) + ".bin" + configfile = WORK_DIR + '/dummybox.conf' + lockfile = WORK_DIR + '/lockfile' + + # Check for dummybox existence + dummyboxes = DummyBoxes(self.api, [dummybox_id]) + if not dummyboxes: + raise PLCInvalidArgument, "No such DummyBox %s" % dummybox_id + + dummybox = dummyboxes[0] + + # Permission checks + assert self.caller is not None + if 'admin' not in self.caller['roles']: + if dummybox['dummybox_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to generate an iso image for %s %s" % \ + (dummybox['hostname'], dummybox_id) + + # Start the generation of the image + # Generate a new key + new_key = compute_key() + + # create working dir and lock file for concurrent runs + if not os.path.exists(WORK_DIR): + print "Creating working directory %s" % WORK_DIR + os.mkdir(WORK_DIR) + + if os.path.exists(lockfile): + raise PLCInvalidArgument, "Lockfile %s exist, try again " % lockfile + else: + print "Executing "+"touch %s" % lockfile + os.system("touch %s" % lockfile) + + # generate the configuration file + conf_file = self.generate_conf_file(dummybox, new_key, configfile) + + # build the shell script to customize the dummynetbox image + # copy the raw file and find the configuration file position + shell_script = "(cp %s %s; export MATCH=`grep -abo START_USER_DATA %s | cut -d: -f1`; " \ + % (BASE_IMAGE, IMAGE_NAME, IMAGE_NAME) + + # cat the configuration file in the raw image + shell_script += "cat %s | dd of=%s seek=$MATCH conv=notrunc bs=1)" \ + % (configfile, IMAGE_NAME) + + # check returned values, 0 means OK, remove the lock file + os.system(shell_script) + os.system("rm %s" % (lockfile)) + + # if all goes fine store the key in the database + dummybox['key'] = new_key + dummybox.sync() + + # return the file + #return IMAGE_NAME + return base64.b64encode(file(IMAGE_NAME).read()) diff --git a/PLC/Methods/GetDummyBoxUsers.py b/PLC/Methods/GetDummyBoxUsers.py new file mode 100644 index 00000000..4532a848 --- /dev/null +++ b/PLC/Methods/GetDummyBoxUsers.py @@ -0,0 +1,155 @@ +# +# Marta Carbone - UniPi +# $Id:$ +# +# This Method returns a list of tuples formatted as follow: +# +# +# +# and an authorized_keys file, to be used on a dummynet box. +# + +from PLC.Method import Method # base class used to derive methods +from PLC.Parameter import Parameter, Mixed # define input parameters +from PLC.Auth import Auth # import the Auth parameter +from PLC.Faults import * # faults library +from PLC.DummyBoxes import DummyBox, DummyBoxes # main class for a DummyBox +from PLC.Nodes import Node, Nodes # main class for Nodes +from PLC.NodeNetworks import * # get the node ip address +from PLC.Slices import Slice, Slices # main class for Slices +from PLC.Keys import Key, Keys # main class for Keys +from PLC.Persons import Person, Persons # main class for Persons + +# authorized file delimiter string +NEWFILE_MARK = "authorized_keys_mark" + +class GetDummyBoxUsers(Method): + """ + Return a list of information about + slice, users, user keys, nodes and dummyboxes. + + This Methods is mean to be used by a DummyBox. + + Return keys, 0 if there are no users. + """ + + # XXX add the correct role instead of node + roles = ['admin', 'pi'] + + accepts = [ + Auth(), + Parameter(int, 'DummyBox id'), + ] + + returns = Parameter(str, "DummyBox files") + + def call(self, auth, dummybox_id = None): + """ + Get information about users on nodes connected to the DummyBox. + + Given a dummybox_id we get the list of connected nodes + For each node_id we get a list of connected slices + For each slice we get a list of connected users + For each user we get a list of keys that we return to the caller. + """ + + # These variables contain some text to be used + # to format the output files + # port-forwarding should be denied in the main sshd configuration file + ssh_command = "/home/user/dbox.cmd " + ssh_configuration = ",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty " + + # Get dummyboxes information + dummyboxes = DummyBoxes(self.api, dummybox_id, ['site_id']) + + if not dummyboxes: + raise PLCInvalidArgument, "No such DummyBox %s" % dummybox_id + + dummybox = dummyboxes[0] + + # this method needs authentication + assert self.caller is not None + + # XXX check if we have rights to do this operation: + # - admins can retrive all information they want, + # - dummyboxes can retrive information regarding their site, + # nodes and slice account present on their nodes. + + # Given a dummybox_id we get the list of connected nodes + nodes = Nodes(self.api, {'dummybox_id': dummybox_id}, ['node_id', 'hostname', 'slice_ids']) + if not nodes: return 0 + + user_map = "# Permission file, check here if a user can configure a link\n" # store slice-node information + user_map+= "# Tuples are `owner slice_name' `hostname to reconfigure'\n" + authorized_keys_dict = {} # user's keys, dictionary + dbox_key_id = 0 # key_id used to identify users keys in the dummynetbox + + # For each node_id we get a list of connected slices + for node in nodes: + + # list of connected slices + slice_ids = node['slice_ids'] + if not slice_ids: continue + + # For each slice we get a list of connected users + for slice_id in slice_ids: + # field to return + slice_name = Slices(self.api, {'slice_id': slice_id}, ['name', 'person_ids']) + if not slice_name: continue + + # Given a slice we get a list of users + person_ids = slice_name[0]['person_ids'] + if not person_ids: continue + + # For each user we get a list of keys + for person_id in person_ids: + # Given a user we get a list of keys + person_list = Persons(self.api, {'person_id': person_id}, ['person_id','key_ids']) + person = person_list[0]['person_id'] + key_list = person_list[0]['key_ids'] + if not key_list: continue + + for key_id in key_list: + key = Keys(self.api, {'key_id': key_id}, ['key']) + + # Here we have all information we need + # to build the authorized key file and + # the user map file + + k = key[0]['key'] + + # split the key in type/ssh_key/comment + splitted_key = k.split(' ',2) + uniq_key = splitted_key[0]+" "+splitted_key[1] + + # retrieve/create a unique dbox_key_id for this ssh_key + if authorized_keys_dict.has_key(uniq_key): + dbox_key_id = authorized_keys_dict[uniq_key] + else: + dbox_key_id+=1 + authorized_keys_dict.update({uniq_key : dbox_key_id}) + + # get the node ip address + # XXX should I check for `is_primary' too ? + nodenetworks = NodeNetworks(self.api, {'node_id': node['node_id']}) + + # append user and slice data to the user_map file + item = str(dbox_key_id) + item +=" " + str(nodenetworks[0]['ip']) + item +=" " + str(slice_name[0]['name']) + "\n" + + user_map += item + + # format change for authorized_keys dict + authorized_keys_file="" + authorized_keys_file += "# generated automatically by GetUsersUpdate.py on the Central Site\n"; + authorized_keys_file += "# format file:\n"; + authorized_keys_file += '# command="command key_id $SSH_ORIGINAL_COMMAND",ssh_options key_type key comment\n' + authorized_keys_file += "# where command, key_id and ssh_options are filled by the Central Site script\n" + authorized_keys_file += "# and $SSH_ORIGINAL_COMMAND is the command line inserted by the node\n" + + for i in authorized_keys_dict: + authorized_keys_file += "command=" + "\"" + ssh_command + str(authorized_keys_dict[i]); + authorized_keys_file += " $SSH_ORIGINAL_COMMAND" + "\"" + ssh_configuration + str(i) + "\n" + + return user_map+NEWFILE_MARK+"\n"+authorized_keys_file diff --git a/PLC/Methods/GetDummyBoxes.py b/PLC/Methods/GetDummyBoxes.py new file mode 100644 index 00000000..8ac81515 --- /dev/null +++ b/PLC/Methods/GetDummyBoxes.py @@ -0,0 +1,45 @@ +# +# Marta Carbone - UniPi +# $Id:$ +# + +from PLC.Faults import * # faults library +from PLC.Method import Method # base class used to derive methods +from PLC.Parameter import Parameter, Mixed # used to define input parameters +from PLC.DummyBoxes import DummyBox, DummyBoxes # main class for a DummyBox +from PLC.Persons import Person, Persons # used to filter fields +from PLC.Auth import Auth # import the Auth parameter +from PLC.Filter import Filter # filter fields + +class GetDummyBoxes(Method): + """ + Take as input a list of fields with fields required, + one or more integer used as dummybox_id + and a list of fields we want to return. + + Returns an array of structs containing details about dummyboxes. + An empty structs will be returned if the dummybox_id does not exist. + """ + + roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous'] + + accepts = [ + Auth(), + Mixed( Parameter(int, "dummybox_id"), + Parameter(list, "dummybox_ids"), + Filter(DummyBox.fields)), + Parameter([str], "List of fields to return", nullok = True), + ] + + # returns one or more rows + returns = [DummyBox.fields] + + def call(self, auth, dummybox_filter = None, return_fields = None): + + # Get dummyboxes information + # dummybox_filter filter output field to be shown + dummyboxes = DummyBoxes(self.api, dummybox_filter, return_fields) + + # XXX should we hide a part of these information + # for some roles? + return dummyboxes diff --git a/PLC/Methods/GetSession.py b/PLC/Methods/GetSession.py index ae752198..3c848bf5 100644 --- a/PLC/Methods/GetSession.py +++ b/PLC/Methods/GetSession.py @@ -6,6 +6,7 @@ from PLC.Auth import Auth from PLC.Sessions import Session, Sessions from PLC.Nodes import Node, Nodes from PLC.Persons import Person, Persons +from PLC.DummyBoxes import DummyBox, DummyBoxes class GetSession(Method): """ @@ -13,6 +14,7 @@ class GetSession(Method): successfully, faults otherwise. """ + #roles = ['admin', 'pi', 'user', 'tech', 'node', 'dummybox'] roles = ['admin', 'pi', 'user', 'tech', 'node'] accepts = [Auth()] returns = Session.fields['session_id'] @@ -35,5 +37,7 @@ class GetSession(Method): session.add_node(self.caller, commit = True) elif isinstance(self.caller, Person): session.add_person(self.caller, commit = True) + elif isinstance(self.caller, DummyBox): + session.add_dummybox(self.caller, commit = True) return session['session_id'] diff --git a/PLC/Methods/UpdateDummyBox.py b/PLC/Methods/UpdateDummyBox.py new file mode 100644 index 00000000..9f415c29 --- /dev/null +++ b/PLC/Methods/UpdateDummyBox.py @@ -0,0 +1,66 @@ +# +# Marta Carbone - UniPi +# $Id:$ +# + +from PLC.Faults import * # faults library +from PLC.Method import Method # this is the base class used to derive methods +from PLC.Parameter import Parameter, Mixed # used to define input parameters +from PLC.DummyBoxes import DummyBox, DummyBoxes # main class for a DummyBox +from PLC.Sites import Site, Sites # used to manage authentication +from PLC.Auth import Auth # import the Auth parameter + +# define unmodifiable fields +can_update = lambda (field, value): field not in ['dummybox_id', 'site_id'] + +class UpdateDummyBox(Method): + """ + Modify a DummyBox. + Takes as input the dummybox_id of the DummyBox and a dict parameter + with new DummyBox fields. + + This operation is restricted to PIs and techs owner of the site. + Admins may modify DummyBoxes to any site. + + Returns the dummybox_id of the DummyBox if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + dummybox_fields = dict(filter(can_update, DummyBox.fields.items())) + + accepts = [ + Auth(), + DummyBox.fields['site_id'], + dummybox_fields + ] + + returns = Parameter(int, 'The dummybox_id if successful, faults otherwise') + + def call(self, auth, dummybox_id, dummybox_fields): + + # Check if we can update selected fields + dummybox_fields = dict(filter(can_update, dummybox_fields.items())) + + # Check for DummyBox existence + dummyboxes = DummyBoxes(self.api, [dummybox_id]) + if not dummyboxes: + raise PLCInvalidArgument, "No such DummyBox %s" % dummybox_id + + dummybox = dummyboxes[0] + + assert self.caller is not None + + # Check if we have rights to do this operation + if 'admin' not in self.caller['roles']: + if dummybox['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to modify this DummyBox" + + dummybox.update(dummybox_fields) + dummybox.sync() + + # Log this operation + self.event_objects = {'DummyBox': [dummybox['dummybox_id']]} + self.message = "DummyBox %s modified" % dummybox['dummybox_id'] + + return dummybox['dummybox_id'] diff --git a/PLC/Methods/UpdateEmulationLink.py b/PLC/Methods/UpdateEmulationLink.py new file mode 100644 index 00000000..6c9a30dc --- /dev/null +++ b/PLC/Methods/UpdateEmulationLink.py @@ -0,0 +1,106 @@ +# +# Marta Carbone - UniPi +# $Id:$ +# + +from PLC.Faults import * # faults library +from PLC.Method import Method # base class used to derive methods +from PLC.Parameter import Parameter, Mixed # used to define input parameters +from PLC.Auth import Auth # import the Auth parameter +from PLC.DummyBoxes import DummyBox, DummyBoxes # main class for a DummyBox +from PLC.Nodes import Node, Nodes # main class for a Node +from PLC.Sites import Site, Sites # main class for Sites + +class UpdateEmulationLink(Method): + """ + Connect a Node with a DummyBox. + Takes as input two ints, a node_id and a dummybox_id. + Add in the `Nodes' table the dummybox_id value at the given node_id. + If the `dummybox_id' value is `0' the emulation link will be deleted. + + This operation is restricted to PIs and techs owner of the site + on which the DummyBox and the Node are located. + Admins may create emulation links for any site, but the DummyBox + and the Node must belong to the same site. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepts = [ + Auth(), + Parameter(int, 'node_id'), + Parameter(int, 'dummybox_id'), + ] + + returns = Parameter(int, '1 is successful, fault otherwise') + + # XXX + # check for the node_id existence, dummybox_id existence + # and update of the Nodes table, should be atomic. + # At the moment they are not. + # + # Before to create the link we do the following checks: + # - if node exist; + # - if dummybox_exist; + # - right roles (admin, pi, tech) + # - if node_id, dummybox_id and site_id match. + + def call(self, auth, node_id, dummybox_id): + + assert self.caller is not None + + # check for node existence + nodes = Nodes(self.api, [node_id]) + if not nodes: + raise PLCInvalidArgument, "No %s Node present" % node_id + + node = nodes[0] + + # we need to manage the special case when the + # dummybox_id is `0' because this means + # that we want to delete the link. + if (dummybox_id != 0): + + # check for dummybox existence, + dummyboxes = DummyBoxes(self.api, [dummybox_id]) + if not dummyboxes: + raise PLCInvalidArgument, "No %s DummyBox present" % dummybox_id + + dummybox = dummyboxes[0] + + # check if the node and the dummybox_id + # belong to the same site + if (node['site_id'] != dummybox['site_id']): + raise PLCInvalidArgument, \ + "The DummyBox must belog to the same site of the Node" + + site = node['site_id'] + + # check for site existence + sites = Sites(self.api, [site]) + if not sites: + raise PLCInvalidArgument, "No such site %s" % site + + # check for roles permission to call this method + if 'admin' not in self.caller['roles']: + if site not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to create this link" + + # we need to update only the dummybox_id field + # so build the dict structure with one field + node_fields = dict([('dummybox_id', dummybox_id)]) + node.update(node_fields) + node.sync() + + # log the creation/deletion of this link + if (dummybox_id != 0): + self.event_objects = {'DummyBox': [dummybox['dummybox_id']]} + self.message = "DummyBox %s linked to node %s" % \ + (dummybox_id, node_id) + else: + self.event_objects = {'Node': [node['node_id']]} + self.message = "Emulation link for node %s deleted" % node_id + + return 1 diff --git a/PLC/Methods/__init__.py b/PLC/Methods/__init__.py index 45c92284..5489b27b 100644 --- a/PLC/Methods/__init__.py +++ b/PLC/Methods/__init__.py @@ -2,6 +2,7 @@ methods = """ AddAddressType AddAddressTypeToAddress AddBootState +AddDummyBox AddConfFile AddConfFileToNodeGroup AddConfFileToNode @@ -111,6 +112,7 @@ DeleteBootState DeleteConfFileFromNodeGroup DeleteConfFileFromNode DeleteConfFile +DeleteDummyBox DeleteInitScript DeleteKey DeleteKeyType @@ -147,6 +149,9 @@ GetAddressTypes GetBootMedium GetBootStates GetConfFiles +GetDummyBoxes +GetDummyBoxMedium +GetDummyBoxUsers GetEventObjects GetEvents GetInitScripts @@ -210,6 +215,8 @@ system.multicall UpdateAddress UpdateAddressType UpdateConfFile +UpdateDummyBox +UpdateEmulationLink UpdateInitScript UpdateKey UpdateMessage diff --git a/PLC/Nodes.py b/PLC/Nodes.py index 08612f61..8764bd63 100644 --- a/PLC/Nodes.py +++ b/PLC/Nodes.py @@ -62,6 +62,7 @@ class Node(Row): 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"), 'pcu_ids': Parameter([int], "List of PCUs that control this node"), 'ports': Parameter([int], "List of PCU ports that this node is connected to"), + 'dummybox_id': Parameter(int, "Dummynet box presence"), 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True), 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True), } diff --git a/PLC/Sessions.py b/PLC/Sessions.py index e0a57b32..3d563e8e 100644 --- a/PLC/Sessions.py +++ b/PLC/Sessions.py @@ -10,6 +10,7 @@ from PLC.Debug import profile from PLC.Table import Row, Table from PLC.Persons import Person, Persons from PLC.Nodes import Node, Nodes +from PLC.DummyBoxes import DummyBox, DummyBoxes class Session(Row): """ @@ -19,11 +20,12 @@ class Session(Row): table_name = 'sessions' primary_key = 'session_id' - join_tables = ['person_session', 'node_session'] + join_tables = ['person_session', 'node_session', 'dummybox_session'] fields = { 'session_id': Parameter(str, "Session key"), 'person_id': Parameter(int, "Account identifier, if applicable"), 'node_id': Parameter(int, "Node identifier, if applicable"), + 'dummybox_id': Parameter(int, "Dummybox identifier, if applicable"), 'expires': Parameter(int, "Date and time when session expires, in seconds since UNIX epoch"), } @@ -43,6 +45,14 @@ class Session(Row): add = Row.add_object(Node, 'node_session') add(self, node, commit = commit) + def add_dummybox(self, dummybox, commit = True): + # DummyBoxes can have only one session at a time + self.api.db.do("DELETE FROM dummybox_session WHERE dummybox_id = %d" % \ + dummybox['dummybox_id']) + + add = Row.add_object(DummyBox, 'dummybox_session') + add(self, dummybox, commit = commit) + def sync(self, commit = True, insert = None): if not self.has_key('session_id'): # Before a new session is added, delete expired sessions diff --git a/PLC/__init__.py b/PLC/__init__.py index d5ddda8a..4188c316 100644 --- a/PLC/__init__.py +++ b/PLC/__init__.py @@ -8,6 +8,7 @@ BootStates ConfFiles Config Debug +DummyBoxes EventObjects Events Faults diff --git a/migrations/010-down-dummyboxes.sql b/migrations/010-down-dummyboxes.sql new file mode 100644 index 00000000..af8885f8 --- /dev/null +++ b/migrations/010-down-dummyboxes.sql @@ -0,0 +1,54 @@ +-- +-- Marta Carbone - UniPi +-- +-- migration 010 +-- +-- +-- Revert dummynetbox changes in the database. +-- + +---------- drop our views +DROP VIEW view_nodes; +DROP VIEW view_dummyboxes; +DROP VIEW dummybox_nodes; +DROP VIEW view_sessions; + +---------- delete fields in nodes table +ALTER TABLE nodes DROP COLUMN dummybox_id; + +---------- restore view_nodes +CREATE OR REPLACE VIEW view_nodes AS +SELECT +nodes.node_id, +nodes.hostname, +nodes.site_id, +nodes.boot_state, +nodes.deleted, +nodes.model, +nodes.boot_nonce, +nodes.version, +nodes.ssh_rsa_key, +nodes.key, +CAST(date_part('epoch', nodes.date_created) AS bigint) AS date_created, +CAST(date_part('epoch', nodes.last_updated) AS bigint) AS last_updated, +CAST(date_part('epoch', nodes.last_contact) AS bigint) AS last_contact, +peer_node.peer_id, +peer_node.peer_node_id, +COALESCE((SELECT nodenetwork_ids FROM node_nodenetworks WHERE node_nodenetworks.node_id = nodes.node_id), '{}') AS nodenetwork_ids, +COALESCE((SELECT nodegroup_ids FROM node_nodegroups WHERE node_nodegroups.node_id = nodes.node_id), '{}') AS nodegroup_ids, +COALESCE((SELECT slice_ids FROM node_slices WHERE node_slices.node_id = nodes.node_id), '{}') AS slice_ids, +COALESCE((SELECT slice_ids_whitelist FROM node_slices_whitelist WHERE node_slices_whitelist.node_id = nodes.node_id), '{}') AS slice_ids_whitelist, +COALESCE((SELECT pcu_ids FROM node_pcus WHERE node_pcus.node_id = nodes.node_id), '{}') AS pcu_ids, +COALESCE((SELECT ports FROM node_pcus WHERE node_pcus.node_id = nodes.node_id), '{}') AS ports, +COALESCE((SELECT conf_file_ids FROM node_conf_files WHERE node_conf_files.node_id = nodes.node_id), '{}') AS conf_file_ids, +node_session.session_id AS session +FROM nodes +LEFT JOIN peer_node USING (node_id) +LEFT JOIN node_session USING (node_id); +---------- delete dummyboxes table +DROP TABLE dummyboxes; +DROP TABLE dummybox_session; + +---------- revert subversion +UPDATE plc_db_version SET subversion = 9; +SELECT subversion from plc_db_version; diff --git a/migrations/010-up-dummyboxes.sql b/migrations/010-up-dummyboxes.sql new file mode 100644 index 00000000..15dc12b8 --- /dev/null +++ b/migrations/010-up-dummyboxes.sql @@ -0,0 +1,116 @@ +-- +-- Marta Carbone - UniPi +-- +-- migration 010 +-- +-- Add DummyBox support to the database. +-- +-- This file modify the main database adding some tables/views. +-- New tables are added to manage dummyboxes and dummyboxes sessions. +-- The `nodes' table needs to be modified in order to know +-- if a Dummybox is connected to that node. +-- + +---------- Add dummyboxes table and view +-- DummyBoxes table +CREATE TABLE dummyboxes ( + -- Mandatory + dummybox_id serial PRIMARY KEY, -- Dummynet Box identifier + hostname text NOT NULL, -- DummyBox fully hostname + site_id integer REFERENCES sites NOT NULL, -- At which site + + key text, -- DummyBox generated by API when iso image is build + ip text NOT NULL, -- IP address + netmask text NOT NULL, -- IP address + gateway text NOT NULL, -- IP address + dns1 text NOT NULL, -- IP address + dns2 text, -- IP address + + deleted boolean NOT NULL DEFAULT false -- Is deleted +) WITH OIDS; +CREATE INDEX dummyboxes_dummmybox_idx ON dummyboxes (dummybox_id) WHERE deleted IS false; + +-- 0 have a special value here, it means that +-- we have no DummyBox associated with this node +ALTER TABLE nodes ADD dummybox_id integer Default 0; + +CREATE VIEW dummybox_nodes AS +SELECT dummybox_id, +array_accum(node_id) AS node_ids +FROM nodes +GROUP BY dummybox_id; + +-- View for the dummyboxes table +CREATE VIEW view_dummyboxes AS +SELECT +dummyboxes.dummybox_id, +dummyboxes.hostname, +dummyboxes.site_id, +dummyboxes.key, +dummyboxes.ip, +dummyboxes.netmask, +dummyboxes.gateway, +dummyboxes.dns1, +dummyboxes.dns2, +dummyboxes.deleted, +COALESCE((SELECT node_ids FROM dummybox_nodes WHERE dummybox_nodes.dummybox_id = dummyboxes.dummybox_id), '{}') AS node_ids +FROM dummyboxes; + +---------- Modify the view_nodes view +DROP VIEW view_nodes; + +CREATE VIEW view_nodes AS +SELECT +nodes.node_id, +nodes.hostname, +nodes.site_id, +nodes.dummybox_id, +nodes.boot_state, +nodes.deleted, +nodes.model, +nodes.boot_nonce, +nodes.version, +nodes.ssh_rsa_key, +nodes.key, +CAST(date_part('epoch', nodes.date_created) AS bigint) AS date_created, +CAST(date_part('epoch', nodes.last_updated) AS bigint) AS last_updated, +CAST(date_part('epoch', nodes.last_contact) AS bigint) AS last_contact, +peer_node.peer_id, +peer_node.peer_node_id, +COALESCE((SELECT nodenetwork_ids FROM node_nodenetworks WHERE node_nodenetworks.node_id = nodes.node_id), '{}') AS nodenetwork_ids, +COALESCE((SELECT nodegroup_ids FROM node_nodegroups WHERE node_nodegroups.node_id = nodes.node_id), '{}') AS nodegroup_ids, +COALESCE((SELECT slice_ids FROM node_slices WHERE node_slices.node_id = nodes.node_id), '{}') AS slice_ids, +COALESCE((SELECT slice_ids_whitelist FROM node_slices_whitelist WHERE node_slices_whitelist.node_id = nodes.node_id), '{}') AS slice_ids_whitelist, +COALESCE((SELECT pcu_ids FROM node_pcus WHERE node_pcus.node_id = nodes.node_id), '{}') AS pcu_ids, +COALESCE((SELECT ports FROM node_pcus WHERE node_pcus.node_id = nodes.node_id), '{}') AS ports, +COALESCE((SELECT conf_file_ids FROM node_conf_files WHERE node_conf_files.node_id = nodes.node_id), '{}') AS conf_file_ids, +node_session.session_id AS session +FROM nodes +LEFT JOIN peer_node USING (node_id) +LEFT JOIN node_session USING (node_id); + +---------- Authenticated sessions, create a new table and modify the related view +-- Create a new table to manage dummybox session +CREATE TABLE dummybox_session ( + dummybox_id integer REFERENCES dummyboxes NOT NULL, -- Dummybox identifier + session_id text REFERENCES sessions NOT NULL, -- Session identifier + UNIQUE (dummybox_id), -- Dummyboxes can have only one session + UNIQUE (session_id) -- Sessions are unique +) WITH OIDS; + +DROP VIEW view_sessions; +CREATE VIEW view_sessions AS +SELECT +sessions.session_id, +CAST(date_part('epoch', sessions.expires) AS bigint) AS expires, +person_session.person_id, +node_session.node_id, +dummybox_session.dummybox_id +FROM sessions +LEFT JOIN person_session USING (session_id) +LEFT JOIN node_session USING (session_id) +LEFT JOIN dummybox_session USING (session_id); + +---------- bump subversion +UPDATE plc_db_version SET subversion = 10; +SELECT subversion from plc_db_version;