]> git.proxmox.com Git - mirror_lxc.git/blob - src/lxc/lxc-start-ephemeral.in
ovl_rsync: make sure to umount
[mirror_lxc.git] / src / lxc / lxc-start-ephemeral.in
1 #!/usr/bin/python3
2 #
3 # lxc-start-ephemeral: Start a copy of a container using an overlay
4 #
5 # This python implementation is based on the work done in the original
6 # shell implementation done by Serge Hallyn in Ubuntu (and other contributors)
7 #
8 # (C) Copyright Canonical Ltd. 2012
9 #
10 # Authors:
11 # Stéphane Graber <stgraber@ubuntu.com>
12 #
13 # This library is free software; you can redistribute it and/or
14 # modify it under the terms of the GNU Lesser General Public
15 # License as published by the Free Software Foundation; either
16 # version 2.1 of the License, or (at your option) any later version.
17 #
18 # This library is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # Lesser General Public License for more details.
22 #
23 # You should have received a copy of the GNU Lesser General Public
24 # License along with this library; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
26 #
27
28 import argparse
29 import gettext
30 import lxc
31 import os
32 import pwd
33 import sys
34 import subprocess
35 import tempfile
36
37 _ = gettext.gettext
38 gettext.textdomain("lxc-start-ephemeral")
39
40
41 # Other functions
42 def randomMAC():
43 import random
44
45 mac = [0x00, 0x16, 0x3e,
46 random.randint(0x00, 0x7f),
47 random.randint(0x00, 0xff),
48 random.randint(0x00, 0xff)]
49 return ':'.join(map(lambda x: "%02x" % x, mac))
50
51
52 def get_rundir():
53 if os.geteuid() == 0:
54 return "@RUNTIME_PATH@"
55
56 if "XDG_RUNTIME_DIR" in os.environ:
57 return os.environ["XDG_RUNTIME_DIR"]
58
59 if "HOME" in os.environ:
60 return "%s/.cache/lxc/run/" % os.environ["HOME"]
61
62 raise Exception("Unable to find a runtime directory")
63
64
65 # Begin parsing the command line
66 parser = argparse.ArgumentParser(description=_(
67 "LXC: Start an ephemeral container"),
68 formatter_class=argparse.RawTextHelpFormatter,
69 epilog=_("If a COMMAND is given, then the "
70 """container will run only as long
71 as the command runs.
72 If no COMMAND is given, this command will attach to tty1 and stop the
73 container when exiting (with ctrl-a-q).
74
75 If no COMMAND is given and -d is used, the name and IP addresses of the
76 container will be printed to the console."""))
77
78 parser.add_argument("--lxcpath", "-P", dest="lxcpath", metavar="PATH",
79 help=_("Use specified container path"), default=None)
80
81 parser.add_argument("--orig", "-o", type=str, required=True,
82 help=_("name of the original container"))
83
84 parser.add_argument("--name", "-n", type=str,
85 help=_("name of the target container"))
86
87 parser.add_argument("--bdir", "-b", type=str, action="append", default=[],
88 help=_("directory to bind mount into container, "
89 "either --bdir=/src-path or --bdir=/src-path:/dst-path"))
90
91 parser.add_argument("--cdir", "-c", type=str, action="append", default=[],
92 help=_("directory to cow mount into container"))
93
94 parser.add_argument("--user", "-u", type=str,
95 help=_("the user to run the command as"))
96
97 parser.add_argument("--key", "-S", type=str,
98 help=_("the path to the key to use to connect "
99 "(when using ssh)"))
100
101 parser.add_argument("--daemon", "-d", action="store_true",
102 help=_("run in the background"))
103
104 parser.add_argument("--storage-type", "-s", type=str, default=None,
105 choices=("tmpfs", "dir"),
106 help=("type of storage use by the container"))
107
108 parser.add_argument("--union-type", "-U", type=str, default="overlayfs",
109 choices=("overlayfs", "aufs"),
110 help=_("type of union (overlayfs or aufs), "
111 "defaults to overlayfs."))
112
113 parser.add_argument("--keep-data", "-k", action="store_true",
114 help=_("don't wipe everything clean at the end"))
115
116 parser.add_argument("command", metavar='CMD', type=str, nargs="*",
117 help=_("Run specific command in container "
118 "(command as argument)"))
119
120 parser.add_argument("--version", action="version", version=lxc.version)
121
122 args = parser.parse_args()
123
124 ## Check that -d and CMD aren't used at the same time
125 if args.command and args.daemon:
126 parser.error(_("You can't use -d and a command at the same time."))
127
128 ## Check that -k isn't used with -s tmpfs
129 if not args.storage_type:
130 if args.keep_data:
131 args.storage_type = "dir"
132 else:
133 args.storage_type = "tmpfs"
134
135 if args.keep_data and args.storage_type == "tmpfs":
136 parser.error(_("You can't use -k with the tmpfs storage type."))
137
138 # Load the orig container
139 orig = lxc.Container(args.orig, args.lxcpath)
140 if not orig.defined:
141 parser.error(_("Source container '%s' doesn't exist." % args.orig))
142
143 # Create the new container paths
144 if not args.lxcpath:
145 lxc_path = lxc.default_config_path
146 else:
147 lxc_path = args.lxcpath
148
149 if args.name:
150 if os.path.exists("%s/%s" % (lxc_path, args.name)):
151 parser.error(_("A container named '%s' already exists." % args.name))
152 dest_path = "%s/%s" % (lxc_path, args.name)
153 os.mkdir(dest_path)
154 else:
155 dest_path = tempfile.mkdtemp(prefix="%s-" % args.orig, dir=lxc_path)
156 os.mkdir(os.path.join(dest_path, "rootfs"))
157 os.chmod(dest_path, 0o770)
158
159 # Setup the new container's configuration
160 dest = lxc.Container(os.path.basename(dest_path), args.lxcpath)
161 dest.load_config(orig.config_file_name)
162 dest.set_config_item("lxc.utsname", dest.name)
163 dest.set_config_item("lxc.rootfs", os.path.join(dest_path, "rootfs"))
164 print("setting rootfs to .%s.", os.path.join(dest_path, "rootfs"))
165 for nic in dest.network:
166 if hasattr(nic, 'hwaddr'):
167 nic.hwaddr = randomMAC()
168
169 overlay_dirs = [(orig.get_config_item("lxc.rootfs"), "%s/rootfs/" % dest_path)]
170
171 # Generate a new fstab
172 if orig.get_config_item("lxc.mount"):
173 dest.set_config_item("lxc.mount", os.path.join(dest_path, "fstab"))
174 with open(orig.get_config_item("lxc.mount"), "r") as orig_fd:
175 with open(dest.get_config_item("lxc.mount"), "w+") as dest_fd:
176 for line in orig_fd.read().split("\n"):
177 # Start by replacing any reference to the container rootfs
178 line.replace(orig.get_config_item("lxc.rootfs"),
179 dest.get_config_item("lxc.rootfs"))
180
181 fields = line.split()
182
183 # Skip invalid entries
184 if len(fields) < 4:
185 continue
186
187 # Non-bind mounts are kept as-is
188 if "bind" not in fields[3]:
189 dest_fd.write("%s\n" % line)
190 continue
191
192 # Bind mounts of virtual filesystems are also kept as-is
193 src_path = fields[0].split("/")
194 if len(src_path) > 1 and src_path[1] in ("proc", "sys"):
195 dest_fd.write("%s\n" % line)
196 continue
197
198 # Skip invalid mount points
199 dest_mount = os.path.abspath(os.path.join("%s/rootfs/" % (
200 dest_path), fields[1]))
201
202 if "%s/rootfs/" % dest_path not in dest_mount:
203 print(_("Skipping mount entry '%s' as it's outside "
204 "of the container rootfs.") % line)
205
206 # Setup an overlay for anything remaining
207 overlay_dirs += [(fields[0], dest_mount)]
208
209 for entry in args.cdir:
210 if not os.path.exists(entry):
211 print(_("Path '%s' doesn't exist, won't be cow-mounted.") %
212 entry)
213 else:
214 src_path = os.path.abspath(entry)
215 dst_path = "%s/rootfs/%s" % (dest_path, src_path)
216 overlay_dirs += [(src_path, dst_path)]
217
218 # do we have the new overlay fs which requires workdir, or the older
219 # overlayfs which does not?
220 have_new_overlay = False
221 with open("/proc/filesystems", "r") as fd:
222 for line in fd:
223 if line == "nodev\toverlay\n":
224 have_new_overlay = True
225
226 # Generate pre-mount script
227 with open(os.path.join(dest_path, "pre-mount"), "w+") as fd:
228 os.fchmod(fd.fileno(), 0o755)
229 fd.write("""#!/bin/sh
230 LXC_DIR="%s"
231 LXC_BASE="%s"
232 LXC_NAME="%s"
233 """ % (dest_path, orig.name, dest.name))
234
235 count = 0
236 for entry in overlay_dirs:
237 tmpdir = "%s/tmpfs" % dest_path
238 fd.write("mkdir -p %s\n" % (tmpdir))
239 if args.storage_type == "tmpfs":
240 fd.write("mount -n -t tmpfs -o mode=0755 none %s\n" % (tmpdir))
241 deltdir = "%s/delta%s" % (tmpdir, count)
242 workdir = "%s/work%s" % (tmpdir, count)
243 fd.write("mkdir -p %s %s\n" % (deltdir, entry[1]))
244 if have_new_overlay:
245 fd.write("mkdir -p %s\n" % workdir)
246
247 fd.write("getfacl -a %s | setfacl --set-file=- %s || true\n" %
248 (entry[0], deltdir))
249 fd.write("getfacl -a %s | setfacl --set-file=- %s || true\n" %
250 (entry[0], entry[1]))
251
252 if args.union_type == "overlayfs":
253 if have_new_overlay:
254 fd.write("mount -n -t overlay"
255 " -oupperdir=%s,lowerdir=%s,workdir=%s none %s\n" % (
256 deltdir,
257 entry[0],
258 workdir,
259 entry[1]))
260 else:
261 fd.write("mount -n -t overlayfs"
262 " -oupperdir=%s,lowerdir=%s none %s\n" % (
263 deltdir,
264 entry[0],
265 entry[1]))
266 elif args.union_type == "aufs":
267 xino_path = "/dev/shm/aufs.xino"
268 if not os.path.exists(os.path.basename(xino_path)):
269 os.makedirs(os.path.basename(xino_path))
270
271 fd.write("mount -n -t aufs "
272 "-o br=%s=rw:%s=ro,noplink,xino=%s none %s\n" % (
273 deltdir,
274 entry[0],
275 xino_path,
276 entry[1]))
277 count += 1
278
279 for entry in args.bdir:
280 if ':' in entry:
281 src_path, dst_path = entry.split(":")
282 else:
283 src_path = entry
284 dst_path = os.path.abspath(entry)
285
286 if not os.path.exists(src_path):
287 print(_("Path '%s' doesn't exist, won't be bind-mounted.") %
288 src_path)
289 else:
290 src_path = os.path.abspath(src_path)
291 dst_path = "%s/rootfs/%s" % (dest_path, dst_path)
292 fd.write("mkdir -p %s\nmount -n --bind %s %s\n" % (
293 dst_path, src_path, dst_path))
294
295 fd.write("""
296 [ -e $LXC_DIR/configured ] && exit 0
297 for file in $LXC_DIR/rootfs/etc/hostname \\
298 $LXC_DIR/rootfs/etc/hosts \\
299 $LXC_DIR/rootfs/etc/sysconfig/network \\
300 $LXC_DIR/rootfs/etc/sysconfig/network-scripts/ifcfg-eth0; do
301 [ -f "$file" ] && sed -i -e "s/$LXC_BASE/$LXC_NAME/" $file
302 done
303 touch $LXC_DIR/configured
304 """)
305
306 dest.set_config_item("lxc.hook.pre-mount",
307 os.path.join(dest_path, "pre-mount"))
308
309 # Generate post-stop script
310 if not args.keep_data:
311 with open(os.path.join(dest_path, "post-stop"), "w+") as fd:
312 os.fchmod(fd.fileno(), 0o755)
313 fd.write("""#!/bin/sh
314 [ -d "%s" ] && rm -Rf "%s"
315 """ % (dest_path, dest_path))
316
317 dest.set_config_item("lxc.hook.post-stop",
318 os.path.join(dest_path, "post-stop"))
319
320 dest.save_config()
321
322 # Start the container
323 if not dest.start() or not dest.wait("RUNNING", timeout=5):
324 print(_("The container '%s' failed to start.") % dest.name)
325 dest.stop()
326 if dest.defined:
327 dest.destroy()
328 sys.exit(1)
329
330 # Deal with the case where we just attach to the container's console
331 if not args.command and not args.daemon:
332 dest.console()
333 if not dest.shutdown(timeout=5):
334 dest.stop()
335 sys.exit(0)
336
337 # Try to get the IP addresses
338 ips = dest.get_ips(timeout=10)
339
340 # Deal with the case where we just print info about the container
341 if args.daemon:
342 print(_("""The ephemeral container is now started.
343
344 You can enter it from the command line with: lxc-console -n %s
345 The following IP addresses have be found in the container:
346 %s""") % (dest.name,
347 "\n".join([" - %s" % entry for entry in ips]
348 or [" - %s" % _("No address could be found")])))
349 sys.exit(0)
350
351 # Now deal with the case where we want to run a command in the container
352 if not ips:
353 print(_("Failed to get an IP for container '%s'.") % dest.name)
354 dest.stop()
355 if dest.defined:
356 dest.destroy()
357 sys.exit(1)
358
359 if os.path.exists("/proc/self/ns/pid"):
360 def attach_as_user(command):
361 try:
362 username = "root"
363 if args.user:
364 username = args.user
365
366 user = pwd.getpwnam(username)
367 os.setgid(user.pw_gid)
368 os.initgroups(user.pw_name, user.pw_gid)
369 os.setuid(user.pw_uid)
370 os.chdir(user.pw_dir)
371 os.environ['HOME'] = user.pw_dir
372 except:
373 print(_("Unable to switch to user: %s" % username))
374 sys.exit(1)
375
376 return lxc.attach_run_command(command)
377
378 retval = dest.attach_wait(attach_as_user, args.command,
379 env_policy=lxc.LXC_ATTACH_CLEAR_ENV)
380
381 else:
382 cmd = ["ssh",
383 "-o", "StrictHostKeyChecking=no",
384 "-o", "UserKnownHostsFile=/dev/null"]
385
386 if args.user:
387 cmd += ["-l", args.user]
388
389 if args.key:
390 cmd += ["-i", args.key]
391
392 for ip in ips:
393 ssh_cmd = cmd + [ip] + args.command
394 retval = subprocess.call(ssh_cmd, universal_newlines=True)
395 if retval == 255:
396 print(_("SSH failed to connect, trying next IP address."))
397 continue
398
399 if retval != 0:
400 print(_("Command returned with non-zero return code: %s") % retval)
401 break
402
403 # Shutdown the container
404 if not dest.shutdown(timeout=5):
405 dest.stop()
406
407 sys.exit(retval)