root/projects/AsynCluster/trunk/asyncluster/master/database.py

Revision 2, 12.4 kB (checked in by edsuom, 1 year ago)

Import of trunk from old repo

Line 
1 # AsynCluster: Master
2 # A cluster management server based on Twisted's Perspective Broker. Dispatches
3 # cluster jobs and regulates when and how much each user can use his account on
4 # any of the cluster node workstations.
5 #
6 # Copyright (C) 2006-2007 by Edwin A. Suominen, http://www.eepatents.com
7 #
8 # This program is free software; you can redistribute it and/or modify it under
9 # the terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 2 of the License, or (at your option) any later
11 # version.
12 #
13 # This program is distributed in the hope that it will be useful, but WITHOUT
14 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15 # FOR A PARTICULAR PURPOSE.  See the file COPYING for more details.
16 #
17 # You should have received a copy of the GNU General Public License along with
18 # this program; if not, write to the Free Software Foundation, Inc., 51
19 # Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
20
21 """
22 Persistent data concerning control and records of user access.
23 """
24
25 import time
26 from datetime import date, timedelta
27 from twisted.internet import defer
28 from sasync.database import transact, AccessBroker, SA
29
30
31 class UserDataTransactor(AccessBroker):
32     """
33     I manage control and records data for user access accounts.
34     """
35     def startup(self):
36         """
37         Ensures that all necessary tables are in place and accessible via
38         instance variables before the first transaction runs.
39         """
40         self._activeSessionStartTimes = {}
41         d1 = self.table(
42             'users',
43             SA.Column('id', SA.String(20), primary_key=True),
44             SA.Column('password', SA.String(40), nullable=False),
45             SA.Column('enabled', SA.Boolean, default=True, nullable=False),
46             SA.Column('restricted', SA.Boolean, default=True, nullable=False),
47             SA.Column('daily_hours', SA.Float),
48             SA.Column('weekly_hours', SA.Float),
49             )
50         d2 = self.table(
51             'openings',
52             SA.Column('weekday', SA.Integer, primary_key=True),
53             SA.Column('start_hour', SA.Float(2), primary_key=True),
54             SA.Column('end_hour', SA.Float(2), primary_key=True),
55             )
56         d3 = self.table(
57             'timesheet',
58             SA.Column('user_id', SA.String(20), primary_key=True),
59             SA.Column('date', SA.Date, primary_key=True),
60             SA.Column('start_hour', SA.Float, primary_key=True),
61             SA.Column('end_hour', SA.Float, primary_key=True),
62             )
63         return defer.DeferredList([d1,d2,d3])
64
65     def recordSessionStartTime(self, userID):
66         thisHour = self._timeToFloat(self._localtime())
67         self._activeSessionStartTimes[userID] = thisHour
68        
69     def retrieveSessionStartTime(self, userID):
70         return self._activeSessionStartTimes.pop(userID, None)
71    
72     @transact
73     def sessionAuthorized(self, userID, userPassword):
74         """
75         Returns a deferred that fires with C{True} if the user is authorized
76         and enabled, or C{False} if not.
77         """
78         if not self.s('authorization'):
79             col = self.users.c
80             self.s(
81                 [col.enabled],
82                 SA.and_(col.id == SA.bindparam('ID'),
83                         col.password == SA.bindparam('password')))
84         row = self.s().execute(ID=userID, password=userPassword).fetchone()
85         if row is None or not row['enabled']:
86             return False
87         return True
88
89     @transact
90     def sessionStart(self, userID):
91         """
92         Begins an account's access session if any time remains for it. Returns
93         the number of hours remaining for access today, if so, or 0.0
94         otherwise.
95         """
96         timesLeft = [self.openingTimeLeft()]
97         if timesLeft[0] <= 0:
98             # DENIED: We're not currently in a usage opening
99             return 0.0
100         # We're in an opening, so see if the user has time left on his account
101         timesLeft.append(self.accountTimeLeft(userID))
102         # Time left is whenever the user's remaining time runs out or the usage
103         # opening ends, whichever comes first
104         timeLeft = min(timesLeft)
105         return timeLeft
106
107     @transact
108     def sessionEnd(self, userID):
109         """
110         Ends an account's access session, inserting a new database record to
111         account for the account's usage during the session.
112         """
113         started = self.retrieveSessionStartTime(userID)
114         if started is not None:
115             ended = self._timeToFloat(self._localtime())
116             self.timesheet.insert().execute(
117                 user_id=userID,
118                 date=self._today(),
119                 start_hour=started,
120                 end_hour=ended
121                 )
122
123     @transact
124     def openingTimeLeft(self):
125         """
126         Returns as a float the hours left in the current usage opening, if any.
127         """
128         if not self.s('pertinentOpenings'):
129             col = self.openings.c
130             self.s(
131                 [col.end_hour],
132                 SA.and_(col.weekday == SA.bindparam('weekday'),
133                         col.start_hour < SA.bindparam('startsBy')))
134         thisWeekday = self._weekday()
135         thisHour = self._timeToFloat(self._localtime())
136         rows = self.s().execute(
137             weekday=thisWeekday, startsBy=thisHour).fetchall()
138         # All rows are for openings that have started by the current time on
139         # this day of the week. (There's no reason why there should be more
140         # than one, since that would only result from duplicative or
141         # overlapping entries.)
142         currentOpenings = [0.0]
143         for row in rows:
144             if thisHour < row['end_hour']:
145                 # We have an opening that has started but not ended yet, so
146                 # account for it
147                 currentOpenings.append(row['end_hour'] - thisHour)
148         return max(currentOpenings)
149    
150     @transact
151     def accountTimeLeft(self, userID):
152         """
153         Returns as a float the hours the identified user has left given the
154         user's daily and weekly quotas, the usage records, and the current
155         date and time.
156         """
157         # First check that the user doesn't already have a session open;
158         # in this case, a new session is not allowed.
159         if self._activeSessionStartTimes.has_key(userID):
160             return 0.0
161        
162         if not self.s('userTime'):
163             col = self.users.c
164             self.s(
165                 [col.daily_hours, col.weekly_hours],
166                 col.id == SA.bindparam('ID'))
167         row = self.s().execute(ID=userID).fetchone()
168         if row is None:
169             return 0.0
170         timesLeft = []
171         timeLeftToday = row['daily_hours'] - self._timeLoggedToday(userID)
172         if timeLeftToday <= 0.0:
173             return 0.0
174         # Still time left today, so get time left this week
175         timesLeft.append(timeLeftToday)
176         timeLeftThisWeek = row['weekly_hours'] - \
177                            self._timeLoggedThisWeek(userID)
178         timesLeft.append(timeLeftThisWeek)
179         # Return time left today or time left this week, whichever runs out
180         # first
181         return min(timesLeft)
182
183     def _timeLoggedToday(self, userID):
184         """
185         Returns as a float the number of hours the user has already logged in
186         previous sessions today.
187         """
188         # Get time logged today
189         if not self.s('usageToday'):
190             col = self.timesheet.c
191             self.s(
192                 [col.start_hour, col.end_hour],
193                 SA.and_(col.user_id == SA.bindparam('userID'),
194                         col.date == SA.bindparam('date')))
195         rows = self.s().execute(userID=userID, date=self._today()).fetchall()
196         return sum([x['end_hour'] - x['start_hour'] for x in rows])
197
198     def _timeLoggedThisWeek(self, userID):
199         """
200         Returns as a float the number of hours the user has already logged in
201         previous sessions this week.
202         """
203         if not self.s('usageThisWeek'):
204             col = self.timesheet.c
205             self.s(
206                 [col.start_hour, col.end_hour],
207                 SA.and_(col.user_id == SA.bindparam('userID'),
208                         col.date >= SA.bindparam('dateFirst'),
209                         col.date <= SA.bindparam('dateLast')))
210         thisWeekday = self._weekday()
211         # Need datetime.timedelta here, not ints. Note that the
212         # result of the addition is another datetime.date.
213         thisWeekFirstDay = self._today() - timedelta(thisWeekday)
214         thisWeekLastDay = thisWeekFirstDay + timedelta(6)
215         rows = self.s().execute(
216             userID=userID,
217             dateFirst=thisWeekFirstDay,
218             dateLast=thisWeekLastDay
219             ).fetchall()
220         return sum([x['end_hour'] - x['start_hour'] for x in rows])
221
222     def _timeToFloat(self, timeObject):
223         hours, minutes = timeObject[3:5]
224         return float(hours) + float(minutes)/60
225
226     def _today(self):
227         return date.today()
228    
229     def _weekday(self):
230         return self._today().weekday()
231
232     def _localtime(self):
233         return time.localtime()
234
235     @transact
236     def enabled(self, userID, isOrNot):
237         """
238         Sets the user's session access to enabled if I{isOrNot} is C{True} or
239         disabled otherwise.
240         """
241         self.users.update(self.users.c.id == userID).execute(enabled=isOrNot)
242
243     @transact
244     def restricted(self, userID, isOrNot=None):
245         """
246         If I{isOrNot} is specified, sets the user's session access to
247         restricted if it is C{True} or unrestricted if C{False}.
248
249         Otherwise, returns a boolean indicating if the user is currently
250         restricted.
251         
252         """
253         if isOrNot is None:
254             if not self.s('userRestricted'):
255                 col = self.users.c
256                 self.s(
257                     [col.restricted],
258                     col.id == SA.bindparam('ID'))
259             row = self.s().execute(ID=userID).fetchone()
260             return (row is None or row['restricted'])
261
262         self.users.update(
263             self.users.c.id == userID
264             ).execute(restricted=isOrNot)
265
266     @transact
267     def password(self, userID, password):
268         """
269         Sets the password for a user session account.  If the account doesn't
270         already exist, it will be created.  A new account is disabled and
271         restricted by default.
272         """
273         # As ever with this sort of thing, we have several ways of doing this;
274         # for example, we can rely on the SQL database's correct enforcing of
275         # the primary key criterion to use the pythonic "EAFP" approach of
276         # always attempting to create the user and just ignoring any
277         # construction errors.
278         #
279         # The way it's implemented here uses the more conservative "LBYL"
280         # approach: we select to see if the user exists, and depending on
281         # the result we either perform a create or an update.
282        
283         if self.userExists(userID):
284             self.users.update(
285                 self.users.c.id == userID
286                 ).execute(password=password)
287         else:
288             self.users.insert().execute(
289                 id         = userID,
290                 password   = password,
291                 enabled    = False,
292                 restricted = True
293                 )
294
295     @transact
296     def deleteUser(self, userID, all=False):
297         """
298         Deletes the user's entry in the I{users} table. If the I{all} keyword
299         is set to C{True} then the user's entries in the I{timesheet} table are
300         also deleted.
301         
302         This method is mostly useful for testing user account creation. It
303         is probably better practice not to delete users, but rather just to
304         disable them.
305         """
306         self.users.delete(self.users.c.id == userID).execute()
307         if all:
308             self.timesheet.delete(self.timesheet.c.user_id == userID).execute()
309        
310     # Note: the following two methods might be security risks in some
311     # circumstances if untrusted users are allowed to access them.
312    
313     @transact
314     def userExists(self,userID):
315         """
316         Returns a deferred that fires with C{True} if the named user exists,
317         or with C{False} otherwise.
318         """
319         # Doesn't matter what column we select here.
320         if not self.s('userExists'):
321             col = self.users.c
322             self.s(
323                 [col.enabled],
324                 col.id == SA.bindparam('ID')
325                 )
326         row = self.s().execute(ID=userID).fetchone()
327         if row is None:
328             return False
329         return True
Note: See TracBrowser for help on using the browser.