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