]>
Commit | Line | Data |
---|---|---|
4e7186c5 SG |
1 | #!/usr/bin/python3 |
2 | # | |
3 | # lxc-ls: List containers | |
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 |
4e7186c5 SG |
26 | # |
27 | ||
4e7186c5 SG |
28 | import argparse |
29 | import gettext | |
0e21ea4b | 30 | import json |
4e7186c5 | 31 | import lxc |
daf04e4c | 32 | import os |
4e7186c5 | 33 | import re |
1563f8ef | 34 | import tempfile |
4e7186c5 SG |
35 | import sys |
36 | ||
37 | _ = gettext.gettext | |
38 | gettext.textdomain("lxc-ls") | |
39 | ||
40 | ||
41 | # Functions used later on | |
42 | def batch(iterable, cols=1): | |
43 | import math | |
44 | ||
45 | length = len(iterable) | |
46 | lines = math.ceil(length / cols) | |
47 | ||
48 | for line in range(lines): | |
49 | fields = [] | |
50 | for col in range(cols): | |
51 | index = line + (col * lines) | |
52 | if index < length: | |
53 | fields.append(iterable[index]) | |
54 | yield fields | |
55 | ||
56 | ||
57 | def getTerminalSize(): | |
58 | import os | |
59 | env = os.environ | |
60 | ||
61 | def ioctl_GWINSZ(fd): | |
62 | try: | |
63 | import fcntl | |
64 | import termios | |
65 | import struct | |
66 | cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, | |
67 | '1234')) | |
68 | return cr | |
69 | except: | |
70 | return | |
71 | ||
72 | cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) | |
73 | if not cr: | |
74 | try: | |
75 | fd = os.open(os.ctermid(), os.O_RDONLY) | |
76 | cr = ioctl_GWINSZ(fd) | |
77 | os.close(fd) | |
78 | except: | |
79 | pass | |
80 | ||
81 | if not cr: | |
82 | cr = (env.get('LINES', 25), env.get('COLUMNS', 80)) | |
83 | ||
84 | return int(cr[1]), int(cr[0]) | |
85 | ||
0e21ea4b | 86 | |
cfd149a6 | 87 | def getSubContainers(container): |
0e21ea4b | 88 | with open(os.devnull, "w") as fd: |
1563f8ef SG |
89 | fdnum, path = tempfile.mkstemp() |
90 | os.remove(path) | |
91 | ||
92 | fd = os.fdopen(fdnum) | |
93 | ||
94 | container.attach_wait( | |
95 | lxc.attach_run_command, [sys.argv[0], "--nesting"], | |
96 | attach_flags=(lxc.LXC_ATTACH_REMOUNT_PROC_SYS), | |
97 | namespaces=(lxc.CLONE_NEWNET + lxc.CLONE_NEWPID), | |
98 | extra_env_vars=["NESTED=/proc/1/root/%s" % | |
99 | lxc.default_config_path], | |
100 | stdout=fd) | |
101 | ||
102 | fd.seek(0) | |
103 | out = fd.read() | |
104 | fd.close() | |
0e21ea4b SG |
105 | if out: |
106 | return json.loads(out) | |
107 | return None | |
108 | ||
7f8c4031 | 109 | # Constants |
63d4950f SG |
110 | FIELDS = ("name", "state", "ipv4", "ipv6", "autostart", "pid", |
111 | "memory", "ram", "swap") | |
0e21ea4b | 112 | |
4e7186c5 SG |
113 | # Begin parsing the command line |
114 | parser = argparse.ArgumentParser(description=_("LXC: List containers"), | |
115 | formatter_class=argparse.RawTextHelpFormatter) | |
116 | ||
117 | parser.add_argument("-1", dest="one", action="store_true", | |
118 | help=_("list one container per line (default when piped)")) | |
119 | ||
9157421a | 120 | parser.add_argument("-P", "--lxcpath", dest="lxcpath", metavar="PATH", |
0e21ea4b SG |
121 | help=_("Use specified container path"), |
122 | default=lxc.default_config_path) | |
9157421a | 123 | |
4e7186c5 | 124 | parser.add_argument("--active", action="store_true", |
3eb967f0 | 125 | help=_("list only active containers")) |
4e7186c5 SG |
126 | |
127 | parser.add_argument("--frozen", dest="state", action="append_const", | |
128 | const="FROZEN", help=_("list only frozen containers")) | |
129 | ||
130 | parser.add_argument("--running", dest="state", action="append_const", | |
131 | const="RUNNING", help=_("list only running containers")) | |
132 | ||
133 | parser.add_argument("--stopped", dest="state", action="append_const", | |
134 | const="STOPPED", help=_("list only stopped containers")) | |
135 | ||
c5afb6e4 | 136 | parser.add_argument("-f", "--fancy", action="store_true", |
4e7186c5 SG |
137 | help=_("use fancy output")) |
138 | ||
c5afb6e4 | 139 | parser.add_argument("-F", "--fancy-format", type=str, |
7f8c4031 | 140 | default="name,state,ipv4,ipv6,autostart", |
4e7186c5 SG |
141 | help=_("comma separated list of fields to show")) |
142 | ||
0e21ea4b SG |
143 | parser.add_argument("--nesting", dest="nesting", action="store_true", |
144 | help=_("show nested containers")) | |
145 | ||
4e7186c5 SG |
146 | parser.add_argument("filter", metavar='FILTER', type=str, nargs="?", |
147 | help=_("regexp to be applied on the container list")) | |
148 | ||
149 | args = parser.parse_args() | |
150 | ||
151 | # --active is the same as --running --frozen | |
152 | if args.active: | |
153 | if not args.state: | |
154 | args.state = [] | |
3eb967f0 | 155 | args.state += ["RUNNING", "FROZEN", "UNKNOWN"] |
4e7186c5 SG |
156 | |
157 | # If the output is piped, default to --one | |
158 | if not sys.stdout.isatty(): | |
159 | args.one = True | |
160 | ||
0e21ea4b | 161 | # Set the lookup path for the containers |
cfd149a6 SG |
162 | # This value will contain the full path for a nested containers |
163 | # use args.lxcpath if you need the value relative to the container | |
164 | nest_lxcpath = os.environ.get('NESTED', args.lxcpath) | |
0e21ea4b | 165 | |
4e7186c5 SG |
166 | # Turn args.fancy_format into a list |
167 | args.fancy_format = args.fancy_format.strip().split(",") | |
168 | ||
7f8c4031 SG |
169 | if set(args.fancy_format) - set(FIELDS): |
170 | parser.error(_("Invalid field(s): %s" % | |
171 | ", ".join(list(set(args.fancy_format) - set(FIELDS))))) | |
172 | ||
0749e740 | 173 | # Basic checks |
3eb967f0 SG |
174 | ## Check for setns |
175 | SUPPORT_SETNS = os.path.exists("/proc/self/ns") | |
176 | SUPPORT_SETNS_NET = False | |
177 | SUPPORT_SETNS_PID = False | |
178 | if SUPPORT_SETNS: | |
179 | SUPPORT_SETNS_NET = os.path.exists("/proc/self/ns/net") | |
180 | SUPPORT_SETNS_PID = os.path.exists("/proc/self/ns/pid") | |
0749e740 | 181 | |
9c073d6b SG |
182 | ## Nesting requires setns to pid and net ns |
183 | if args.nesting: | |
3eb967f0 | 184 | if not SUPPORT_SETNS: |
9c073d6b SG |
185 | parser.error(_("Showing nested containers requires setns support " |
186 | "which your kernel doesn't support.")) | |
187 | ||
3eb967f0 | 188 | if not SUPPORT_SETNS_NET: |
9c073d6b | 189 | parser.error(_("Showing nested containers requires setns to the " |
3eb967f0 | 190 | "network namespace which your kernel doesn't support.")) |
9c073d6b | 191 | |
3eb967f0 | 192 | if not SUPPORT_SETNS_PID: |
9c073d6b | 193 | parser.error(_("Showing nested containers requires setns to the " |
3eb967f0 | 194 | "PID namespace which your kernel doesn't support.")) |
9c073d6b | 195 | |
4e7186c5 SG |
196 | # List of containers, stored as dictionaries |
197 | containers = [] | |
cfd149a6 | 198 | for container_name in lxc.list_containers(config_path=nest_lxcpath): |
0749e740 SG |
199 | entry = {} |
200 | entry['name'] = container_name | |
4e7186c5 SG |
201 | |
202 | # Apply filter | |
0749e740 | 203 | if args.filter and not re.match(args.filter, container_name): |
4e7186c5 SG |
204 | continue |
205 | ||
0749e740 | 206 | # Return before grabbing the object (non-root) |
0e21ea4b | 207 | if not args.state and not args.fancy and not args.nesting: |
0749e740 SG |
208 | containers.append(entry) |
209 | continue | |
210 | ||
9157421a | 211 | container = lxc.Container(container_name, args.lxcpath) |
0749e740 | 212 | |
cfd149a6 SG |
213 | if 'NESTED' in os.environ: |
214 | container.load_config(os.path.join(nest_lxcpath, container_name, | |
215 | "config")) | |
216 | ||
3eb967f0 SG |
217 | if container.controllable: |
218 | state = container.state | |
219 | else: | |
220 | state = 'UNKNOWN' | |
221 | ||
0749e740 | 222 | # Filter by status |
3eb967f0 | 223 | if args.state and state not in args.state: |
0749e740 | 224 | continue |
4e7186c5 SG |
225 | |
226 | # Nothing more is needed if we're not printing some fancy output | |
0e21ea4b | 227 | if not args.fancy and not args.nesting: |
4e7186c5 SG |
228 | containers.append(entry) |
229 | continue | |
230 | ||
231 | # Some extra field we may want | |
0e21ea4b | 232 | if 'state' in args.fancy_format or args.nesting: |
3eb967f0 | 233 | entry['state'] = state |
0e21ea4b SG |
234 | |
235 | if 'pid' in args.fancy_format or args.nesting: | |
4e7186c5 | 236 | entry['pid'] = "-" |
3eb967f0 SG |
237 | if state == 'UNKNOWN': |
238 | entry['pid'] = state | |
239 | elif container.init_pid != -1: | |
4e7186c5 SG |
240 | entry['pid'] = str(container.init_pid) |
241 | ||
7f8c4031 SG |
242 | if 'autostart' in args.fancy_format or args.nesting: |
243 | entry['autostart'] = "NO" | |
244 | try: | |
245 | if container.get_config_item("lxc.start.auto") == "1": | |
246 | entry['autostart'] = "YES" | |
247 | ||
248 | groups = container.get_config_item("lxc.group") | |
249 | if len(groups) > 0: | |
250 | entry['autostart'] = "YES (%s)" % ", ".join(groups) | |
251 | except KeyError: | |
252 | pass | |
253 | ||
63d4950f SG |
254 | if 'memory' in args.fancy_format or \ |
255 | 'ram' in args.fancy_format or \ | |
256 | 'swap' in args.fancy_format: | |
257 | ||
258 | if container.running: | |
259 | try: | |
260 | memory_total = int(container.get_cgroup_item( | |
261 | "memory.usage_in_bytes")) | |
262 | except: | |
263 | memory_total = 0 | |
264 | ||
265 | try: | |
266 | memory_swap = int(container.get_cgroup_item( | |
267 | "memory.memsw.usage_in_bytes")) | |
268 | except: | |
269 | memory_swap = 0 | |
270 | else: | |
271 | memory_total = 0 | |
272 | memory_swap = 0 | |
273 | ||
274 | if 'memory' in args.fancy_format: | |
275 | if container.running: | |
276 | entry['memory'] = "%sMB" % round(memory_total / 1048576, 2) | |
277 | else: | |
278 | entry['memory'] = "-" | |
279 | ||
280 | if 'ram' in args.fancy_format: | |
281 | if container.running: | |
282 | entry['ram'] = "%sMB" % round( | |
283 | (memory_total - memory_swap) / 1048576, 2) | |
284 | else: | |
285 | entry['ram'] = "-" | |
286 | ||
287 | if 'swap' in args.fancy_format: | |
288 | if container.running: | |
289 | entry['swap'] = "%sMB" % round(memory_swap / 1048576, 2) | |
290 | else: | |
291 | entry['swap'] = "-" | |
292 | ||
4e7186c5 | 293 | # Get the IPs |
ad5f1515 | 294 | for family, protocol in {'inet': 'ipv4', 'inet6': 'ipv6'}.items(): |
0e21ea4b | 295 | if protocol in args.fancy_format or args.nesting: |
4e7186c5 | 296 | entry[protocol] = "-" |
3eb967f0 SG |
297 | |
298 | if state == 'UNKNOWN': | |
299 | entry[protocol] = state | |
300 | continue | |
301 | ||
c868b261 | 302 | if container.running: |
ae22a220 | 303 | if not SUPPORT_SETNS_NET: |
c868b261 ÇO |
304 | entry[protocol] = 'UNKNOWN' |
305 | continue | |
306 | ||
307 | ips = container.get_ips(family=family) | |
308 | if ips: | |
309 | entry[protocol] = ", ".join(ips) | |
4e7186c5 | 310 | |
0e21ea4b | 311 | # Append the container |
4e7186c5 SG |
312 | containers.append(entry) |
313 | ||
0e21ea4b | 314 | # Nested containers |
1563f8ef | 315 | if args.nesting and container.state == "RUNNING": |
cfd149a6 | 316 | sub = getSubContainers(container) |
0e21ea4b SG |
317 | if sub: |
318 | for entry in sub: | |
319 | if 'nesting_parent' not in entry: | |
320 | entry['nesting_parent'] = [] | |
321 | entry['nesting_parent'].insert(0, container_name) | |
322 | entry['nesting_real_name'] = entry.get('nesting_real_name', | |
323 | entry['name']) | |
324 | entry['name'] = "%s/%s" % (container_name, entry['name']) | |
325 | containers += sub | |
326 | ||
327 | # Deal with json output: | |
328 | if 'NESTED' in os.environ: | |
329 | print(json.dumps(containers)) | |
330 | sys.exit(0) | |
4e7186c5 SG |
331 | |
332 | # Print the list | |
333 | ## Standard list with one entry per line | |
334 | if not args.fancy and args.one: | |
335 | for container in sorted(containers, | |
336 | key=lambda container: container['name']): | |
337 | print(container['name']) | |
338 | sys.exit(0) | |
339 | ||
340 | ## Standard list with multiple entries per line | |
341 | if not args.fancy and not args.one: | |
342 | # Get the longest name and extra simple list | |
343 | field_maxlength = 0 | |
344 | container_names = [] | |
345 | for container in containers: | |
346 | if len(container['name']) > field_maxlength: | |
347 | field_maxlength = len(container['name']) | |
348 | container_names.append(container['name']) | |
349 | ||
350 | # Figure out how many we can put per line | |
351 | width = getTerminalSize()[0] | |
352 | ||
353 | entries = int(width / (field_maxlength + 2)) | |
354 | if entries == 0: | |
355 | entries = 1 | |
356 | ||
357 | for line in batch(sorted(container_names), entries): | |
358 | line_format = "" | |
359 | for index in range(len(line)): | |
360 | line_format += "{line[%s]:%s}" % (index, field_maxlength + 2) | |
361 | ||
362 | print(line_format.format(line=line)) | |
363 | ||
364 | ## Fancy listing | |
365 | if args.fancy: | |
366 | field_maxlength = {} | |
367 | ||
368 | # Get the maximum length per field | |
369 | for field in args.fancy_format: | |
370 | field_maxlength[field] = len(field) | |
371 | ||
372 | for container in containers: | |
373 | for field in args.fancy_format: | |
0e21ea4b SG |
374 | if field == 'name' and 'nesting_real_name' in container: |
375 | fieldlen = len(" " * ((len(container['nesting_parent']) - 1) | |
376 | * 4) + " \_ " + container['nesting_real_name']) | |
377 | if fieldlen > field_maxlength[field]: | |
378 | field_maxlength[field] = fieldlen | |
379 | elif len(container[field]) > field_maxlength[field]: | |
4e7186c5 SG |
380 | field_maxlength[field] = len(container[field]) |
381 | ||
382 | # Generate the line format string based on the maximum length and | |
383 | # a 2 character padding | |
384 | line_format = "" | |
385 | index = 0 | |
386 | for field in args.fancy_format: | |
387 | line_format += "{fields[%s]:%s}" % (index, field_maxlength[field] + 2) | |
388 | index += 1 | |
389 | ||
390 | # Get the line length minus the padding of the last field | |
391 | line_length = -2 | |
392 | for field in field_maxlength: | |
393 | line_length += field_maxlength[field] + 2 | |
394 | ||
395 | # Print header | |
396 | print(line_format.format(fields=[header.upper() | |
397 | for header in args.fancy_format])) | |
398 | print("-" * line_length) | |
399 | ||
400 | # Print the entries | |
401 | for container in sorted(containers, | |
402 | key=lambda container: container['name']): | |
0e21ea4b SG |
403 | fields = [] |
404 | for field in args.fancy_format: | |
405 | if field == 'name' and 'nesting_real_name' in container: | |
406 | prefix = " " * ((len(container['nesting_parent']) - 1) * 4) | |
407 | fields.append(prefix + " \_ " + container['nesting_real_name']) | |
408 | else: | |
409 | fields.append(container[field]) | |
410 | ||
4e7186c5 | 411 | print(line_format.format(fields=fields)) |