merged master into lxc_devel
[nodemanager.git] / account.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 # xxx strictly speaking this class should not use self.name that in fact
73 # is accidentally inited by the subclasses constructor...
74 class Account:
75     def __init__(self, name):
76         self.name = name
77         self.keys = ''
78         logger.verbose('account: Initing account %s'%name)
79
80 #    @staticmethod
81 #    def create(name, vref = None): abstract
82
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('account: configuring %s'%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('account: 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('account: %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
127     def __init__(self, name):
128         self.name = name  # username
129         self._acct = None  # the account object currently associated with this worker
130
131     def ensure_created(self, rec):
132         """Check account type is still valid.  If not, recreate sliver.
133 If still valid, check if running and configure/start if not."""
134         logger.log_data_in_file(rec,"/var/lib/nodemanager/%s.rec.txt"%rec['name'],
135                                 'raw rec captured in ensure_created',logger.LOG_VERBOSE)
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)
142             finally: create_sem.release()
143         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
144         logger.verbose("account.Worker.ensure_created: %s, running=%r"%(self.name,self.is_running()))
145
146         # reservation_alive is set on reervable nodes, and its value is a boolean
147         if 'reservation_alive' in rec:
148             # reservable nodes
149             if rec['reservation_alive']:
150                 # this sliver has the lease, it is safe to start it
151                 if not self.is_running(): self.start(rec)
152                 else: self.configure(rec)
153             else:
154                 # not having the lease, do not start it
155                 self.configure(rec)
156         # usual nodes - preserve old code
157         # xxx it's not clear what to do when a sliver changes type/class
158         # in a reservable node
159         else:
160             if not self.is_running() or next_class != curr_class:
161                 self.start(rec)
162             else: self.configure(rec)
163
164     def ensure_destroyed(self): self._destroy(self._get_class())
165
166     def start(self, rec, d = 0):
167         self._acct.configure(rec)
168         self._acct.start(delay=d)
169
170     def configure(self, rec):
171         self._acct.configure(rec)
172
173     def stop(self): self._acct.stop()
174
175     def is_running(self):
176         if (self._acct != None) and self._acct.is_running():
177             status = True
178         else:
179             status = False
180             logger.verbose("account: Worker(%s): is not running" % self.name)
181         return status
182
183     def _destroy(self, curr_class):
184         self._acct = None
185         if curr_class:
186             destroy_sem.acquire()
187             try: curr_class.destroy(self.name)
188             finally: destroy_sem.release()
189
190     def _get_class(self):
191         try: shell = pwd.getpwnam(self.name)[6]
192         except KeyError: return None
193         return shell_acct_class[shell]
194
195     # bind mount root side dir to sliver side
196     # needs to be done before sliver starts, in the vserver case at least
197     def expose_ssh_dir (self):
198         try:
199             root_ssh="/home/%s/.ssh"%self.name
200             sliver_ssh="/vservers/%s/home/%s/.ssh"%(self.name,self.name)
201             # any of both might not exist yet
202             for path in [root_ssh,sliver_ssh]:
203                 if not os.path.exists (path):
204                     os.mkdir(path)
205                 if not os.path.isdir (path):
206                     raise Exception
207             mounts=file('/proc/mounts').read()
208             if mounts.find(sliver_ssh)<0:
209                 # xxx perform mount
210                 subprocess.call("mount --bind -o ro %s %s"%(root_ssh,sliver_ssh),shell=True)
211                 logger.log("expose_ssh_dir: %s mounted into slice %s"%(root_ssh,self.name))
212         except:
213             logger.log_exc("expose_ssh_dir with slice %s failed"%self.name)
214