Setting tag lxc-userspace-2.0-0
[lxc-userspace.git] / lxcsu
1 #!/usr/bin/python3
2
3 import sys
4 import os
5 import setns
6 import pwd
7
8 from argparse import ArgumentParser
9
10 # can set to True here, but also use the -d option
11 debug = False
12
13
14 def getarch(pid):
15     exe_filename = '/proc/%s/exe' % pid
16     output = os.popen('readelf -h %s 2>&1' % exe_filename).readlines()
17     classlines = [x for x in output if ('Class' in x.split(':')[0])]
18     line = classlines[0]
19     c = line.split(':')[1]
20     if ('ELF64' in c):
21         return 'x86_64'
22     elif ('ELF32' in c):
23         return 'i686'
24     else:
25         raise Exception('Could not determine architecture for pid %s' % pid)
26
27
28 def get_cgroup_subdirs_for_pid(pid):
29     cgroup_info_file = '/proc/%s/cgroup' % pid
30     cgroup_lines = open(cgroup_info_file).read().splitlines()
31
32     subdirs = {}
33     for line in cgroup_lines:
34         try:
35             _, cgroup_name, subdir = line.split(':')
36             subdirs[cgroup_name] = subdir
37         except Exception as e:
38             print("Error reading cgroup info: %s" % str(e))
39             pass
40
41     return subdirs
42
43
44 def umount(fs_dir, opts=''):
45     output = os.popen('/bin/umount %s %s 2>&1' % (opts, fs_dir)).read()
46     return ('device is busy' not in output)
47
48
49 def main():
50     parser = ArgumentParser()
51     parser.add_argument("-n", "--nonet",
52                         action="store_true", dest="no_netns", default=False,
53                         help="Don't enter network namespace")
54     parser.add_argument("-m", "--nomnt",
55                         action="store_true", dest="no_mntns", default=False,
56                         help="Don't enter mount namespace")
57     parser.add_argument("-p", "--nopid",
58                         action="store_true", dest="no_pidns", default=False,
59                         help="Don't enter pid namespace")
60     parser.add_argument("-r", "--root",
61                         action="store_true", dest="root", default=False,
62                         help="Enter as root: be careful")
63     parser.add_argument("-i", "--internal",
64                         action="store_true", dest="internal", default=False,
65                         help="does *not* prepend '-- -c' to arguments - or invoke lxcsu-internal")
66     parser.add_argument("-d", "--debug",
67                         action='store_true', dest='debug', default=False,
68                         help="debug option")
69     parser.add_argument("-s", "--nosliceuid",
70                         action='store_true', dest="nosliceuid", default=False,
71                         help="do not change to slice uid inside of slice")
72     parser.add_argument("-o", "--noslicehome",
73                         action='store_true', dest="noslicehome", default=False,
74                         help="do not change to slice home directory inside of slice")
75
76     if os.path.exists("/etc/lxcsu_default"):
77         defaults = parser.parse_args(
78             file("/etc/lxcsu_default", "r").read().split())
79         parser.set_defaults(**defaults.__dict__)
80
81     parser.add_argument("slice_name")
82     parser.add_argument("command_to_run", nargs="*")
83
84     args = parser.parse_args()
85     slice_name = args.slice_name
86
87     # support for either setting debug at the top of this file, or on the command-line
88     if args.debug:
89         global debug
90         debug = True
91
92     # somehow some older nodes won't be able to find the login name in /etc/passwd
93     # when this is done down the road, so compute slice_uid while in a safe env
94     # even though we don't use the slice_uid any more, this is still
95     # checked later on as a means to ensure existence of the slice account
96     try:
97         slice_uid = pwd.getpwnam(slice_name).pw_uid
98     except Exception as e:
99         if debug:
100             import traceback
101             print('error while computing slice_uid', e)
102             traceback.print_exc()
103         slice_uid = None
104
105     # unless we run the symlink 'lxcsu-internal', or we specify the -i option, prepend '--' '-c'
106     if sys.argv[0].find('internal') >= 0:
107         args.internal = True
108
109     if len(args.command_to_run) > 0 and (args.command_to_run[0] == "/sbin/service"):
110         # A quick hack to support nodemanager interfaces.py when restarting
111         # networking in a slice.
112         args.nosliceuid = True
113
114     # plain lxcsu
115     if not args.internal:
116         # no command given: enter interactive shell
117         if not args.command_to_run:
118             args.command_to_run = ['/bin/sh']
119         args.command_to_run = ['-c'] + [" ".join(args.command_to_run)]
120
121     try:
122         cmd = '/usr/bin/virsh --connect lxc:/// domid %s' % slice_name
123         # convert to int as a minimal raincheck
124         driver_pid = int(os.popen(cmd).read().strip())
125         # locate the pid for the - expected - single child, that would be the init for that VM
126         #init_pid = int(open("/proc/%s/task/%s/children"%(driver_pid,driver_pid)).read().strip())
127         init_pid = int(os.popen('pgrep -P %s' %
128                                 driver_pid).readlines()[0].strip())
129         # Thierry: I am changing the code below to use init_pid instead of driver_pid
130         # for the namespace handling features, that I was able to check
131         # I've left the other ones as they were, i.e. using driver_pid, but I suspect
132         # some should be changed as well
133
134     except:
135         print("Domain %s not found" % slice_name)
136         exit(1)
137
138     if not driver_pid or not init_pid:
139         print("Domain %s not started" % slice_name)
140         exit(1)
141
142     if debug:
143         print("Found driver_pid", driver_pid, 'and init_pid=', init_pid)
144     # driver_pid is always x86_64, we need to look at the VM's init process here
145     arch = getarch(init_pid)
146
147     # Set sysctls specific to slice
148     sysctls = []
149     sysctl_dir = '/etc/planetlab/vsys-attributes/%s' % slice_name
150     if (os.access(sysctl_dir, 0)):
151         entries = os.listdir(sysctl_dir)
152         for e in entries:
153             prefix = 'vsys_sysctl.'
154             if (e.startswith(prefix)):
155                 sysctl_file = '/'.join([sysctl_dir, e])
156                 sysctl_name = e[len(prefix):]
157                 sysctl_val = open(sysctl_file).read()
158                 sysctls.append((sysctl_file, sysctl_name, sysctl_val))
159
160     # xxx probably init_pid here too
161     subdirs = get_cgroup_subdirs_for_pid(driver_pid)
162     sysfs_root = '/sys/fs/cgroup'
163
164     # If the slice is frozen, then we'll get an EBUSY when trying to write to the task
165     # list for the freezer cgroup. Since the user couldn't do anything anyway, it's best
166     # in this case to error out the shell. (an alternative would be to un-freeze it,
167     # add the task, and re-freeze it)
168     # Enter cgroups
169     current_cgroup = ''
170     for subsystem in ['cpuset', 'memory', 'blkio', 'cpuacct', 'cpuacct,cpu', 'freezer']:
171         try:
172             current_cgroup = subsystem
173
174             # There seems to be a bug in the cgroup schema: cpuacct,cpu can become cpu,cpuacct
175             # We need to handle both
176             task_path_alt = None
177             try:
178                 subsystem_comps = subsystem.split(',')
179                 subsystem_comps.reverse()
180                 subsystem_alt = ','.join(subsystem_comps)
181                 tasks_path_alt = [sysfs_root, subsystem_alt,
182                                   subdirs[subsystem], 'tasks']
183             except Exception as e:
184                 pass
185
186             tasks_path = [sysfs_root, subsystem, subdirs[subsystem], 'tasks']
187             tasks_path_str = '/'.join(tasks_path)
188
189             try:
190                 f = open(tasks_path_str, 'w')
191             except:
192                 tasks_path_alt_str = '/'.join(tasks_path_alt)
193                 f = open(tasks_path_alt_str, 'w')
194
195             f.write(str(os.getpid()))
196             if (subsystem == 'freezer'):
197                 f.close()
198
199         except Exception as e:
200             if (subsystem not in subdirs):
201                 pass
202             else:
203                 if debug:
204                     print(e)
205                 print("Error assigning cgroup %s (pid=%s) for slice %s" %
206                       (current_cgroup, driver_pid, slice_name))
207                 exit(1)
208
209     def chcontext(path):
210         retcod = setns.chcontext(path)
211         if retcod != 0:
212             print('WARNING - setns(%s)=>%s (ignored)' % (path, retcod))
213         return retcod
214
215     # Use init_pid and not driver_pid to locate reference namespaces
216     ref_ns = "/proc/%s/ns/" % init_pid
217
218     if True:
219         chcontext(ref_ns+'uts')
220     if True:
221         chcontext(ref_ns+'ipc')
222
223     if (not args.no_pidns):
224         chcontext(ref_ns+'pid')
225     if (not args.no_netns):
226         chcontext(ref_ns+'net')
227     if (not args.no_mntns):
228         chcontext(ref_ns+'mnt')
229
230     proc_mounted = False
231     if (not os.access('/proc/self', 0)):
232         proc_mounted = True
233         setns.proc_mount()
234
235     for (sysctl_file, sysctl_name, sysctl_val) in sysctls:
236         for fn in ["/sbin/sysctl", "/usr/sbin/sysctl", "/bin/sysctl", "/usr/bin/sysctl"]:
237             if os.path.exists(fn):
238                 os.system('%s -w %s=%s  >/dev/null 2>&1' %
239                           (fn, sysctl_name, sysctl_val))
240                 break
241             else:
242                 print("Error: image does not have a sysctl binary")
243
244     # cgroups is not yet LXC-safe, so we need to use the coarse grained access control
245     # strategy of unmounting the filesystem
246
247     umount_result = True
248     for subsystem in ['cpuset', 'cpu,cpuacct', 'memory', 'devices', 'freezer', 'net_cls', 'blkio', 'perf_event', 'systemd']:
249         fs_path = '/sys/fs/cgroup/%s' % subsystem
250         if (not umount(fs_path, '-l')):
251             print('WARNING - umount failed (ignored) with path=', fs_path)
252             pass
253             # Leaving these comments for historical reference
254             # print "Error disabling cgroup access"
255             # exit(1) - Don't need this because failure here implies failure in the call to umount /sys/fs/cgroup
256
257     if (not umount('/sys/fs/cgroup')):
258         print("Error disabling cgroup access")
259         exit(1)
260
261     fork_pid = os.fork()
262
263     if (fork_pid == 0):
264         if (not args.root):
265             setns.drop_caps()
266             if (args.nosliceuid):
267                 # we still want to drop capabilities, but don't want to switch UIDs
268                 exec_args = [arch, '/bin/sh', '--login', ]+args.command_to_run
269             else:
270                 if not slice_uid:
271                     print("lxcsu could not spot %s in /etc/passwd - exiting" %
272                           slice_name)
273                     exit(1)
274                 exec_args = [arch, '/usr/bin/sudo', '-u', slice_name,
275                              '/bin/sh', '--login', ]+args.command_to_run
276 # once we can drop f12, it would be nicer to instead go for
277 # exec_args = [arch,'/usr/sbin/capsh',cap_arg,'--user=%s'%slice_name,'--login',]+args.command_to_run
278         else:
279             exec_args = [arch, '/bin/sh', '--login']+args.command_to_run
280
281         os.environ['SHELL'] = '/bin/sh'
282         if os.path.exists('/etc/planetlab/lib/bind_public.so'):
283             os.environ['LD_PRELOAD'] = '/etc/planetlab/lib/bind_public.so'
284         if not args.noslicehome:
285             os.environ['HOME'] = '/home/%s' % slice_name
286             os.chdir("/home/%s" % (slice_name))
287         if debug:
288             print('lxcsu:execv:', '/usr/bin/setarch', exec_args)
289         os.execv('/usr/bin/setarch', exec_args)
290     else:
291         setns.proc_umount()
292         _, status = os.waitpid(fork_pid, 0)
293         exit(os.WEXITSTATUS(status))
294
295
296 if __name__ == '__main__':
297     main()