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