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