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