moving towards reservable nodes
[nodemanager.git] / accounts.py
1 # $Id$
2 # $URL$
3
4 """Functionality common to all account classes.
5
6 Each subclass of Account must provide five methods: 
7   (*) create() and destroy(), which are static; 
8   (*) configure(), start(), and stop(), which are not.  
9
10 configure(), which takes a record as its only argument, does
11 things like set up ssh keys. In addition, an Account subclass must
12 provide static member variables SHELL, which contains the unique shell
13 that it uses; and TYPE, a string that is used by the account creation
14 code.  For no particular reason, TYPE is divided hierarchically by
15 periods; at the moment the only convention is that all sliver accounts
16 have type that begins with sliver.
17
18 There are any number of race conditions that may result from the fact
19 that account names are not unique over time.  Moreover, it's a bad
20 idea to perform lengthy operations while holding the database lock.
21 In order to deal with both of these problems, we use a worker thread
22 for each account name that ever exists.  On 32-bit systems with large
23 numbers of accounts, this may cause the NM process to run out of
24 *virtual* memory!  This problem may be remedied by decreasing the
25 maximum stack size.
26 """
27
28 #import Queue
29 import os
30 import pwd, grp
31 import threading
32
33 import logger
34 import tools
35
36
37 # When this variable is true, start after any ensure_created
38 Startingup = False
39 # shell path -> account class association
40 shell_acct_class = {}
41 # account type -> account class association
42 type_acct_class = {}
43
44 # these semaphores are acquired before creating/destroying an account
45 create_sem = threading.Semaphore(1)
46 destroy_sem = threading.Semaphore(1)
47
48 def register_class(acct_class):
49     """Call once for each account class.  This method adds the class to the dictionaries used to look up account classes by shell and type."""
50     shell_acct_class[acct_class.SHELL] = acct_class
51     type_acct_class[acct_class.TYPE] = acct_class
52
53
54 # private account name -> worker object association and associated lock
55 name_worker_lock = threading.Lock()
56 name_worker = {}
57
58 def allpwents():
59     return [pw_ent for pw_ent in pwd.getpwall() if pw_ent[6] in shell_acct_class]
60
61 def all():
62     """Return the names of all accounts on the system with recognized shells."""
63     return [pw_ent[0] for pw_ent in allpwents()]
64
65 def get(name):
66     """Return the worker object for a particular username.  If no such object exists, create it first."""
67     name_worker_lock.acquire()
68     try:
69         if name not in name_worker: name_worker[name] = Worker(name)
70         return name_worker[name]
71     finally: name_worker_lock.release()
72
73
74 class Account:
75     def __init__(self, rec):
76         logger.verbose('accounts: Initing account %s'%rec['name'])
77         self.name = rec['name']
78         self.keys = ''
79         self.initscriptchanged = False
80         self.configure(rec)
81
82     @staticmethod
83     def create(name, vref = None): abstract
84
85     @staticmethod
86     def destroy(name): abstract
87
88     def configure(self, rec):
89         """Write <rec['keys']> to my authorized_keys file."""
90         logger.verbose('accounts: configuring %s'%self.name)
91         new_keys = rec['keys']
92         if new_keys != self.keys:
93             # get the unix account info
94             gid = grp.getgrnam("slices")[2]
95             pw_info = pwd.getpwnam(self.name)
96             uid = pw_info[2]
97             pw_dir = pw_info[5]
98
99             # write out authorized_keys file and conditionally create
100             # the .ssh subdir if need be.
101             dot_ssh = os.path.join(pw_dir,'.ssh')
102             if not os.path.isdir(dot_ssh):
103                 if not os.path.isdir(pw_dir):
104                     logger.verbose('accounts: WARNING: homedir %s does not exist for %s!'%(pw_dir,self.name))
105                     os.mkdir(pw_dir)
106                     os.chown(pw_dir, uid, gid)
107                 os.mkdir(dot_ssh)
108
109             auth_keys = os.path.join(dot_ssh,'authorized_keys')
110             tools.write_file(auth_keys, lambda f: f.write(new_keys))
111
112             # set access permissions and ownership properly
113             os.chmod(dot_ssh, 0700)
114             os.chown(dot_ssh, uid, gid)
115             os.chmod(auth_keys, 0600)
116             os.chown(auth_keys, uid, gid)
117
118             # set self.keys to new_keys only when all of the above ops succeed
119             self.keys = new_keys
120
121             logger.log('accounts: %s: installed ssh keys' % self.name)
122
123     def start(self, delay=0): pass
124     def stop(self): pass
125     def is_running(self): pass
126
127 class Worker:
128
129     def __init__(self, name):
130         self.name = name  # username
131         self._acct = None  # the account object currently associated with this worker
132
133     def ensure_created(self, rec, startingup = Startingup):
134         """Check account type is still valid.  If not, recreate sliver.  If still valid,
135         check if running and configure/start if not."""
136         curr_class = self._get_class()
137         next_class = type_acct_class[rec['type']]
138         if next_class != curr_class:
139             self._destroy(curr_class)
140             create_sem.acquire()
141             try: next_class.create(self.name, rec['vref'])
142             finally: create_sem.release()
143         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
144         if startingup or \
145           not self.is_running() or \
146           next_class != curr_class or \
147           self._acct.initscriptchanged:
148             self.start(rec)
149         else: self._acct.configure(rec)
150
151     def ensure_destroyed(self): self._destroy(self._get_class())
152
153     def start(self, rec, d = 0): 
154         self._acct.configure(rec)
155         self._acct.start(delay=d)
156
157     def stop(self): self._acct.stop()
158
159     def is_running(self): 
160         if (self._acct != None) and self._acct.is_running():
161             status = True
162         else:
163             status = False
164             logger.verbose("accounts: Worker(%s): is not running" % self.name)
165         return status
166
167     def _destroy(self, curr_class):
168         self._acct = None
169         if curr_class:
170             destroy_sem.acquire()
171             try: curr_class.destroy(self.name)
172             finally: destroy_sem.release()
173
174     def _get_class(self):
175         try: shell = pwd.getpwnam(self.name)[6]
176         except KeyError: return None
177         return shell_acct_class[shell]