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
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)
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"
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
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):
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):
"""
--- /dev/null
+#
+# 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 <marta.carbone@iet.unipi.it>
+# 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)
--- /dev/null
+#
+# 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']
--- /dev/null
+#
+# 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
'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,
( 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()
--- /dev/null
+#
+# 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())
--- /dev/null
+#
+# Marta Carbone - UniPi
+# $Id:$
+#
+# This Method returns a list of tuples formatted as follow:
+#
+# <key_id> <node_ip> <slicename>
+#
+# 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
--- /dev/null
+#
+# 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
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):
"""
successfully, faults otherwise.
"""
+ #roles = ['admin', 'pi', 'user', 'tech', 'node', 'dummybox']
roles = ['admin', 'pi', 'user', 'tech', 'node']
accepts = [Auth()]
returns = Session.fields['session_id']
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']
--- /dev/null
+#
+# 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']
--- /dev/null
+#
+# 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
AddAddressType
AddAddressTypeToAddress
AddBootState
+AddDummyBox
AddConfFile
AddConfFileToNodeGroup
AddConfFileToNode
DeleteConfFileFromNodeGroup
DeleteConfFileFromNode
DeleteConfFile
+DeleteDummyBox
DeleteInitScript
DeleteKey
DeleteKeyType
GetBootMedium
GetBootStates
GetConfFiles
+GetDummyBoxes
+GetDummyBoxMedium
+GetDummyBoxUsers
GetEventObjects
GetEvents
GetInitScripts
UpdateAddress
UpdateAddressType
UpdateConfFile
+UpdateDummyBox
+UpdateEmulationLink
UpdateInitScript
UpdateKey
UpdateMessage
'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),
}
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):
"""
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"),
}
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
ConfFiles
Config
Debug
+DummyBoxes
EventObjects
Events
Faults
--- /dev/null
+--
+-- 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;
--- /dev/null
+--
+-- 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;