Cut out unneeded stuff
[nodemanager.git] / tools.py
1 """A few things that didn't seem to fit anywhere else."""
2
3 import os, os.path
4 import pwd
5 import tempfile
6 import fcntl
7 import errno
8 import threading
9 import subprocess
10 import shutil
11 import sys
12
13 import logger
14
15 PID_FILE = '/var/run/nodemanager.pid'
16
17 ####################
18 def get_default_if():
19     interface = get_if_from_hwaddr(get_hwaddr_from_plnode())
20     if not interface: interface = "eth0"
21     return interface
22
23 def get_hwaddr_from_plnode():
24     try:
25         for line in open("/usr/boot/plnode.txt", 'r').readlines():
26             if line.startswith("NET_DEVICE"):
27                 return line.split("=")[1].strip().strip('"')
28     except:
29         pass
30     return None
31
32 def get_if_from_hwaddr(hwaddr):
33     import sioc
34     devs = sioc.gifconf()
35     for dev in devs:
36         dev_hwaddr = sioc.gifhwaddr(dev)
37         if dev_hwaddr == hwaddr: return dev
38     return None
39
40 ####################
41 # daemonizing
42 def as_daemon_thread(run):
43     """Call function <run> with no arguments in its own thread."""
44     thr = threading.Thread(target=run)
45     thr.setDaemon(True)
46     thr.start()
47
48 def close_nonstandard_fds():
49     """Close all open file descriptors other than 0, 1, and 2."""
50     _SC_OPEN_MAX = 4
51     for fd in range(3, os.sysconf(_SC_OPEN_MAX)):
52         try: os.close(fd)
53         except OSError: pass  # most likely an fd that isn't open
54
55 # after http://www.erlenstar.demon.co.uk/unix/faq_2.html
56 def daemon():
57     """Daemonize the current process."""
58     if os.fork() != 0: os._exit(0)
59     os.setsid()
60     if os.fork() != 0: os._exit(0)
61     os.chdir('/')
62     os.umask(0022)
63     devnull = os.open(os.devnull, os.O_RDWR)
64     os.dup2(devnull, 0)
65     # xxx fixme - this is just to make sure that nothing gets stupidly lost - should use devnull
66     crashlog = os.open('/var/log/nodemanager.daemon', os.O_RDWR | os.O_APPEND | os.O_CREAT, 0644)
67     os.dup2(crashlog, 1)
68     os.dup2(crashlog, 2)
69
70 def fork_as(su, function, *args):
71     """fork(), cd / to avoid keeping unused directories open, close all nonstandard file descriptors (to avoid capturing open sockets), fork() again (to avoid zombies) and call <function> with arguments <args> in the grandchild process.  If <su> is not None, set our group and user ids appropriately in the child process."""
72     child_pid = os.fork()
73     if child_pid == 0:
74         try:
75             os.chdir('/')
76             close_nonstandard_fds()
77             if su:
78                 pw_ent = pwd.getpwnam(su)
79                 os.setegid(pw_ent[3])
80                 os.seteuid(pw_ent[2])
81             child_pid = os.fork()
82             if child_pid == 0: function(*args)
83         except:
84             os.seteuid(os.getuid())  # undo su so we can write the log file
85             os.setegid(os.getgid())
86             logger.log_exc("tools: fork_as")
87         os._exit(0)
88     else: os.waitpid(child_pid, 0)
89
90 ####################
91 # manage files
92 def pid_file():
93     """We use a pid file to ensure that only one copy of NM is running at a given time.
94 If successful, this function will write a pid file containing the pid of the current process.
95 The return value is the pid of the other running process, or None otherwise."""
96     other_pid = None
97     if os.access(PID_FILE, os.F_OK):  # check for a pid file
98         handle = open(PID_FILE)  # pid file exists, read it
99         other_pid = int(handle.read())
100         handle.close()
101         # check for a process with that pid by sending signal 0
102         try: os.kill(other_pid, 0)
103         except OSError, e:
104             if e.errno == errno.ESRCH: other_pid = None  # doesn't exist
105             else: raise  # who knows
106     if other_pid == None:
107         # write a new pid file
108         write_file(PID_FILE, lambda f: f.write(str(os.getpid())))
109     return other_pid
110
111 def write_file(filename, do_write, **kw_args):
112     """Write file <filename> atomically by opening a temporary file, using <do_write> to write that file, and then renaming the temporary file."""
113     shutil.move(write_temp_file(do_write, **kw_args), filename)
114
115 def write_temp_file(do_write, mode=None, uidgid=None):
116     fd, temporary_filename = tempfile.mkstemp()
117     if mode: os.chmod(temporary_filename, mode)
118     if uidgid: os.chown(temporary_filename, *uidgid)
119     f = os.fdopen(fd, 'w')
120     try: do_write(f)
121     finally: f.close()
122     return temporary_filename
123
124 # replace a target file with a new contents - checks for changes
125 # can handle chmod if requested
126 # can also remove resulting file if contents are void, if requested
127 # performs atomically:
128 #    writes in a tmp file, which is then renamed (from sliverauth originally)
129 # returns True if a change occurred, or the file is deleted
130 def replace_file_with_string (target, new_contents, chmod=None, remove_if_empty=False):
131     try:
132         current=file(target).read()
133     except:
134         current=""
135     if current==new_contents:
136         # if turns out to be an empty string, and remove_if_empty is set,
137         # then make sure to trash the file if it exists
138         if remove_if_empty and not new_contents and os.path.isfile(target):
139             logger.verbose("tools.replace_file_with_string: removing file %s"%target)
140             try: os.unlink(target)
141             finally: return True
142         return False
143     # overwrite target file: create a temp in the same directory
144     path=os.path.dirname(target) or '.'
145     fd, name = tempfile.mkstemp('','repl',path)
146     os.write(fd,new_contents)
147     os.close(fd)
148     if os.path.exists(target):
149         os.unlink(target)
150     shutil.move(name,target)
151     if chmod: os.chmod(target,chmod)
152     return True
153
154
155 ####################
156 # utilities functions to get (cached) information from the node
157
158 # get node_id from /etc/planetlab/node_id and cache it
159 _node_id=None
160 def node_id():
161     global _node_id
162     if _node_id is None:
163         try:
164             _node_id=int(file("/etc/planetlab/node_id").read())
165         except:
166             _node_id=""
167     return _node_id
168
169 _root_context_arch=None
170 def root_context_arch():
171     global _root_context_arch
172     if not _root_context_arch:
173         sp=subprocess.Popen(["uname","-i"],stdout=subprocess.PIPE)
174         (_root_context_arch,_)=sp.communicate()
175         _root_context_arch=_root_context_arch.strip()
176     return _root_context_arch
177
178
179 ####################
180 class NMLock:
181     def __init__(self, file):
182         logger.log("tools: Lock %s initialized." % file, 2)
183         self.fd = os.open(file, os.O_RDWR|os.O_CREAT, 0600)
184         flags = fcntl.fcntl(self.fd, fcntl.F_GETFD)
185         flags |= fcntl.FD_CLOEXEC
186         fcntl.fcntl(self.fd, fcntl.F_SETFD, flags)
187     def __del__(self):
188         os.close(self.fd)
189     def acquire(self):
190         logger.log("tools: Lock acquired.", 2)
191         fcntl.lockf(self.fd, fcntl.LOCK_SH)
192     def release(self):
193         logger.log("tools: Lock released.", 2)
194         fcntl.lockf(self.fd, fcntl.LOCK_UN)
195
196 ####################
197 # Utilities for getting the IP address of a LXC/Openvswitch slice. Do this by
198 # running ifconfig inside of the slice's context.
199
200 def get_sliver_process(slice_name, process_cmdline):
201     """ Utility function to find a process inside of an LXC sliver. Returns
202         (cgroup_fn, pid). cgroup_fn is the filename of the cgroup file for
203         the process, for example /proc/2592/cgroup. Pid is the process id of
204         the process. If the process is not found then (None, None) is returned.
205     """
206     try:
207         cmd = 'grep %s /proc/*/cgroup | grep freezer'%slice_name
208         output = os.popen(cmd).readlines()
209     except:
210         # the slice couldn't be found
211         logger.log("get_sliver_process: couldn't find slice %s" % slice_name)
212         return (None, None)
213
214     cgroup_fn = None
215     pid = None
216     for e in output:
217         try:
218             l = e.rstrip()
219             path = l.split(':')[0]
220             comp = l.rsplit(':')[-1]
221             slice_name_check = comp.rsplit('/')[-1]
222
223             if (slice_name_check == slice_name):
224                 slice_path = path
225                 pid = slice_path.split('/')[2]
226                 cmdline = open('/proc/%s/cmdline'%pid).read().rstrip('\n\x00')
227                 if (cmdline == process_cmdline):
228                     cgroup_fn = slice_path
229                     break
230         except:
231             break
232
233     if (not cgroup_fn) or (not pid):
234         logger.log("get_sliver_process: process %s not running in slice %s" % (process_cmdline, slice_name))
235         return (None, None)
236
237     return (cgroup_fn, pid)
238
239 def get_sliver_ifconfig(slice_name, device="eth0"):
240     """ return the output of "ifconfig" run from inside the sliver.
241
242         side effects: adds "/usr/sbin" to sys.path
243     """
244
245     # See if setns is installed. If it's not then we're probably not running
246     # LXC.
247     if not os.path.exists("/usr/sbin/setns.so"):
248         return None
249
250     # setns is part of lxcsu and is installed to /usr/sbin
251     if not "/usr/sbin" in sys.path:
252         sys.path.append("/usr/sbin")
253     import setns
254
255     (cgroup_fn, pid) = get_sliver_process(slice_name, "/sbin/init")
256     if (not cgroup_fn) or (not pid):
257         return None
258
259     path = '/proc/%s/ns/net'%pid
260
261     result = None
262     try:
263         setns.chcontext(path)
264
265         args = ["/sbin/ifconfig", device]
266         sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
267         sub.wait()
268
269         if (sub.returncode != 0):
270             logger.log("get_slice_ifconfig: error in ifconfig: %s" % sub.stderr.read())
271
272         result = sub.stdout.read()
273     finally:
274         setns.chcontext("/proc/1/ns/net")
275
276     return result
277
278 def get_sliver_ip(slice_name):
279     ifconfig = get_sliver_ifconfig(slice_name)
280     if not ifconfig:
281         return None
282
283     for line in ifconfig.split("\n"):
284         if "inet addr:" in line:
285             # example: '          inet addr:192.168.122.189  Bcast:192.168.122.255  Mask:255.255.255.0'
286             parts = line.strip().split()
287             if len(parts)>=2 and parts[1].startswith("addr:"):
288                 return parts[1].split(":")[1]
289
290     return None
291
292 ### this returns the kind of virtualization on the node
293 # either 'vs' or 'lxc'
294 # also caches it in /etc/planetlab/virt for next calls
295 # could be promoted to core nm if need be
296 virt_stamp="/etc/planetlab/virt"
297 def get_node_virt ():
298     try:
299         return file(virt_stamp).read().strip()
300     except:
301         pass
302     logger.log("Computing virt..")
303     try: 
304         if subprocess.call ([ 'vserver', '--help' ]) ==0: virt='vs'
305         else:                                             virt='lxc'      
306     except:
307         virt='lxc'
308     with file(virt_stamp,"w") as f:
309         f.write(virt)
310     return virt
311
312 # how to run a command in a slice
313 # now this is a painful matter
314 # the problem is with capsh that forces a bash command to be injected in its exec'ed command
315 # so because lxcsu uses capsh, you cannot exec anything else than bash
316 # bottom line is, what actually needs to be called is
317 # vs:  vserver exec slicename command and its arguments
318 # lxc: lxcsu slicename "command and its arguments"
319 # which, OK, is no big deal as long as the command is simple enough, 
320 # but do not stretch it with arguments that have spaces or need quoting as that will become a nightmare
321 def command_in_slice (slicename, argv):
322     virt=get_node_virt()
323     if virt=='vs':
324         return [ 'vserver', slicename, 'exec', ] + argv
325     elif virt=='lxc':
326         # wrap up argv in a single string for -c
327         return [ 'lxcsu', slicename, ] + [ " ".join(argv) ]
328     logger.log("command_in_slice: WARNING: could not find a valid virt")
329     return argv
330