First draft for leases
authorThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Mon, 3 May 2010 14:44:26 +0000 (14:44 +0000)
committerThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Mon, 3 May 2010 14:44:26 +0000 (14:44 +0000)
WARNING: this change sets the SQL timezone to UTC
leases are not yet exposed to GetSlivers in this delivery
all code for handling time (Timestamp, Duration) gathered in the new PLC/Timestamp.py file

21 files changed:
Makefile
PLC/Filter.py
PLC/LeaseFilter.py [new file with mode: 0644]
PLC/Leases.py [new file with mode: 0644]
PLC/Methods/AddLeases.py [new file with mode: 0644]
PLC/Methods/AddNode.py
PLC/Methods/DeleteLeases.py [new file with mode: 0644]
PLC/Methods/GetLeases.py [new file with mode: 0644]
PLC/Methods/UpdateLeases.py [new file with mode: 0644]
PLC/Methods/__init__.py
PLC/Nodes.py
PLC/Parameter.py
PLC/Persons.py
PLC/PostgreSQL.py
PLC/Slices.py
PLC/Table.py
PLC/Timestamp.py [new file with mode: 0644]
PLC/__init__.py
migrations/101-down-leases.sql [new file with mode: 0644]
migrations/101-up-leases.sql [new file with mode: 0644]
plc.d/postgresql

index 034846f..3cc5ce7 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -123,6 +123,7 @@ ifeq (,$(SSHURL))
 else
        +$(RSYNC) plcsh PLC planetlab5.sql migrations $(SSHURL)/usr/share/plc_api/
        +$(RSYNC) db-config.d/ $(SSHURL)/etc/planetlab/db-config.d/
 else
        +$(RSYNC) plcsh PLC planetlab5.sql migrations $(SSHURL)/usr/share/plc_api/
        +$(RSYNC) db-config.d/ $(SSHURL)/etc/planetlab/db-config.d/
+       +$(RSYNC) plc.d/ $(SSHURL)/etc/plc.d/
        $(SSHCOMMAND) exec apachectl graceful
 endif
 
        $(SSHCOMMAND) exec apachectl graceful
 endif
 
index e5c6c13..5d0d22d 100644 (file)
@@ -31,9 +31,16 @@ class Filter(Parameter, dict):
     filter should be a dictionary of field names and values
     representing  the criteria for filtering. 
     example : filter = { 'hostname' : '*.edu' , site_id : [34,54] }
     filter should be a dictionary of field names and values
     representing  the criteria for filtering. 
     example : filter = { 'hostname' : '*.edu' , site_id : [34,54] }
+
+
     Whether the filter represents an intersection (AND) or a union (OR) 
     Whether the filter represents an intersection (AND) or a union (OR) 
-    of these criteria is determined by the join_with argument 
-    provided to the sql method below
+    of these criteria is determined as follows:
+    * if the dictionnary has the '-AND' or the '-OR' key, this is chosen
+    * otherwise, the join_with argument, as provided to the sql method below, 
+      is expected to hold the 'AND' or 'OR' string 
+      this argument defaults to 'AND' and in most of the code, this default applies 
+      as the join_with argument is left unspecified
+
 
     Special features:
 
 
     Special features:
 
@@ -73,11 +80,16 @@ class Filter(Parameter, dict):
     * '-LIMIT' : the amount of rows to be returned 
     example : filter = { '-OFFSET' : 100, '-LIMIT':25}
 
     * '-LIMIT' : the amount of rows to be returned 
     example : filter = { '-OFFSET' : 100, '-LIMIT':25}
 
+
     Here are a few realistic examples
 
     Here are a few realistic examples
 
-    GetNodes ( { 'node_type' : 'regular' , 'hostname' : '*.edu' , '-SORT' : 'hostname' , '-OFFSET' : 30 , '-LIMIT' : 25 } )
+    GetNodes ( { 'node_type' : 'regular' , 'hostname' : '*.edu' , 
+                 '-SORT' : 'hostname' , '-OFFSET' : 30 , '-LIMIT' : 25 } )
       would return regular (usual) nodes matching '*.edu' in alphabetical order from 31th to 55th
 
       would return regular (usual) nodes matching '*.edu' in alphabetical order from 31th to 55th
 
+    GetNodes ( { '~peer_id' : None } ) 
+      returns the foreign nodes - that have an integer peer_id 
+
     GetPersons ( { '|role_ids' : [ 20 , 40] } )
       would return all persons that have either pi (20) or tech (40) roles
 
     GetPersons ( { '|role_ids' : [ 20 , 40] } )
       would return all persons that have either pi (20) or tech (40) roles
 
@@ -88,6 +100,9 @@ class Filter(Parameter, dict):
       all 4 forms are equivalent and would return all admin users in the system
     """
 
       all 4 forms are equivalent and would return all admin users in the system
     """
 
+    debug=False
+#    debug=True
+
     def __init__(self, fields = {}, filter = {}, doc = "Attribute filter"):
         # Store the filter in our dict instance
         dict.__init__(self, filter)
     def __init__(self, fields = {}, filter = {}, doc = "Attribute filter"):
         # Store the filter in our dict instance
         dict.__init__(self, filter)
@@ -106,6 +121,15 @@ class Filter(Parameter, dict):
         Returns a SQL conditional that represents this filter.
         """
 
         Returns a SQL conditional that represents this filter.
         """
 
+        if self.has_key('-AND'): 
+            del self['-AND']
+            join_with='AND'
+        if self.has_key('-OR'): 
+            del self['-OR']
+            join_with='OR'
+
+        self.join_with=join_with
+
         # So that we always return something
         if join_with == "AND":
             conditionals = ["True"]
         # So that we always return something
         if join_with == "AND":
             conditionals = ["True"]
@@ -260,5 +284,5 @@ class Filter(Parameter, dict):
             clip_part += " ORDER BY " + ",".join(sorts)
         if clips:
             clip_part += " " + " ".join(clips)
             clip_part += " ORDER BY " + ",".join(sorts)
         if clips:
             clip_part += " " + " ".join(clips)
-#      print 'where_part=',where_part,'clip_part',clip_part
+        if Filter.debug: print 'Filter.sql: where_part=',where_part,'clip_part',clip_part
         return (where_part,clip_part)
         return (where_part,clip_part)
diff --git a/PLC/LeaseFilter.py b/PLC/LeaseFilter.py
new file mode 100644 (file)
index 0000000..0d13f9e
--- /dev/null
@@ -0,0 +1,174 @@
+#
+# $Id$
+# $URL$
+# Thierry Parmentelat -- INRIA
+#
+# Utilities for filtering on leases
+# 
+
+from types import StringTypes
+from PLC.Faults import *
+from PLC.Filter import Filter
+from PLC.Parameter import Parameter, Mixed
+from PLC.Timestamp import Timestamp
+
+# supersede the generic Filter class to support time intersection
+class LeaseFilter (Filter):
+    
+    # general notes on input parameters
+    # int_timestamp: number of seconds since the epoch
+    # str_timestamp: see Timestamp.sql_validate
+    # timeslot: a tuple (from,until), each being either int_timestamp or str_timestamp
+
+    local_fields = { 'alive': Mixed ( Parameter (int,  "int_timestamp: leases alive at that time"),
+                                      Parameter (str,  "str_timestamp: leases alive at that time"),
+                                      Parameter (tuple,"timeslot: the leases alive during this timeslot")),
+                     'clip':  Mixed ( Parameter (int,  "int_timestamp: leases alive after that time"),
+                                      Parameter (str,  "str_timestamp: leases alive after at that time"),
+                                      Parameter (tuple,"timeslot: the leases alive during this timeslot")),
+                     }
+
+    def __init__(self, fields = {}, filter = {}, 
+                 doc = "Lease filter -- adds the 'alive' and 'clip' capabilities for filtering on leases"):
+        Filter.__init__(self,fields,filter,doc)
+        self.fields.update (LeaseFilter.local_fields) 
+
+    
+    ## canonical type
+    @staticmethod
+    def quote (timestamp): return Timestamp.cast_long(timestamp)
+
+    ## basic SQL utilities
+    @staticmethod
+    def sql_time_intersect (f1,u1,f2,u2):
+        # either f2 is in [f1,u1], or u2 is in [f1,u1], or f2<=f1<=u1<=u2
+        return ("((%(f1)s <= %(f2)s) AND (%(f2)s <= %(u1)s)) " + \
+            "OR ((%(f1)s <= %(u2)s) AND (%(u2)s <= %(u1)s)) " + \
+            "OR ((%(f2)s<=%(f1)s) AND (%(u1)s<=%(u2)s))")%locals()
+
+    @staticmethod
+    def time_in_range (timestamp,f1,u1):
+        return Timestamp.cast_long(f1) <= Timestamp.cast_long(timestamp) \
+           and Timestamp.cast_long(timestamp) <= Timestamp.cast_long(u1)
+
+    @staticmethod
+    def sql_time_in_range (timestamp,f1,u1):
+        # is timestamp in [f1,u1]
+        return "((%(f1)s <= %(timestamp)s) AND (%(timestamp)s <= %(u1)s))"%locals()
+
+    @staticmethod
+    def sql_timeslot_after (f1,u1,mark):
+        # is the lease alive after mark, i.e. u1 >= mark
+        return "(%(u1)s >= %(mark)s)"%locals()
+
+
+    ## hooks for the local fields
+    def sql_alive (self, alive):
+        if isinstance (alive,int) or isinstance (alive, StringTypes):
+            # the lease is alive at that time if from <= alive <= until
+            alive=LeaseFilter.quote(alive)
+            return LeaseFilter.sql_time_in_range(alive,'t_from','t_until')
+        elif isinstance (alive,tuple):
+            (f,u)=alive
+            f=LeaseFilter.quote(f)
+            u=LeaseFilter.quote(u)
+            return LeaseFilter.sql_time_intersect (f,u,'t_from','t_until')
+        else: raise PLCInvalidArgument ("LeaseFilter: alive field %r"%alive)
+
+    def sql_clip (self, clip):
+        if isinstance (clip,int) or isinstance (clip, StringTypes):
+            start=LeaseFilter.quote(clip)
+            return LeaseFilter.sql_timeslot_after('t_from','t_until',start)
+        elif isinstance (clip,tuple):
+            (f,u)=clip
+            f=LeaseFilter.quote(f)
+            u=LeaseFilter.quote(u)
+            return LeaseFilter.sql_time_intersect(f,u,'t_from','t_until')
+        else: raise PLCInvalidArgument ("LeaseFilter: clip field %r"%clip)
+
+
+    ## supersede the generic Filter 'sql' method
+    def sql(self, api, join_with = "AND"):
+        # preserve locally what belongs to us, hide it from the superclass
+        # self.local is a dict    local_key : user_value
+        # self.negation is a dict  local_key : string
+        self.local={}
+        self.negation={}
+        for (k,v) in LeaseFilter.local_fields.items():
+            if self.has_key(k):
+                self.local[k]=self[k]
+                del self[k]
+                self.negation[k]=""
+            elif self.has_key('~'+k):
+                self.local[k]=self['~'+k]
+                del self['~'+k]
+                self.negation[k]="NOT "
+        # run the generic filtering code
+        (where_part,clip_part) = Filter.sql(self,api,join_with)
+        for (k,v) in self.local.items():
+            try:
+                # locate hook function associated with key
+                method=LeaseFilter.__dict__['sql_'+k]
+                where_part += " %s %s(%s)" %(self.join_with,self.negation[k],method(self,self.local[k]))
+            except Exception,e:
+                raise PLCInvalidArgument,"LeaseFilter: something wrong with filter key %s, val was %r -- %r"%(k,v,e)
+        if Filter.debug: print 'LeaseFilter.sql: where_part=',where_part,'clip_part',clip_part
+        return (where_part,clip_part)
+
+######## xxx not sure where this belongs yet
+# given a set of nodes, and a timeslot, 
+# returns the available leases that have at least a given duration
+def free_leases (api, node_ids, t_from, t_until, min_duration):
+    
+    # get the leases for these nodes and timeslot
+    filter = {'node_id':node_ids,
+              'clip': (t_from, t_until),
+              # sort by node, and inside one node, chronologically
+              '-SORT' : ('node_id','t_from'),
+              }
+    leases = Leases (api, filter)
+
+    result=[]
+
+    # sort node_ids
+    node_ids.sort()
+
+    # scan nodes from the input
+    input_node_id=0
+    # scan nodes from the leases
+    lease_node_id=0
+
+    return '?? what now ??'
+
+def node_free_leases (node_id, node_leases, t_from, t_until):
+
+    # no lease yet : return one solid lease
+    if not node_leases:
+        return [ {'node_id':node_id,
+                  't_from':t_from,
+                  't_until':t_until} ]
+
+    result=[]
+    current_time=t_from
+    is_on=LeaseFilter.time_in_range(node_leases[0]['t_from'],t_from,t_until)
+
+    while True:
+#        print 'DBG','current_time',current_time,'is_on',is_on,'result',result
+        # lease is active
+        if is_on:
+            current_time=node_leases[0]['t_until']
+            is_on=False
+            del node_leases[0]
+            if not node_leases: return result
+        # free, has no remaining lease
+        elif not node_leases:
+            result.append( {'node_id':node_id, 't_from':current_time, 't_until': t_until} )
+            return result
+        # free and has remaining leases
+        else:
+            next_time = node_leases[0]['t_from']
+            result.append( {'node_id':node_id,'t_from':current_time,'t_until':next_time})
+            current_time = next_time
+            is_on=True
+
+
diff --git a/PLC/Leases.py b/PLC/Leases.py
new file mode 100644 (file)
index 0000000..ebe2752
--- /dev/null
@@ -0,0 +1,97 @@
+#
+# Functions for interacting with the leases table in the database
+#
+# $Id$
+# $URL$
+# Thierry Parmentelat -- INRIA
+#
+
+from datetime import datetime
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Table import Row, Table
+from PLC.Nodes import Node, Nodes
+from PLC.Slices import Slice, Slices
+from PLC.LeaseFilter import LeaseFilter
+from PLC.Timestamp import Timestamp
+
+class Lease(Row):
+    """
+    Representation of a row in the leases table. To use, optionally
+    instantiate with a dict of values. Update as you would a
+    dict. Commit to the database with sync().
+    """
+
+    table_name = 'leases'
+    primary_key = 'lease_id'
+    join_tables = [ ]
+    fields = {
+        # native
+        'lease_id': Parameter(int, "Lease identifier"),
+        't_from': Timestamp.Parameter("timeslot start"),
+        't_until': Timestamp.Parameter("timeslot end"),
+        'node_id': Node.fields['node_id'],
+        'slice_id': Slice.fields['slice_id'],
+
+        # derived
+        'hostname': Node.fields['hostname'],
+        'node_type': Node.fields['node_type'],
+        'name': Slice.fields['name'],
+        'site_id': Slice.fields['site_id'],
+        'duration': Parameter(int, "duration in seconds"),
+        'expired' : Parameter(bool, "time slot is over"),
+        }
+
+    related_fields = { }
+
+    # leases do not have arbitrary boundaries
+    # f_from and t_until are rounded to this period of time
+    # initial model is one hour
+    granularity = 60*60
+
+    def validate_time (self, timestamp, round_up):
+        # convert to long
+        timestamp = Timestamp.cast_long(timestamp)
+        # the trick for rounding up rather than down
+        if round_up: timestamp += (Lease.granularity-1)
+        # round down
+        timestamp = (timestamp/Lease.granularity) * Lease.granularity
+        # return a SQL string
+        return Timestamp.sql_validate_utc(timestamp)
+
+    # round UP 
+    def validate_t_from(self,timestamp):
+        return self.validate_time (timestamp, round_up=True)
+    # round DOWN 
+    def validate_t_until (self, timestamp):
+        return self.validate_time (timestamp, round_up=False)
+
+class Leases(Table):
+    """
+    Representation of row(s) from the leases table in the
+    database.
+    """
+
+    def __init__(self, api, lease_filter = None, columns = None):
+        Table.__init__(self, api, Lease, columns)
+
+        # the view that we're selecting upon: start with view_leases
+        view = "view_leases"
+        sql = "SELECT %s FROM %s WHERE true" % (", ".join(self.columns.keys()),view)
+
+
+        if lease_filter is not None:
+
+            if isinstance (lease_filter, int):
+                lease_filter = Filter (Lease.fields, {'lease_id': lease_filter})
+            elif isinstance(lease_filter, (list, tuple, set)):
+                lease_filter = Filter(Lease.fields, {'lease_id': lease_filter})
+            elif isinstance(lease_filter, dict):
+                lease_filter = LeaseFilter(Lease.fields, lease_filter)
+            else:
+                raise PLCInvalidArgument, "Wrong lease filter %r"%lease_filter
+            sql += " AND (%s) %s" % lease_filter.sql(api)
+
+        self.selectall(sql)
diff --git a/PLC/Methods/AddLeases.py b/PLC/Methods/AddLeases.py
new file mode 100644 (file)
index 0000000..ea2f3ca
--- /dev/null
@@ -0,0 +1,100 @@
+# $Id$
+# $URL$
+# Thierry Parmentelat -- INRIA
+
+from PLC.Faults import *
+from PLC.Auth import Auth
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Table import Row
+
+from PLC.Leases import Leases, Lease
+from PLC.Nodes import Nodes, Node
+from PLC.Slices import Slices, Slice
+from PLC.Timestamp import Timestamp
+
+can_update = ['name', 'instantiation', 'url', 'description', 'max_nodes']
+
+class AddLeases(Method):
+    """
+    Adds a new lease. 
+    Mandatory arguments are node(s), slice, t_from and t_until
+    times can be either integers, datetime's, or human readable (see Timestamp)
+
+    PIs may only add leases associated with their own sites (i.e.,
+    to a slice that belongs to their site). 
+    Users may only add leases associated with their own slices.
+
+    Returns the new lease_ids if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'user']
+
+    accepts = [
+        Auth(),
+       Mixed(Node.fields['node_id'],[Node.fields['node_id']],
+              Node.fields['hostname'],[Node.fields['hostname']],),
+        Mixed(Slice.fields['slice_id'],
+              Slice.fields['name']),
+        Mixed(Lease.fields['t_from']),
+        Mixed(Lease.fields['t_until']),
+        ]
+
+    returns = Parameter(dict, " 'new_ids' is the list of newly created ids, 'errors' is a list of error strings")
+
+    def call(self, auth, node_id_or_hostname_s, slice_id_or_name, t_from, t_until):
+
+        # xxx - round to plain hours somewhere
+
+        # Get node information
+        nodes = Nodes(self.api, node_id_or_hostname_s)
+        if not nodes:
+            raise PLCInvalidArgument, "No such node(s) %r"%node_id_or_hostname_s
+        for node in nodes:
+            if node['node_type'] != 'reservable':
+                raise PLCInvalidArgument, "Node %s is not reservable"%node['hostname']
+
+        # Get slice information
+        slices = Slices(self.api, [slice_id_or_name])
+        if not slices:
+            raise PLCInvalidArgument, "No such slice %r"%slice_id_or_name
+        slice = slices[0]
+
+        # check access
+        if 'admin' not in self.caller['roles']:
+            if self.caller['person_id'] in slice['person_ids']:
+                pass
+            elif 'pi' not in self.caller['roles']:
+                raise PLCPermissionDenied, "Not a member of the specified slice"
+            elif slice['site_id'] not in self.caller['site_ids']:
+                raise PLCPermissionDenied, "Specified slice not associated with any of your sites"
+
+        # normalize timestamps
+        t_from = Timestamp.sql_validate_utc(t_from)
+        t_until = Timestamp.sql_validate_utc(t_until)
+
+        ########## create stuff
+        errors=[]
+        result_ids=[]
+        for node in nodes:
+            if node['peer_id'] is not None:
+                errors.append("Cannot set lease on remote node %r"%node['hostname'])
+                continue
+            # let the DB check for time consistency
+            try:
+                lease = Lease (self.api, {'node_id':node['node_id'], 'slice_id': slice['slice_id'],
+                                          't_from':t_from, 't_until':t_until})
+                lease.sync()
+                result_ids.append(lease['lease_id'])
+            except Exception,e:
+                errors.append("Could not create lease on n=%s s=%s [%s .. %s] -- %r" % \
+                                  (node['hostname'],slice['name'],t_from,t_until,e))
+                nodes.remove(node)
+
+        self.event_objects = {'Slice': [slice['slice_id']],
+                              'Node': [node['node_id'] for node in nodes]}     
+        self.message = "New leases %r on n=%r s=%s [%s -> %s]" % \
+            (result_ids,[node['hostname'] for node in nodes],slice['name'],t_from,t_until)
+
+        return {'new_ids': result_ids,
+                'errors': errors}
index c787011..f2ff661 100644 (file)
@@ -89,7 +89,7 @@ class AddNode(Method):
                 UpdateNodeTag(self.api).__call__(auth,node_tags[0]['node_tag_id'],value)
 
         self.event_objects = {'Site': [site['site_id']],
                 UpdateNodeTag(self.api).__call__(auth,node_tags[0]['node_tag_id'],value)
 
         self.event_objects = {'Site': [site['site_id']],
-                            'Node': [node['node_id']]} 
-        self.message = "Node %s created" % node['node_id']
+                              'Node': [node['node_id']]}       
+        self.message = "Node %d=%s created" % (node['node_id'],node['hostname'])
         
         return node['node_id']
         
         return node['node_id']
diff --git a/PLC/Methods/DeleteLeases.py b/PLC/Methods/DeleteLeases.py
new file mode 100644 (file)
index 0000000..ff021b7
--- /dev/null
@@ -0,0 +1,62 @@
+# $Id$
+# $URL$
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+from PLC.Leases import Lease, Leases
+from PLC.Slices import Slice, Slices
+
+class DeleteLeases(Method):
+    """
+    Deletes a lease.
+
+    Users may only delete leases attached to their slices. 
+    PIs may delete any of the leases for slices at their sites, or any
+    slices of which they are members. Admins may delete any lease.
+
+    Returns 1 if successful, faults otherwise.
+    """
+
+    roles = ['admin', 'pi', 'tech', 'user']
+
+    accepts = [
+        Auth(),
+        Mixed(Lease.fields['lease_id'],[ Lease.fields['lease_id']]),
+        ]
+
+    returns = Parameter(int, '1 if successful')
+
+
+    def call(self, auth, lease_ids):
+        # Get associated lease details
+        leases = Leases(self.api, lease_ids)
+        if len(leases) != len(lease_ids):
+            raise PLCInvalidArgument, "Could not find all leases %r"%lease_ids
+
+        # fetch related slices
+        slices = Slices(self.api, [ lease['slice_id'] for lease in leases],['slice_id','person_ids'])
+        # create hash on slice_id
+        slice_map = dict ( [ (slice['slice_id'],slice) for slice in slices ] )
+
+        lease_ids=[lease['lease_id'] for lease in leases]
+        for lease in leases:
+            if 'admin' not in self.caller['roles']:
+                slice=slice_map[lease['slice_id']]
+                # check slices only once
+                if not slice.has_key('verified'):
+                    if self.caller['person_id'] in slice['person_ids']:
+                        pass
+                    elif 'pi' not in self.caller['roles']:
+                        raise PLCPermissionDenied, "Not a member of slice %r"%slice['name']
+                    elif slice['site_id'] not in self.caller['site_ids']:
+                        raise PLCPermissionDenied, "Slice %r not associated with any of your sites"%slice['name']
+                slice['verified']=True
+
+            lease.delete()
+       
+        # Logging variables
+        self.event_objects = {'Lease': lease_ids }
+        self.message = 'Leases %r deleted' % lease_ids
+
+        return 1
diff --git a/PLC/Methods/GetLeases.py b/PLC/Methods/GetLeases.py
new file mode 100644 (file)
index 0000000..f3dfa78
--- /dev/null
@@ -0,0 +1,61 @@
+# $Id$
+# $URL$
+# Thierry Parmentelat -- INRIA
+
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Filter import Filter
+from PLC.Auth import Auth
+from PLC.Leases import Lease, Leases, LeaseFilter
+
+class GetLeases(Method):
+    """
+    Returns an array of structs containing details about leases. If
+    lease_filter is specified and is an array of lease identifiers or
+    lease names, or a struct of lease attributes, only leases matching
+    the filter will be returned. If return_fields is specified, only the
+    specified details will be returned.
+
+    All leases are exposed to all users.
+
+    In addition to the usual filter capabilities, the following are supported:
+     * GetLeases ({ 'alive' : '2010-02-20 20:00' , <regular_filter_fields...> })
+       returns the leases that are active at that point in time
+     * GetLeases ({ 'alive' : ('2010-02-20 20:00' , '2010-02-20 21:00' ) , ... })
+       ditto for a time range
+
+    This is implemented in the LeaseFilter class; negation actually is supported
+    through the usual '~alive' form, although maybe not really useful.
+
+    """
+
+    roles = ['admin', 'pi', 'user', 'node']
+
+    accepts = [
+        Auth(),
+        Mixed(Lease.fields['lease_id'],
+              [Lease.fields['lease_id']],
+              LeaseFilter(Lease.fields)),    
+        Parameter([str], "List of fields to return", nullok = True)
+        ]
+
+    returns = [Lease.fields]
+
+    def call(self, auth, lease_filter = None, return_fields = None):
+
+        # Must query at least lease_id (see below)
+        if return_fields is not None and 'lease_id' not in return_fields:
+            return_fields.append('lease_id')
+            added_fields = True
+        else:
+            added_fields = False
+
+        leases = Leases(self.api, lease_filter, return_fields)
+
+        # Remove lease_id if not specified
+        if added_fields:
+            for lease in leases:
+               if 'lease_id' in lease:
+                   del lease['lease_id']
+
+        return leases
diff --git a/PLC/Methods/UpdateLeases.py b/PLC/Methods/UpdateLeases.py
new file mode 100644 (file)
index 0000000..6e8ebca
--- /dev/null
@@ -0,0 +1,116 @@
+# $Id$
+# $URL$
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.Timestamp import Timestamp, Duration
+
+from PLC.Leases import Lease, Leases
+from PLC.Slices import Slice, Slices
+
+can_update = lambda (field, value): field in ['t_from', 't_until', 'duration']
+
+class UpdateLeases(Method):
+    """
+    Updates the parameters of a (set of) existing lease(s) with the values in
+    lease_fields; specifically this applies to the timeslot definition.
+    As a convenience you may, in addition to the t_from and t_until fields,
+    you can also set the 'duration' field.
+
+    Users may only update leases attached to their slices. 
+    PIs may update any of the leases for slices at their sites, or any
+    slices of which they are members. Admins may update any lease.
+
+    Returns a dict of successfully updated lease_ids and error messages.
+    """
+
+    roles = ['admin', 'pi', 'tech', 'user']
+
+    lease_fields = dict(filter(can_update, Lease.fields.items()))
+
+    accepts = [
+        Auth(),
+        Mixed (Lease.fields['lease_id'],
+               [Lease.fields['lease_id']]),
+        lease_fields
+        ]
+
+    returns = Parameter(dict, " 'updated_ids' is the list ids updated, 'errors' is a list of error strings")
+
+    debug=False
+#    debug=True
+
+    def call(self, auth, lease_ids, input_fields):
+        input_fields = dict(filter(can_update, input_fields.items()))
+
+        if 'duration' in input_fields:
+            if 't_from' in input_fields and 't_until' in input_fields:
+                raise PLCInvalidArgument, "Cannot set t_from AND t_until AND duration"
+            # specify 'duration':0 to keep duration unchanged
+            if input_fields['duration'] : input_fields['duration']=Duration.validate(input_fields['duration'])
+
+        # Get lease information
+        leases = Leases(self.api, lease_ids)
+        if not leases:
+            raise PLCInvalidArgument, "No such leases %r"%lease_ids
+
+        # fetch related slices
+        slices = Slices(self.api, [ lease['slice_id'] for lease in leases],['slice_id','person_ids'])
+        # create hash on slice_id
+        slice_map = dict ( [ (slice['slice_id'],slice) for slice in slices ] )
+
+        updated_ids=[]
+        errors=[]
+
+        lease_ids=[lease['lease_id'] for lease in leases]
+        for lease in leases:
+
+            if 'admin' not in self.caller['roles']:
+                slice=slice_map[lease['slice_id']]
+                # check slices only once
+                if not slice.has_key('verified'):
+                    if self.caller['person_id'] in slice['person_ids']:
+                        pass
+                    elif 'pi' not in self.caller['roles']:
+                        raise PLCPermissionDenied, "Not a member of slice %r"%slice['name']
+                    elif slice['site_id'] not in self.caller['site_ids']:
+                        raise PLCPermissionDenied, "Slice %r not associated with any of your sites"%slice['name']
+            slice['verified']=True
+
+            try:
+                # we've ruled out already the case where all 3 (from, to, duration) where specified
+                if 'duration' not in input_fields:
+                    lease_fields=input_fields
+                else:
+                    # all arithmetics on longs..
+                    duration=Duration.validate(input_fields['duration'])
+                    # specify 'duration':0 to keep duration unchanged
+                    if not duration: 
+                        duration = Timestamp.cast_long(lease['t_until'])-Timestamp.cast_long(lease['t_from'])
+                    if 't_from' in input_fields:
+                        lease_fields={'t_from':input_fields['t_from'],
+                                      't_until':Timestamp.cast_long(input_fields['from'])+duration}
+                    elif 't_until' in input_fields:
+                        lease_fields={'t_from':Timestamp.cast_long(input_fields['t_until'])-duration,
+                                      't_until':input_fields['t_until']}
+                    else:
+                        lease_fields={'t_until':Timestamp.cast_long(lease['t_from'])+duration}
+                if UpdateLeases.debug: 
+                    print 'lease_fields',lease_fields
+                    for k in [ 't_from', 't_until'] :
+                        if k in lease_fields: print k,'aka',Timestamp.sql_validate_utc(lease_fields[k])
+                
+                lease.update(lease_fields)
+                lease.sync()
+                updated_ids.append(lease['lease_id'])
+            except Exception,e:
+                errors.append("Could not update lease %d - check new time limits ? -- %r"%(lease['lease_id'],e))
+       
+       # Logging variables
+       self.event_objects = {'Lease': updated_ids}
+       self.message = 'lease %r updated: %s' %  (lease_ids, ", ".join(input_fields.keys()))
+
+        return {'updated_ids' : updated_ids,
+                'errors' : errors }
index f885d10..3f95fce 100644 (file)
@@ -11,6 +11,7 @@ AddInitScript
 AddInterface
 AddInterfaceTag
 AddKeyType
 AddInterface
 AddInterfaceTag
 AddKeyType
+AddLeases
 AddMessage
 AddNetworkMethod
 AddNetworkType
 AddMessage
 AddNetworkMethod
 AddNetworkType
@@ -60,6 +61,7 @@ DeleteInterface
 DeleteInterfaceTag
 DeleteKey
 DeleteKeyType
 DeleteInterfaceTag
 DeleteKey
 DeleteKeyType
+DeleteLeases
 DeleteMessage
 DeleteNetworkMethod
 DeleteNetworkType
 DeleteMessage
 DeleteNetworkMethod
 DeleteNetworkType
@@ -101,6 +103,7 @@ GetInterfaceTags
 GetInterfaces
 GetKeyTypes
 GetKeys
 GetInterfaces
 GetKeyTypes
 GetKeys
+GetLeases
 GetMessages
 GetNetworkMethods
 GetNetworkTypes
 GetMessages
 GetNetworkMethods
 GetNetworkTypes
@@ -166,6 +169,7 @@ UpdateInitScript
 UpdateInterface
 UpdateInterfaceTag
 UpdateKey
 UpdateInterface
 UpdateInterfaceTag
 UpdateKey
+UpdateLeases
 UpdateMessage
 UpdateNode
 UpdateNodeGroup
 UpdateMessage
 UpdateNode
 UpdateNodeGroup
index 48f8eeb..8f25e28 100644 (file)
@@ -42,7 +42,7 @@ class Node(Row):
     primary_key = 'node_id'
     join_tables = [ 'slice_node', 'peer_node', 'slice_tag', 
                     'node_session', 'node_slice_whitelist', 
     primary_key = 'node_id'
     join_tables = [ 'slice_node', 'peer_node', 'slice_tag', 
                     'node_session', 'node_slice_whitelist', 
-                    'node_tag', 'conf_file_node', 'pcu_node', ]
+                    'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
     fields = {
         'node_id': Parameter(int, "Node identifier"),
         'node_type': Parameter(str,"Node type",max=20),
     fields = {
         'node_id': Parameter(int, "Node identifier"),
         'node_type': Parameter(str,"Node type",max=20),
index ad72106..3f0d42a 100644 (file)
@@ -61,7 +61,6 @@ class Mixed(tuple):
     def __new__(cls, *types):
         return tuple.__new__(cls, types)
 
     def __new__(cls, *types):
         return tuple.__new__(cls, types)
 
-
 def python_type(arg):
     """
     Returns the Python type of the specified argument, which may be a
 def python_type(arg):
     """
     Returns the Python type of the specified argument, which may be a
index 7f7bd3c..5a02403 100644 (file)
@@ -9,7 +9,6 @@
 #
 
 from types import StringTypes
 #
 
 from types import StringTypes
-from datetime import datetime
 try:
     from hashlib import md5
 except ImportError:
 try:
     from hashlib import md5
 except ImportError:
index 076a864..09132e2 100644 (file)
@@ -25,30 +25,6 @@ from pprint import pformat
 from PLC.Debug import profile, log
 from PLC.Faults import *
 
 from PLC.Debug import profile, log
 from PLC.Faults import *
 
-if not psycopg2:
-    is8bit = re.compile("[\x80-\xff]").search
-
-    def unicast(typecast):
-        """
-        pgdb returns raw UTF-8 strings. This function casts strings that
-        appear to contain non-ASCII characters to unicode objects.
-        """
-    
-        def wrapper(*args, **kwds):
-            value = typecast(*args, **kwds)
-
-            # pgdb always encodes unicode objects as UTF-8 regardless of
-            # the DB encoding (and gives you no option for overriding
-            # the encoding), so always decode 8-bit objects as UTF-8.
-            if isinstance(value, str) and is8bit(value):
-                value = unicode(value, "utf-8")
-
-            return value
-
-        return wrapper
-
-    pgdb.pgdbTypeCache.typecast = unicast(pgdb.pgdbTypeCache.typecast)
-
 class PostgreSQL:
     def __init__(self, api):
         self.api = api
 class PostgreSQL:
     def __init__(self, api):
         self.api = api
@@ -59,25 +35,19 @@ class PostgreSQL:
     def cursor(self):
         if self.connection is None:
             # (Re)initialize database connection
     def cursor(self):
         if self.connection is None:
             # (Re)initialize database connection
-            if psycopg2:
-                try:
-                    # Try UNIX socket first
-                    self.connection = psycopg2.connect(user = self.api.config.PLC_DB_USER,
-                                                       password = self.api.config.PLC_DB_PASSWORD,
-                                                       database = self.api.config.PLC_DB_NAME)
-                except psycopg2.OperationalError:
-                    # Fall back on TCP
-                    self.connection = psycopg2.connect(user = self.api.config.PLC_DB_USER,
-                                                       password = self.api.config.PLC_DB_PASSWORD,
-                                                       database = self.api.config.PLC_DB_NAME,
-                                                       host = self.api.config.PLC_DB_HOST,
-                                                       port = self.api.config.PLC_DB_PORT)
-                self.connection.set_client_encoding("UNICODE")
-            else:
-                self.connection = pgdb.connect(user = self.api.config.PLC_DB_USER,
-                                               password = self.api.config.PLC_DB_PASSWORD,
-                                               host = "%s:%d" % (api.config.PLC_DB_HOST, api.config.PLC_DB_PORT),
-                                               database = self.api.config.PLC_DB_NAME)
+            try:
+                # Try UNIX socket first
+                self.connection = psycopg2.connect(user = self.api.config.PLC_DB_USER,
+                                                   password = self.api.config.PLC_DB_PASSWORD,
+                                                   database = self.api.config.PLC_DB_NAME)
+            except psycopg2.OperationalError:
+                # Fall back on TCP
+                self.connection = psycopg2.connect(user = self.api.config.PLC_DB_USER,
+                                                   password = self.api.config.PLC_DB_PASSWORD,
+                                                   database = self.api.config.PLC_DB_NAME,
+                                                   host = self.api.config.PLC_DB_HOST,
+                                                   port = self.api.config.PLC_DB_PORT)
+            self.connection.set_client_encoding("UNICODE")
 
         (self.rowcount, self.description, self.lastrowid) = \
                         (None, None, None)
 
         (self.rowcount, self.description, self.lastrowid) = \
                         (None, None, None)
index aeb9f09..81da51d 100644 (file)
@@ -13,6 +13,7 @@ from PLC.SliceInstantiations import SliceInstantiation, SliceInstantiations
 from PLC.Nodes import Node
 from PLC.Persons import Person, Persons
 from PLC.SliceTags import SliceTag
 from PLC.Nodes import Node
 from PLC.Persons import Person, Persons
 from PLC.SliceTags import SliceTag
+from PLC.Timestamp import Timestamp
 
 class Slice(Row):
     """
 
 class Slice(Row):
     """
@@ -24,7 +25,7 @@ class Slice(Row):
 
     table_name = 'slices'
     primary_key = 'slice_id'
 
     table_name = 'slices'
     primary_key = 'slice_id'
-    join_tables = ['slice_node', 'slice_person', 'slice_tag', 'peer_slice', 'node_slice_whitelist']
+    join_tables = ['slice_node', 'slice_person', 'slice_tag', 'peer_slice', 'node_slice_whitelist', 'leases', ]
     fields = {
         'slice_id': Parameter(int, "Slice identifier"),
         'site_id': Parameter(int, "Identifier of the site to which this slice belongs"),
     fields = {
         'slice_id': Parameter(int, "Slice identifier"),
         'site_id': Parameter(int, "Identifier of the site to which this slice belongs"),
@@ -86,7 +87,7 @@ class Slice(Row):
         # N.B.: Responsibility of the caller to ensure that expires is
         # not too far into the future.
         check_future = not ('is_deleted' in self and self['is_deleted'])
         # N.B.: Responsibility of the caller to ensure that expires is
         # not too far into the future.
         check_future = not ('is_deleted' in self and self['is_deleted'])
-        return Row.validate_timestamp(self, expires, check_future = check_future)
+        return Timestamp.sql_validate( expires, check_future = check_future)
 
     add_person = Row.add_object(Person, 'slice_person')
     remove_person = Row.remove_object(Person, 'slice_person')
 
     add_person = Row.add_object(Person, 'slice_person')
     remove_person = Row.remove_object(Person, 'slice_person')
index 434db2e..c3cf0ec 100644 (file)
@@ -4,9 +4,11 @@ from types import StringTypes, IntType, LongType
 import time
 import calendar
 
 import time
 import calendar
 
+from PLC.Timestamp import Timestamp
 from PLC.Faults import *
 from PLC.Parameter import Parameter
 
 from PLC.Faults import *
 from PLC.Parameter import Parameter
 
+
 class Row(dict):
     """
     Representation of a row in a database table. To use, optionally
 class Row(dict):
     """
     Representation of a row in a database table. To use, optionally
@@ -96,28 +98,8 @@ class Row(dict):
        else:
            raise PLCInvalidArguemnt, "No such associate function associate_%s" % args[1]
 
        else:
            raise PLCInvalidArguemnt, "No such associate function associate_%s" % args[1]
 
-    def validate_timestamp(self, timestamp, check_future = False):
-        """
-        Validates the specified GMT timestamp string (must be in
-        %Y-%m-%d %H:%M:%S format) or number (seconds since UNIX epoch,
-        i.e., 1970-01-01 00:00:00 GMT). If check_future is True,
-        raises an exception if timestamp is not in the future. Returns
-        a GMT timestamp string.
-        """
-
-        time_format = "%Y-%m-%d %H:%M:%S"
-
-       if isinstance(timestamp, StringTypes):
-           # calendar.timegm() is the inverse of time.gmtime()
-           timestamp = calendar.timegm(time.strptime(timestamp, time_format))
-
-        # Human readable timestamp string
-       human = time.strftime(time_format, time.gmtime(timestamp))
-
-       if check_future and timestamp < time.time():
-            raise PLCInvalidArgument, "'%s' not in the future" % human
-
-       return human
+    def validate_timestamp (self, timestamp):
+        return Timestamp.sql_validate(timestamp)
 
     def add_object(self, classobj, join_table, columns = None):
         """
 
     def add_object(self, classobj, join_table, columns = None):
         """
diff --git a/PLC/Timestamp.py b/PLC/Timestamp.py
new file mode 100644 (file)
index 0000000..1b1f9ad
--- /dev/null
@@ -0,0 +1,161 @@
+#
+# Utilities to handle timestamps / durations from/to integers and strings
+#
+# $Id$
+# $URL$
+#
+
+#
+# datetime.{datetime,timedelta} are powerful tools, but these objects are not
+# natively marshalled over xmlrpc
+#
+
+from types import StringTypes
+import time, calendar
+import datetime
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter, Mixed
+
+# a dummy class mostly used as a namespace
+class Timestamp:
+    
+    debug=False
+#    debug=True
+
+    # this is how we expose times to SQL
+    sql_format = "%Y-%m-%d %H:%M:%S"
+    sql_format_utc = "%Y-%m-%d %H:%M:%S UTC"
+    # this one (datetime.isoformat) would work too but that's less readable - we support this input though
+    iso_format = "%Y-%m-%dT%H:%M:%S"
+    # sometimes it's convenient to understand more formats
+    input_formats = [ sql_format,
+                      sql_format_utc,
+                      iso_format,
+                      "%Y-%m-%d %H:%M",
+                      "%Y-%m-%d %H:%M UTC",
+                      ]
+
+    # for timestamps we usually accept either an int, or an ISO string, 
+    # the datetime.datetime stuff can in general be used locally, 
+    # but not sure it can be marshalled over xmlrpc though
+
+    @staticmethod
+    def Parameter (doc):
+        return Mixed (Parameter (int, doc + " (unix timestamp)"),
+                      Parameter (str, doc + " (formatted as %s)"%Timestamp.sql_format),
+                      )
+
+    @staticmethod
+    def sql_validate (input, timezone=False, check_future = False):
+        """
+        Validates the specified GMT timestamp, returns a 
+        standardized string suitable for SQL input.
+
+        Input may be a number (seconds since UNIX epoch back in 1970,
+        or a string (in one of the supported input formats).  
+
+        If timezone is True, the resulting string contains 
+        timezone information, which is hard-wired as 'UTC'
+        
+        If check_future is True, raises an exception if timestamp is in
+        the past. 
+
+        Returns a GMT timestamp string suitable to feed SQL.
+        """
+
+        if not timezone: output_format = Timestamp.sql_format
+        else:            output_format = Timestamp.sql_format_utc
+
+        if Timestamp.debug: print 'sql_validate, in:',input,
+        if isinstance(input, StringTypes):
+            sql=''
+            # calendar.timegm() is the inverse of time.gmtime()
+            for time_format in Timestamp.input_formats:
+                try:
+                    timestamp = calendar.timegm(time.strptime(input, time_format))
+                    sql = time.strftime(output_format, time.gmtime(timestamp))
+                    break
+                # wrong format: ignore
+                except ValueError: pass
+            # could not parse it
+            if not sql:
+                raise PLCInvalidArgument, "Cannot parse timestamp %r - not in any of %r formats"%(input,Timestamp.input_formats)
+        elif isinstance (input,(int,long,float)):
+            try:
+                timestamp = long(input)
+                sql = time.strftime(output_format, time.gmtime(timestamp))
+            except Exception,e:
+                raise PLCInvalidArgument, "Timestamp %r not recognized -- %r"%(input,e)
+        else:
+            raise PLCInvalidArgument, "Timestamp %r - unsupported type %r"%(input,type(input))
+
+        if check_future and input < time.time():
+            raise PLCInvalidArgument, "'%s' not in the future" % sql
+
+        if Timestamp.debug: print 'sql_validate, out:',sql
+        return sql
+
+    @staticmethod
+    def sql_validate_utc (timestamp):
+        "For convenience, return sql_validate(intput, timezone=True, check_future=False)"
+        return Timestamp.sql_validate (timestamp, timezone=True, check_future=False)
+
+
+    @staticmethod
+    def cast_long (input):
+        """
+        Translates input timestamp as a unix timestamp.
+
+        Input may be a number (seconds since UNIX epoch, i.e., 1970-01-01
+        00:00:00 GMT), a string (in one of the supported input formats above).  
+
+        """
+        if Timestamp.debug: print 'cast_long, in:',input,
+        if isinstance(input, StringTypes):
+            timestamp=0
+            for time_format in Timestamp.input_formats:
+                try:
+                    result=calendar.timegm(time.strptime(input, time_format))
+                    if Timestamp.debug: print 'out:',result
+                    return result
+                # wrong format: ignore
+                except ValueError: pass
+            raise PLCInvalidArgument, "Cannot parse timestamp %r - not in any of %r formats"%(input,Timestamp.input_formats)
+        elif isinstance (input,(int,long,float)):
+            result=long(input)
+            if Timestamp.debug: print 'out:',result
+            return result
+        else:
+            raise PLCInvalidArgument, "Timestamp %r - unsupported type %r"%(input,type(input))
+
+
+# utility for displaying durations
+# be consistent in avoiding the datetime stuff
+class Duration:
+
+    MINUTE = 60
+    HOUR = 3600
+    DAY = 3600*24
+
+    @staticmethod
+    def to_string(duration):
+        result=[]
+        left=duration
+        (days,left) = divmod(left,Duration.DAY)
+        if days:    result.append("%d d)"%td.days)
+        (hours,left) = divmod (left,Duration.HOUR)
+        if hours:   result.append("%d h"%hours)
+        (minutes, seconds) = divmod (left, Duration.MINUTE)
+        if minutes: result.append("%d m"%minutes)
+        if seconds: result.append("%d s"%seconds)
+        if not result: result = ['void']
+        return "-".join(result)
+
+    @staticmethod
+    def validate (duration):
+        # support seconds only for now, works for int/long/str
+        try:
+            return long (duration)
+        except:
+            raise PLCInvalidArgument, "Could not parse duration %r"%duration
index 7debd12..b892448 100644 (file)
@@ -1,6 +1,7 @@
 ## Please use make index to update this file
 all = """
 API
 ## Please use make index to update this file
 all = """
 API
+Accessor
 AddressTypes
 Addresses
 Auth
 AddressTypes
 Addresses
 Auth
@@ -20,8 +21,11 @@ InterfaceTags
 Interfaces
 KeyTypes
 Keys
 Interfaces
 KeyTypes
 Keys
+LeaseFilter
+Leases
 Messages
 Method
 Messages
 Method
+Namespace
 NetworkMethods
 NetworkTypes
 NodeGroups
 NetworkMethods
 NetworkTypes
 NodeGroups
@@ -48,5 +52,6 @@ SliceTags
 Slices
 Table
 TagTypes
 Slices
 Table
 TagTypes
+Timestamp
 sendmail
 """.split()
 sendmail
 """.split()
diff --git a/migrations/101-down-leases.sql b/migrations/101-down-leases.sql
new file mode 100644 (file)
index 0000000..32bfe8e
--- /dev/null
@@ -0,0 +1,18 @@
+-- $Id$
+-- $URL$
+
+-- revert cleanup on node_types
+INSERT INTO node_types VALUES ('dummynet');
+
+UPDATE nodes SET node_type='regular' WHERE node_type='reservable';
+DELETE FROM node_types WHERE node_type='reservable';
+
+-- drop new tables
+DROP VIEW view_leases;
+DROP VIEW view_all_leases;
+DROP TABLE leases;
+
+DROP FUNCTION IF EXISTS overlapping_trigger();
+
+--------------------------------------------------
+UPDATE plc_db_version SET subversion = 100;
diff --git a/migrations/101-up-leases.sql b/migrations/101-up-leases.sql
new file mode 100644 (file)
index 0000000..42ef63f
--- /dev/null
@@ -0,0 +1,83 @@
+-- $Id$
+-- $URL$
+
+-- we're using the 'lease' nodetype to model reservable nodes
+INSERT INTO node_types VALUES ('reservable');
+-- also the dummynet node_type is obsolete
+DELETE FROM node_types WHERE node_type='dummynet';
+
+SET TIMEZONE TO 'UTC';
+
+CREATE TABLE leases (
+    lease_id serial PRIMARY KEY,                       -- id
+    t_from timestamp with time zone NOT NULL,  -- from
+    t_until timestamp with time zone NOT NULL, -- until
+    node_id integer REFERENCES nodes NOT NULL,         -- subject node
+    slice_id integer REFERENCES slices,                        -- slice owning the node
+-- xxx for testing
+--    CONSTRAINT future CHECK (t_from > CURRENT_TIMESTAMP),
+    CONSTRAINT start_before_end CHECK (t_until > t_from)
+) WITH OIDS;
+
+--
+-- hook to check for overlapping time slots on a given node_id
+-- xxx might use the builtin OVERLAPS feature
+-- http://www.postgresql.org/docs/8.3/interactive/functions-datetime.html
+-- 
+CREATE language plpgsql;
+CREATE FUNCTION overlapping_trigger() RETURNS trigger AS $overlapping_trigger$
+BEGIN
+  PERFORM lease_id FROM leases WHERE 
+    -- consider only leases on the same node
+        NEW.node_id = node_id
+    -- consider only non expired leases    
+    AND t_until > CURRENT_TIMESTAMP
+    -- useful for updates
+    AND NEW.lease_id <> lease_id
+    -- new start date is in range
+    AND (   (NEW.t_from >= t_from AND NEW.t_from < t_until)
+    -- new end date is in range
+          OR (NEW.t_until > t_from AND NEW.t_until <= t_until)
+    -- complete overlap: new from before from, new until after until
+          OR (NEW.t_from <= t_from AND NEW.t_until >= t_until));
+  IF FOUND THEN
+    RAISE EXCEPTION 'overlapping  error: node % - slice %, % -> %', NEW.node_id, NEW.slice_id, NEW.t_from, NEW.t_until;
+  END IF;
+  RETURN NEW;
+END;
+$overlapping_trigger$ LANGUAGE plpgsql;
+
+CREATE 
+  TRIGGER overlapping_trigger BEFORE INSERT OR UPDATE 
+  ON leases FOR EACH ROW EXECUTE PROCEDURE overlapping_trigger();
+
+       
+-- this is to let the API a chance to check for leases attached 
+-- to a node that is not 'reservable'
+CREATE OR REPLACE VIEW view_all_leases AS
+SELECT 
+leases.lease_id,
+CAST(date_part('epoch', leases.t_from) AS bigint) AS t_from,
+CAST(date_part('epoch', leases.t_until) AS bigint) AS t_until,
+-- dbg
+leases.t_from as s_from,
+leases.t_until as s_until,
+leases.node_id,
+leases.slice_id,
+nodes.hostname,
+nodes.node_type,
+slices.name,
+slices.site_id,
+CAST( date_part ('epoch',leases.t_until-leases.t_from) AS bigint) AS duration,
+leases.t_until < CURRENT_TIMESTAMP as expired
+FROM slices INNER JOIN leases USING (slice_id)
+JOIN nodes USING (node_id);
+
+-- only the relevant leases
+CREATE OR REPLACE VIEW view_leases AS
+SELECT * FROM view_all_leases
+WHERE node_type = 'reservable';
+
+
+--------------------------------------------------
+UPDATE plc_db_version SET subversion = 101;
index defab86..d1c56d4 100755 (executable)
@@ -85,10 +85,14 @@ case "$1" in
        # PostgreSQL 7.x uses tcpip_socket.
        if grep -q listen_addresses $postgresql_conf ; then
            sed -i -e '/^listen_addresses/d' $postgresql_conf
        # PostgreSQL 7.x uses tcpip_socket.
        if grep -q listen_addresses $postgresql_conf ; then
            sed -i -e '/^listen_addresses/d' $postgresql_conf
-           echo "listen_addresses = '*'" >>$postgresql_conf
-       elif grep -q tcpip_socket $postgresql_conf ; then
-           sed -i -e '/^tcpip_socket/d' $postgresql_conf
-           echo "tcpip_socket = true" >>$postgresql_conf
+           echo "listen_addresses = '*'" >> $postgresql_conf
+           # tweak timezone to be 'UTC'
+           sed -i -e '/^timezone=/d' $postgresql_conf
+           echo "timezone='UTC'" >> $postgresql_conf
+       else
+           dialog "PostgreSQL <= 7.x - not supported"
+           /bin/false
+           check
        fi
 
        # Disable access to all DBs from all hosts
        fi
 
        # Disable access to all DBs from all hosts