]> git.proxmox.com Git - mirror_lxc.git/blob - src/lxc/lxc-start-ephemeral.in
pivot_root: switch to a new mechanism (v2)
[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,
88 help=_("directory to bind mount into container"))
89
90 parser.add_argument("--user", "-u", type=str,
91 help=_("the user to run the command as"))
92
93 parser.add_argument("--key", "-S", type=str,
94 help=_("the path to the key to use to connect "
95 "(when using ssh)"))
96
97 parser.add_argument("--daemon", "-d", action="store_true",
98 help=_("run in the background"))
99
100 parser.add_argument("--storage-type", "-s", type=str, default=None,
101 choices=("tmpfs", "dir"),
102 help=("type of storage use by the container"))
103
104 parser.add_argument("--union-type", "-U", type=str, default="overlayfs",
105 choices=("overlayfs", "aufs"),
106 help=_("type of union (overlayfs or aufs), "
107 "defaults to overlayfs."))
108
109 parser.add_argument("--keep-data", "-k", action="store_true",
110 help=_("don't wipe everything clean at the end"))
111
112 parser.add_argument("command", metavar='CMD', type=str, nargs="*",
113 help=_("Run specific command in container "
114 "(command as argument)"))
115
116 parser.add_argument("--version", action="version", version=lxc.version)
117
118 args = parser.parse_args()
119
120 # Basic requirements check
121 ## We only support privileged containers for now
122 if os.geteuid() != 0 and args.union_type != "overlayfs":
123 parser.error(_("Unprivileged containers may only use "
124 "overlayfs at this time."))
125
126 ## Check that -d and CMD aren't used at the same time
127 if args.command and args.daemon:
128 parser.error(_("You can't use -d and a command at the same time."))
129
130 ## Check that -k isn't used with -s tmpfs
131 if not args.storage_type:
132 if args.keep_data:
133 args.storage_type = "dir"
134 else:
135 args.storage_type = "tmpfs"
136
137 if args.keep_data and args.storage_type == "tmpfs":
138 parser.error(_("You can't use -k with the tmpfs storage type."))
139
140 # Load the orig container
141 orig = lxc.Container(args.orig, args.lxcpath)
142 if not orig.defined:
143 parser.error(_("Source container '%s' doesn't exist." % args.orig))
144
145 # Create the new container paths
146 if not args.lxcpath:
147 lxc_path = lxc.default_config_path
148 else:
149 lxc_path = args.lxcpath
150
151 if args.name:
152 if os.path.exists("%s/%s" % (lxc_path, args.name)):
153 parser.error(_("A container named '%s' already exists." % args.name))
154 dest_path = "%s/%s" % (lxc_path, args.name)
155 os.mkdir(dest_path)
156 else:
157 dest_path = tempfile.mkdtemp(prefix="%s-" % args.orig, dir=lxc_path)
158 os.mkdir(os.path.join(dest_path, "rootfs"))
159
160 # Setup the new container's configuration
161 dest = lxc.Container(os.path.basename(dest_path), args.lxcpath)
162 dest.load_config(orig.config_file_name)
163 dest.set_config_item("lxc.utsname", dest.name)
164 dest.set_config_item("lxc.rootfs", 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 # Generate pre-mount script
210 with open(os.path.join(dest_path, "pre-mount"), "w+") as fd:
211 os.fchmod(fd.fileno(), 0o755)
212 fd.write("""#!/bin/sh
213 LXC_DIR="%s"
214 LXC_BASE="%s"
215 LXC_NAME="%s"
216 """ % (dest_path, orig.name, dest.name))
217
218 count = 0
219 for entry in overlay_dirs:
220 target = "%s/delta%s" % (dest_path, count)
221 fd.write("mkdir -p %s %s\n" % (target, entry[1]))
222
223 if args.storage_type == "tmpfs":
224 fd.write("mount -n -t tmpfs -o mode=0755 none %s\n" % (target))
225
226 if args.union_type == "overlayfs":
227 fd.write("mount -n -t overlayfs"
228 " -oupperdir=%s,lowerdir=%s none %s\n" % (
229 target,
230 entry[0],
231 entry[1]))
232 elif args.union_type == "aufs":
233 xino_path = "%s/lxc/aufs.xino" % get_rundir()
234 if not os.path.exists(os.path.basename(xino_path)):
235 os.makedirs(os.path.basename(xino_path))
236
237 fd.write("mount -n -t aufs "
238 "-o br=%s=rw:%s=ro,noplink,xino=%s none %s\n" % (
239 target,
240 entry[0],
241 xino_path,
242 entry[1]))
243 count += 1
244
245 if args.bdir:
246 if not os.path.exists(args.bdir):
247 print(_("Path '%s' doesn't exist, won't be bind-mounted.") %
248 args.bdir)
249 else:
250 src_path = os.path.abspath(args.bdir)
251 dst_path = "%s/rootfs/%s" % (dest_path, os.path.abspath(args.bdir))
252 fd.write("mkdir -p %s\nmount -n --bind %s %s\n" % (
253 dst_path, src_path, dst_path))
254
255 fd.write("""
256 [ -e $LXC_DIR/configured ] && exit 0
257 for file in $LXC_DIR/rootfs/etc/hostname \\
258 $LXC_DIR/rootfs/etc/hosts \\
259 $LXC_DIR/rootfs/etc/sysconfig/network \\
260 $LXC_DIR/rootfs/etc/sysconfig/network-scripts/ifcfg-eth0; do
261 [ -f "$file" ] && sed -i -e "s/$LXC_BASE/$LXC_NAME/" $file
262 done
263 touch $LXC_DIR/configured
264 """)
265
266 dest.set_config_item("lxc.hook.pre-mount",
267 os.path.join(dest_path, "pre-mount"))
268
269 # Generate post-stop script
270 if not args.keep_data:
271 with open(os.path.join(dest_path, "post-stop"), "w+") as fd:
272 os.fchmod(fd.fileno(), 0o755)
273 fd.write("""#!/bin/sh
274 [ -d "%s" ] && rm -Rf "%s"
275 """ % (dest_path, dest_path))
276
277 dest.set_config_item("lxc.hook.post-stop",
278 os.path.join(dest_path, "post-stop"))
279
280 dest.save_config()
281
282 # Start the container
283 if not dest.start() or not dest.wait("RUNNING", timeout=5):
284 print(_("The container '%s' failed to start.") % dest.name)
285 dest.stop()
286 if dest.defined:
287 dest.destroy()
288 sys.exit(1)
289
290 # Deal with the case where we just attach to the container's console
291 if not args.command and not args.daemon:
292 dest.console()
293 if not dest.shutdown(timeout=5):
294 dest.stop()
295 sys.exit(0)
296
297 # Try to get the IP addresses
298 ips = dest.get_ips(timeout=10)
299
300 # Deal with the case where we just print info about the container
301 if args.daemon:
302 print(_("""The ephemeral container is now started.
303
304 You can enter it from the command line with: lxc-console -n %s
305 The following IP addresses have be found in the container:
306 %s""") % (dest.name,
307 "\n".join([" - %s" % entry for entry in ips]
308 or [" - %s" % _("No address could be found")])))
309 sys.exit(0)
310
311 # Now deal with the case where we want to run a command in the container
312 if not ips:
313 print(_("Failed to get an IP for container '%s'.") % dest.name)
314 dest.stop()
315 if dest.defined:
316 dest.destroy()
317 sys.exit(1)
318
319 if os.path.exists("/proc/self/ns/pid"):
320 def attach_as_user(command):
321 try:
322 username = "root"
323 if args.user:
324 username = args.user
325
326 user = pwd.getpwnam(username)
327 os.setgid(user.pw_gid)
328 os.initgroups(user.pw_name, user.pw_gid)
329 os.setuid(user.pw_uid)
330 os.chdir(user.pw_dir)
331 os.environ['HOME'] = user.pw_dir
332 except:
333 print(_("Unable to switch to user: %s" % username))
334 sys.exit(1)
335
336 return lxc.attach_run_command(command)
337
338 retval = dest.attach_wait(attach_as_user, args.command,
339 env_policy=lxc.LXC_ATTACH_CLEAR_ENV)
340
341 else:
342 cmd = ["ssh",
343 "-o", "StrictHostKeyChecking=no",
344 "-o", "UserKnownHostsFile=/dev/null"]
345
346 if args.user:
347 cmd += ["-l", args.user]
348
349 if args.key:
350 cmd += ["-i", args.key]
351
352 for ip in ips:
353 ssh_cmd = cmd + [ip] + args.command
354 retval = subprocess.call(ssh_cmd, universal_newlines=True)
355 if retval == 255:
356 print(_("SSH failed to connect, trying next IP address."))
357 continue
358
359 if retval != 0:
360 print(_("Command returned with non-zero return code: %s") % retval)
361 break
362
363 # Shutdown the container
364 if not dest.shutdown(timeout=5):
365 dest.stop()
366
367 sys.exit(retval)