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