Setting tag nodemanager-1.8-39
[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 import grp
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             # get the unix account info
89             gid = grp.getgrnam("slices")[2]
90             pw_info = pwd.getpwnam(self.name)
91             uid = pw_info[2]
92             pw_dir = pw_info[5]
93
94             # write out authorized_keys file and conditionally create
95             # the .ssh subdir if need be.
96             dot_ssh = os.path.join(pw_dir,'.ssh')
97             if not os.path.isdir(dot_ssh):
98                 if not os.path.isdir(pw_dir):
99                     logger.verbose('WARNING: homedir %s does not exist for %s!'%(pw_dir,self.name))
100                     os.mkdir(pw_dir)
101                     os.chown(pw_dir, uid, gid)
102                 os.mkdir(dot_ssh)
103
104             auth_keys = os.path.join(dot_ssh,'authorized_keys')
105             tools.write_file(auth_keys, lambda f: f.write(new_keys))
106
107             # set access permissions and ownership properly
108             os.chmod(dot_ssh, 0700)
109             os.chown(dot_ssh, uid, gid)
110             os.chmod(auth_keys, 0600)
111             os.chown(auth_keys, uid, gid)
112
113             # set self.keys to new_keys only when all of the above ops succeed
114             self.keys = new_keys
115
116             logger.log('%s: installed ssh keys' % self.name)
117
118     def start(self, delay=0): pass
119     def stop(self): pass
120     def is_running(self): pass
121
122 class Worker:
123     def __init__(self, name):
124         self.name = name  # username
125         self._acct = None  # the account object currently associated with this worker
126
127     def ensure_created(self, rec, startingup = Startingup):
128         """Check account type is still valid.  If not, recreate sliver.  If still valid,
129         check if running and configure/start if not."""
130         curr_class = self._get_class()
131         next_class = type_acct_class[rec['type']]
132         if next_class != curr_class:
133             self._destroy(curr_class)
134             create_sem.acquire()
135             try: next_class.create(self.name, rec['vref'])
136             finally: create_sem.release()
137         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
138         if startingup or \
139           not self.is_running() or \
140           next_class != curr_class or \
141           self._acct.initscriptchanged:
142             self.start(rec)
143         else: self._acct.configure(rec)
144
145     def ensure_destroyed(self): self._destroy(self._get_class())
146
147     def start(self, rec, d = 0): 
148         self._acct.configure(rec)
149         self._acct.start(delay=d)
150
151     def stop(self): self._acct.stop()
152
153     def is_running(self): 
154         if (self._acct != None) and self._acct.is_running():
155             status = True
156         else:
157             status = False
158             logger.verbose("Worker(%s): is not running" % self.name)
159         return status
160
161     def _destroy(self, curr_class):
162         self._acct = None
163         if curr_class:
164             destroy_sem.acquire()
165             try: curr_class.destroy(self.name)
166             finally: destroy_sem.release()
167
168     def _get_class(self):
169         try: shell = pwd.getpwnam(self.name)[6]
170         except KeyError: return None
171         return shell_acct_class[shell]