]>
Commit | Line | Data |
---|---|---|
d7415aea SG |
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
26 | # | |
27 | ||
28 | # NOTE: To remove once the API is stabilized | |
29 | import warnings | |
30 | warnings.filterwarnings("ignore", "The python-lxc API isn't yet stable") | |
31 | ||
32 | import argparse | |
33 | import gettext | |
34 | import lxc | |
35 | import os | |
36 | import sys | |
37 | import subprocess | |
38 | import tempfile | |
39 | ||
40 | _ = gettext.gettext | |
41 | gettext.textdomain("lxc-start-ephemeral") | |
42 | ||
43 | ||
44 | # Other functions | |
45 | def randomMAC(): | |
46 | import random | |
47 | ||
48 | mac = [0x00, 0x16, 0x3e, | |
49 | random.randint(0x00, 0x7f), | |
50 | random.randint(0x00, 0xff), | |
51 | random.randint(0x00, 0xff)] | |
52 | return ':'.join(map(lambda x: "%02x" % x, mac)) | |
53 | ||
54 | # Begin parsing the command line | |
55 | parser = argparse.ArgumentParser( | |
56 | description=_("LXC: Start an ephemeral container"), | |
57 | formatter_class=argparse.RawTextHelpFormatter, epilog=_( | |
58 | """If a COMMAND is given, then the container will run only as long | |
59 | as the command runs. | |
60 | If no COMMAND is given, this command will attach to tty1 and stop the | |
61 | container when exiting (with ctrl-a-q). | |
62 | ||
63 | If no COMMAND is given and -d is used, the name and IP addresses of the | |
64 | container will be printed to the console.""")) | |
65 | ||
66 | parser.add_argument("--orig", "-o", type=str, required=True, | |
67 | help=_("name of the original container")) | |
68 | ||
69 | parser.add_argument("--bdir", "-b", type=str, | |
70 | help=_("directory to bind mount into container")) | |
71 | ||
72 | parser.add_argument("--user", "-u", type=str, | |
73 | help=_("the user to connect to the container as")) | |
74 | ||
75 | parser.add_argument("--key", "-S", type=str, | |
76 | help=_("the path to the SSH key to use to connect")) | |
77 | ||
78 | parser.add_argument("--daemon", "-d", action="store_true", | |
79 | help=_("run in the background")) | |
80 | ||
81 | parser.add_argument("--union-type", "-U", type=str, default="overlayfs", | |
82 | choices=("overlayfs", "aufs"), | |
83 | help=_("type of union (overlayfs or aufs), defaults to overlayfs.")) | |
84 | ||
85 | parser.add_argument("--keep-data", "-k", action="store_true", | |
86 | help=_("Use a persistent backend instead of tmpfs.")) | |
87 | ||
88 | parser.add_argument("command", metavar='CMD', type=str, nargs="*", | |
89 | help=_("Run specific command in container (command as argument)")) | |
90 | ||
91 | args = parser.parse_args() | |
92 | ||
93 | # Basic requirements check | |
94 | ## Check that -d and CMD aren't used at the same time | |
95 | if args.command and args.daemon: | |
96 | print(_("You can't use -d and a command at the same time.")) | |
97 | sys.exit(1) | |
98 | ||
99 | ## The user needs to be uid 0 | |
100 | if not os.geteuid() == 0: | |
101 | print(_("You must be root to run this script. Try running: sudo %s" % | |
102 | (sys.argv[0]))) | |
103 | sys.exit(1) | |
104 | ||
105 | # Load the orig container | |
106 | orig = lxc.Container(args.orig) | |
107 | if not orig.defined: | |
108 | print(_("Source container '%s' doesn't exist." % args.orig)) | |
109 | sys.exit(1) | |
110 | ||
111 | # Create the new container paths | |
95a717e9 | 112 | dest_path = tempfile.mkdtemp(prefix="%s-" % args.orig, dir="@LXCPATH@") |
d7415aea SG |
113 | os.mkdir(os.path.join(dest_path, "rootfs")) |
114 | ||
115 | # Setup the new container's configuration | |
116 | dest = lxc.Container(os.path.basename(dest_path)) | |
117 | dest.load_config(orig.config_file_name) | |
118 | dest.set_config_item("lxc.utsname", dest.name) | |
119 | dest.set_config_item("lxc.rootfs", os.path.join(dest_path, "rootfs")) | |
120 | dest.set_config_item("lxc.network.hwaddr", randomMAC()) | |
121 | ||
122 | overlay_dirs = [(orig.get_config_item("lxc.rootfs"), "%s/rootfs/" % dest_path)] | |
123 | ||
124 | # Generate a new fstab | |
125 | if orig.get_config_item("lxc.mount"): | |
126 | dest.set_config_item("lxc.mount", os.path.join(dest_path, "fstab")) | |
127 | with open(orig.get_config_item("lxc.mount"), "r") as orig_fd: | |
128 | with open(dest.get_config_item("lxc.mount"), "w+") as dest_fd: | |
129 | for line in orig_fd.read().split("\n"): | |
130 | # Start by replacing any reference to the container rootfs | |
131 | line.replace(orig.get_config_item("lxc.rootfs"), | |
132 | dest.get_config_item("lxc.rootfs")) | |
133 | ||
134 | # Skip any line that's not a bind mount | |
135 | fields = line.split() | |
136 | if len(fields) < 4: | |
137 | dest_fd.write("%s\n" % line) | |
138 | continue | |
139 | ||
140 | if fields[2] != "bind" and "bind" not in fields[3]: | |
141 | dest_fd.write("%s\n" % line) | |
142 | continue | |
143 | ||
144 | # Process any remaining line | |
145 | dest_mount = os.path.abspath(os.path.join("%s/rootfs/" % ( | |
146 | dest_path), fields[1])) | |
147 | ||
148 | if dest_mount == os.path.abspath("%s/rootfs/%s" % ( | |
149 | dest_path, args.bdir)): | |
150 | ||
151 | dest_fd.write("%s\n" % line) | |
152 | continue | |
153 | ||
154 | if "%s/rootfs/" % dest_path not in dest_mount: | |
155 | print(_( | |
156 | "Skipping mount entry '%s' as it's outside of the container rootfs.") % line) | |
157 | ||
158 | overlay_dirs += [(fields[0], dest_mount)] | |
159 | ||
160 | # Generate pre-mount script | |
161 | with open(os.path.join(dest_path, "pre-mount"), "w+") as fd: | |
162 | os.fchmod(fd.fileno(), 0o755) | |
163 | fd.write("""#!/bin/sh | |
164 | LXC_DIR="%s" | |
165 | LXC_BASE="%s" | |
166 | LXC_NAME="%s" | |
167 | """ % (dest_path, orig.name, dest.name)) | |
168 | ||
169 | count = 0 | |
170 | for entry in overlay_dirs: | |
171 | target = "%s/delta%s" % (dest_path, count) | |
172 | fd.write("mkdir -p %s %s\n" % (target, entry[1])) | |
173 | ||
174 | if not args.keep_data: | |
175 | fd.write("mount -n -t tmpfs none %s\n" % (target)) | |
176 | ||
177 | if args.union_type == "overlayfs": | |
178 | fd.write("mount -n -t overlayfs" | |
179 | " -oupperdir=%s,lowerdir=%s none %s\n" % ( | |
180 | target, | |
181 | entry[0], | |
182 | entry[1])) | |
183 | elif args.union_type == "aufs": | |
184 | fd.write("mount -n -t aufs " | |
185 | "-o br=${upper}=rw:${lower}=ro,noplink none %s\n" % ( | |
186 | target, | |
187 | entry[0], | |
188 | entry[1])) | |
189 | count += 1 | |
190 | ||
191 | if args.bdir: | |
192 | if not os.path.exists(args.bdir): | |
193 | print(_("Path '%s' doesn't exist, won't be bind-mounted.") % | |
194 | args.bdir) | |
195 | else: | |
196 | src_path = os.path.abspath(args.bdir) | |
197 | dst_path = "%s/rootfs/%s" % (dest_path, os.path.abspath(args.bdir)) | |
198 | fd.write("mkdir -p %s\nmount -n --bind %s %s\n" % ( | |
199 | dst_path, src_path, dst_path)) | |
200 | ||
201 | fd.write(""" | |
202 | [ -e $LXC_DIR/configured ] && exit 0 | |
203 | for file in $LXC_DIR/rootfs/etc/hostname \\ | |
204 | $LXC_DIR/rootfs/etc/hosts \\ | |
205 | $LXC_DIR/rootfs/etc/sysconfig/network \\ | |
206 | $LXC_DIR/rootfs/etc/sysconfig/network-scripts/ifcfg-eth0; do | |
207 | [ -f "$file" ] && sed -i -e "s/$LXC_BASE/$LXC_NAME/" $file | |
208 | done | |
209 | touch $LXC_DIR/configured | |
210 | """) | |
211 | ||
212 | dest.set_config_item("lxc.hook.pre-mount", | |
213 | os.path.join(dest_path, "pre-mount")) | |
214 | ||
215 | # Generate post-stop script | |
216 | if not args.keep_data: | |
217 | with open(os.path.join(dest_path, "post-stop"), "w+") as fd: | |
218 | os.fchmod(fd.fileno(), 0o755) | |
219 | fd.write("""#!/bin/sh | |
95a717e9 SG |
220 | [ -d "%s" ] && rm -Rf "%s" |
221 | """ % (dest.get_config_item("lxc.rootfs"), dest.get_config_item("lxc.rootfs")) | |
d7415aea SG |
222 | |
223 | dest.set_config_item("lxc.hook.post-stop", | |
224 | os.path.join(dest_path, "post-stop")) | |
225 | ||
226 | dest.save_config() | |
227 | ||
228 | # Start the container | |
229 | if not dest.start() or not dest.wait("RUNNING", timeout=5): | |
230 | print(_("The container '%s' failed to start.") % dest.name) | |
231 | dest.stop() | |
232 | if dest.defined: | |
233 | dest.destroy() | |
234 | sys.exit(1) | |
235 | ||
236 | # Try to get the IP addresses | |
237 | ips = dest.get_ips(timeout=5) | |
238 | ||
239 | # Deal with the case where we don't start a command in the container | |
240 | if not args.command: | |
241 | if args.daemon: | |
242 | print(_("""The ephemeral container is now started. | |
243 | ||
244 | You can enter it from the command line with: lxc-console -n %s | |
245 | The following IP addresses have be found in the container: | |
246 | %s""") % ( | |
247 | dest.name, | |
248 | "\n".join([" - %s" % entry for entry in ips] | |
249 | or [" - %s" % _("No address could be found")]) | |
250 | )) | |
251 | sys.exit(0) | |
252 | else: | |
253 | dest.console(tty=1) | |
254 | if not dest.shutdown(timeout=5): | |
255 | dest.stop() | |
256 | sys.exit(0) | |
257 | ||
258 | # Now deal with the case where we want to run a command in the container | |
259 | if not ips: | |
260 | print(_("Failed to get an IP for container '%s'.") % dest.name) | |
261 | dest.stop() | |
262 | if dest.defined: | |
263 | dest.destroy() | |
264 | sys.exit(1) | |
265 | ||
266 | # NOTE: To replace by .attach() once the kernel supports it | |
267 | cmd = ["ssh", | |
268 | "-o", "StrictHostKeyChecking=no", | |
269 | "-o", "UserKnownHostsFile=/dev/null"] | |
270 | ||
271 | if args.user: | |
272 | cmd += ["-l", args.user] | |
273 | ||
274 | if args.key: | |
275 | cmd += ["-k", args.key] | |
276 | ||
277 | for ip in ips: | |
278 | ssh_cmd = cmd + [ip] + args.command | |
279 | retval = subprocess.call(ssh_cmd, universal_newlines=True) | |
280 | if retval == 255: | |
281 | print(_("SSH failed to connect, trying next IP address.")) | |
282 | continue | |
283 | ||
284 | if retval != 0: | |
285 | print(_("Command returned with non-zero return code: %s") % retval) | |
286 | break | |
287 | ||
288 | # Shutdown the container | |
289 | if not dest.shutdown(timeout=5): | |
290 | dest.stop() |