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