leasefilter has the new ‘day’ key
[plcapi.git] / PLC / LeaseFilter.py
1 #
2 # Thierry Parmentelat -- INRIA
3 #
4 # Utilities for filtering on leases
5
6 import time
7 import calendar
8
9 from types import StringTypes
10 from PLC.Faults import *
11 from PLC.Filter import Filter
12 from PLC.Parameter import Parameter, Mixed
13 from PLC.Timestamp import Timestamp
14
15 # supersede the generic Filter class to support time intersection
16
17
18 class LeaseFilter (Filter):
19
20     # general notes on input parameters
21     # int_timestamp: number of seconds since the epoch
22     # str_timestamp: see Timestamp.sql_validate
23     # timeslot: a tuple (from, until), each being either int_timestamp or
24     # str_timestamp
25
26     local_fields = {
27         'alive': Mixed(
28             Parameter(
29                 int,  "int_timestamp: leases alive at that time"),
30             Parameter(
31                 str,  "str_timestamp: leases alive at that time"),
32             Parameter(
33                 tuple, "timeslot: the leases alive during this timeslot")),
34         'clip':  Mixed(
35             Parameter(
36                 int,  "int_timestamp: leases alive after that time"),
37             Parameter(
38                 str,  "str_timestamp: leases alive after at that time"),
39             Parameter(
40                 tuple, "timeslot: the leases alive during this timeslot")),
41         ########## macros
42         # {'day' : 0} : all leases from today and on
43         # {'day' : 1} : all leases today (localtime at the myplc)
44         # {'day' : 2} : all leases today and tomorrow (localtime at the myplc)
45         # etc..
46         'day': Parameter(int, "clip on a number of days from today and on;"
47                          " 0 means no limit in the future"),
48     }
49
50     def __init__(self, fields={}, filter={},
51                  doc="Lease filter -- adds the 'alive' and 'clip'"
52                  "capabilities for filtering on leases"):
53         Filter.__init__(self, fields, filter, doc)
54         self.fields.update(LeaseFilter.local_fields)
55
56     # canonical type
57     @staticmethod
58     def quote(timestamp): return Timestamp.cast_long(timestamp)
59
60     # basic SQL utilities
61     @staticmethod
62     def sql_time_intersect(f1, u1, f2, u2):
63         # either f2 is in [f1,u1], or u2 is in [f1,u1], or f2<=f1<=u1<=u2
64         return ("(({f1} <= {f2}) AND ({f2} <= {u1})) " +
65                 "OR (({f1} <= {u2}) AND ({u2} <= {u1})) " +
66                 "OR (({f2}<={f1}) AND ({u1}<={u2}))").format(**locals())
67
68     @staticmethod
69     def time_in_range(timestamp, f1, u1):
70         return Timestamp.cast_long(f1) <= Timestamp.cast_long(timestamp) \
71             and Timestamp.cast_long(timestamp) <= Timestamp.cast_long(u1)
72
73     @staticmethod
74     def sql_time_in_range(timestamp, f1, u1):
75         # is timestamp in [f1, u1]
76         return "(({f1} <= {timestamp}) AND ({timestamp} <= {u1}))"\
77             .format(**locals())
78
79     @staticmethod
80     def sql_timeslot_after(f1, u1, mark):
81         # is the lease alive after mark, i.e. u1 >= mark
82         return "({u1} >= {mark})".format(**locals())
83
84     # hooks for the local fields
85     def sql_alive(self, alive):
86         if isinstance(alive, int) or isinstance(alive, StringTypes):
87             # the lease is alive at that time if from <= alive <= until
88             alive = LeaseFilter.quote(alive)
89             return LeaseFilter.sql_time_in_range(alive, 't_from', 't_until')
90         elif isinstance(alive, tuple):
91             (f, u) = alive
92             f = LeaseFilter.quote(f)
93             u = LeaseFilter.quote(u)
94             return LeaseFilter.sql_time_intersect(f, u, 't_from', 't_until')
95         else:
96             raise PLCInvalidArgument("LeaseFilter: alive field {}"
97                                      .format(alive))
98
99     def sql_clip(self, clip):
100         if isinstance(clip, int) or isinstance(clip, StringTypes):
101             start = LeaseFilter.quote(clip)
102             return LeaseFilter.sql_timeslot_after('t_from', 't_until', start)
103         elif isinstance(clip, tuple):
104             (f, u) = clip
105             f = LeaseFilter.quote(f)
106             u = LeaseFilter.quote(u)
107             return LeaseFilter.sql_time_intersect(f, u, 't_from', 't_until')
108         else:
109             raise PLCInvalidArgument("LeaseFilter: clip field {}"
110                                      .format(clip))
111
112     # the whole key to implementing day is to compute today's beginning 
113     def today_start(self):
114         # a struct_time
115         st = time.localtime()
116         seconds_today = st.tm_hour * 3600 + st.tm_min * 60 + st.tm_sec
117         return int(time.time()) - seconds_today
118
119     # supersede the generic Filter 'sql' method
120     def sql(self, api, join_with="AND"):
121         # implement 'day' as a clip
122         if 'day' in self:
123             if 'clip' in self:
124                 raise PLCInvalidArgument("LeaseFilter cannot have both 'clip' and 'day'")
125             today = self.today_start()
126             nb_days = self['day']
127             if nb_days == 0:
128                 self['clip'] = today
129             else:
130                 self['clip'] = (today, today + nb_days * 24 * 3600)
131             del self['day']
132                 
133         # preserve locally what belongs to us, hide it from the superclass
134         # self.local is a dict    local_key : user_value
135         # self.negation is a dict  local_key : string
136         self.local = {}
137         self.negation = {}
138         for (k, v) in LeaseFilter.local_fields.items():
139             if k in self:
140                 self.local[k] = self[k]
141                 del self[k]
142                 self.negation[k] = ""
143             elif ('~' + k) in self:
144                 self.local[k] = self['~' + k]
145                 del self['~' + k]
146                 self.negation[k] = "NOT "
147         # run the generic filtering code
148         (where_part, clip_part) = Filter.sql(self, api, join_with)
149         for (k, v) in self.local.items():
150             try:
151                 # locate hook function associated with key
152                 method = LeaseFilter.__dict__['sql_' + k]
153                 where_part += " {} {}({})"\
154                               .format(self.join_with,
155                                       self.negation[k],
156                                       method(self, self.local[k]))
157             except Exception, e:
158                 raise PLCInvalidArgument(
159                     "LeaseFilter: something wrong with filter"
160                     "key {}, val was {} -- {}".format(k, v, e))
161         return (where_part, clip_part)
162
163 # xxx not sure where this belongs yet
164 # given a set of nodes, and a timeslot,
165 # returns the available leases that have at least a given duration
166
167
168 def free_leases(api, node_ids, t_from, t_until, min_duration):
169
170     # get the leases for these nodes and timeslot
171     filter = {'node_id': node_ids,
172               'clip': (t_from, t_until),
173               # sort by node, and inside one node, chronologically
174               '-SORT': ('node_id', 't_from'),
175               }
176     leases = Leases(api, filter)
177
178     result = []
179
180     # sort node_ids
181     node_ids.sort()
182
183     # scan nodes from the input
184     input_node_id = 0
185     # scan nodes from the leases
186     lease_node_id = 0
187
188     return '?? what now ??'
189
190
191 def node_free_leases(node_id, node_leases, t_from, t_until):
192
193     # no lease yet : return one solid lease
194     if not node_leases:
195         return [{'node_id': node_id,
196                  't_from': t_from,
197                  't_until': t_until}]
198
199     result = []
200     current_time = t_from
201     is_on = LeaseFilter.time_in_range(
202         node_leases[0]['t_from'], t_from, t_until)
203
204     while True:
205         # print 'DBG','current_time',current_time,'is_on',is_on,'result',result
206         # lease is active
207         if is_on:
208             current_time = node_leases[0]['t_until']
209             is_on = False
210             del node_leases[0]
211             if not node_leases:
212                 return result
213         # free, has no remaining lease
214         elif not node_leases:
215             result.append(
216                 {'node_id': node_id,
217                  't_from': current_time, 't_until': t_until})
218             return result
219         # free and has remaining leases
220         else:
221             next_time = node_leases[0]['t_from']
222             result.append(
223                 {'node_id': node_id,
224                  't_from': current_time, 't_until': next_time})
225             current_time = next_time
226             is_on = True