lxc-ls: Implement support for nested containers

Add initial support for showing and querying nested containers.

This is done through a new --nesting argument to lxc-ls and uses
lxc-attach to go look for sub-containers.

Known limitations include the dependency on setns support for the PID
and NETWORK namespaces and the assumption that LXCPATH for the sub-containers
matches that of the host.

Signed-off-by: Stéphane Graber <stgraber@ubuntu.com>
Acked-by: Serge E. Hallyn <serge.hallyn@ubuntu.com>
This commit is contained in:
Stéphane Graber 2013-02-28 18:04:46 -05:00
parent 36368228d2
commit 0e21ea4b15
3 changed files with 81 additions and 21 deletions

View File

@ -56,6 +56,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
<arg choice="opt">--stopped</arg> <arg choice="opt">--stopped</arg>
<arg choice="opt">--fancy</arg> <arg choice="opt">--fancy</arg>
<arg choice="opt">--fancy-format</arg> <arg choice="opt">--fancy-format</arg>
<arg choice="opt">--nesting</arg>
<arg choice="opt">filter</arg> <arg choice="opt">filter</arg>
</cmdsynopsis> </cmdsynopsis>
</refsynopsisdiv> </refsynopsisdiv>
@ -150,6 +151,17 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term>
<option><optional>--nesting</optional></option>
</term>
<listitem>
<para>
Show nested containers.
</para>
</listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term> <term>
<option><optional>filter</optional></option> <option><optional>filter</optional></option>

View File

@ -31,9 +31,11 @@ warnings.filterwarnings("ignore", "The python-lxc API isn't yet stable")
import argparse import argparse
import gettext import gettext
import json
import lxc import lxc
import os import os
import re import re
import subprocess
import sys import sys
_ = gettext.gettext _ = gettext.gettext
@ -85,6 +87,23 @@ def getTerminalSize():
return int(cr[1]), int(cr[0]) return int(cr[1]), int(cr[0])
def getSubContainers(container, lxcpath):
attach = ['lxc-attach', '-R', '-s', 'NETWORK|PID', '-n', container,
'--', sys.argv[0], "--nesting"]
with open(os.devnull, "w") as fd:
newenv = dict(os.environ)
newenv['NESTED'] = "/proc/1/root/%s" % lxcpath
sp = subprocess.Popen(attach, stderr=fd, stdout=subprocess.PIPE,
env=newenv, universal_newlines=True)
sp.wait()
out = sp.stdout.read()
if out:
return json.loads(out)
return None
# Begin parsing the command line # Begin parsing the command line
parser = argparse.ArgumentParser(description=_("LXC: List containers"), parser = argparse.ArgumentParser(description=_("LXC: List containers"),
formatter_class=argparse.RawTextHelpFormatter) formatter_class=argparse.RawTextHelpFormatter)
@ -93,7 +112,8 @@ parser.add_argument("-1", dest="one", action="store_true",
help=_("list one container per line (default when piped)")) help=_("list one container per line (default when piped)"))
parser.add_argument("-P", "--lxcpath", dest="lxcpath", metavar="PATH", parser.add_argument("-P", "--lxcpath", dest="lxcpath", metavar="PATH",
help=_("Use specified container path"), default=None) help=_("Use specified container path"),
default=lxc.default_config_path)
parser.add_argument("--active", action="store_true", parser.add_argument("--active", action="store_true",
help=_("list only active containers " help=_("list only active containers "
@ -114,6 +134,9 @@ parser.add_argument("--fancy", action="store_true",
parser.add_argument("--fancy-format", type=str, default="name,state,ipv4,ipv6", parser.add_argument("--fancy-format", type=str, default="name,state,ipv4,ipv6",
help=_("comma separated list of fields to show")) help=_("comma separated list of fields to show"))
parser.add_argument("--nesting", dest="nesting", action="store_true",
help=_("show nested containers"))
parser.add_argument("filter", metavar='FILTER', type=str, nargs="?", parser.add_argument("filter", metavar='FILTER', type=str, nargs="?",
help=_("regexp to be applied on the container list")) help=_("regexp to be applied on the container list"))
@ -129,6 +152,9 @@ if args.active:
if not sys.stdout.isatty(): if not sys.stdout.isatty():
args.one = True args.one = True
# Set the lookup path for the containers
lxcpath = os.environ.get('NESTED', args.lxcpath)
# Turn args.fancy_format into a list # Turn args.fancy_format into a list
args.fancy_format = args.fancy_format.strip().split(",") args.fancy_format = args.fancy_format.strip().split(",")
@ -141,7 +167,7 @@ if not os.geteuid() == 0 and (args.fancy or args.state):
# List of containers, stored as dictionaries # List of containers, stored as dictionaries
containers = [] containers = []
for container_name in lxc.list_containers(config_path=args.lxcpath): for container_name in lxc.list_containers(config_path=lxcpath):
entry = {} entry = {}
entry['name'] = container_name entry['name'] = container_name
@ -150,7 +176,7 @@ for container_name in lxc.list_containers(config_path=args.lxcpath):
continue continue
# Return before grabbing the object (non-root) # Return before grabbing the object (non-root)
if not args.state and not args.fancy: if not args.state and not args.fancy and not args.nesting:
containers.append(entry) containers.append(entry)
continue continue
@ -161,28 +187,47 @@ for container_name in lxc.list_containers(config_path=args.lxcpath):
continue continue
# Nothing more is needed if we're not printing some fancy output # Nothing more is needed if we're not printing some fancy output
if not args.fancy: if not args.fancy and not args.nesting:
containers.append(entry) containers.append(entry)
continue continue
# Some extra field we may want # Some extra field we may want
if 'state' in args.fancy_format: if 'state' in args.fancy_format or args.nesting:
entry['state'] = container.state entry['state'] = container.state
if 'pid' in args.fancy_format:
if 'pid' in args.fancy_format or args.nesting:
entry['pid'] = "-" entry['pid'] = "-"
if container.init_pid != -1: if container.init_pid != -1:
entry['pid'] = str(container.init_pid) entry['pid'] = str(container.init_pid)
# Get the IPs # Get the IPs
for protocol in ('ipv4', 'ipv6'): for protocol in ('ipv4', 'ipv6'):
if protocol in args.fancy_format: if protocol in args.fancy_format or args.nesting:
entry[protocol] = "-" entry[protocol] = "-"
ips = container.get_ips(protocol=protocol, timeout=1) ips = container.get_ips(protocol=protocol, timeout=1)
if ips: if ips:
entry[protocol] = ", ".join(ips) entry[protocol] = ", ".join(ips)
# Append the container
containers.append(entry) containers.append(entry)
# Nested containers
if args.nesting:
sub = getSubContainers(container_name, args.lxcpath)
if sub:
for entry in sub:
if 'nesting_parent' not in entry:
entry['nesting_parent'] = []
entry['nesting_parent'].insert(0, container_name)
entry['nesting_real_name'] = entry.get('nesting_real_name',
entry['name'])
entry['name'] = "%s/%s" % (container_name, entry['name'])
containers += sub
# Deal with json output:
if 'NESTED' in os.environ:
print(json.dumps(containers))
sys.exit(0)
# Print the list # Print the list
## Standard list with one entry per line ## Standard list with one entry per line
@ -226,7 +271,12 @@ if args.fancy:
for container in containers: for container in containers:
for field in args.fancy_format: for field in args.fancy_format:
if len(container[field]) > field_maxlength[field]: if field == 'name' and 'nesting_real_name' in container:
fieldlen = len(" " * ((len(container['nesting_parent']) - 1)
* 4) + " \_ " + container['nesting_real_name'])
if fieldlen > field_maxlength[field]:
field_maxlength[field] = fieldlen
elif len(container[field]) > field_maxlength[field]:
field_maxlength[field] = len(container[field]) field_maxlength[field] = len(container[field])
# Generate the line format string based on the maximum length and # Generate the line format string based on the maximum length and
@ -250,5 +300,12 @@ if args.fancy:
# Print the entries # Print the entries
for container in sorted(containers, for container in sorted(containers,
key=lambda container: container['name']): key=lambda container: container['name']):
fields = [container[field] for field in args.fancy_format] fields = []
for field in args.fancy_format:
if field == 'name' and 'nesting_real_name' in container:
prefix = " " * ((len(container['nesting_parent']) - 1) * 4)
fields.append(prefix + " \_ " + container['nesting_real_name'])
else:
fields.append(container[field])
print(line_format.format(fields=fields)) print(line_format.format(fields=fields))

View File

@ -337,18 +337,9 @@ class Container(_lxc.Container):
Returns the list of IP addresses for the container. Returns the list of IP addresses for the container.
""" """
if not self.defined or not self.running: if not self.running:
return False return False
try:
os.makedirs("/run/netns")
except:
pass
path = tempfile.mktemp(dir="/run/netns")
os.symlink("/proc/%s/ns/net" % self.init_pid, path)
ips = [] ips = []
count = 0 count = 0
@ -356,7 +347,8 @@ class Container(_lxc.Container):
if count != 0: if count != 0:
time.sleep(1) time.sleep(1)
base_cmd = ["ip", "netns", "exec", path.split("/")[-1], "ip"] base_cmd = ["lxc-attach", "-s", "NETWORK", "-n", self.name, "--",
"ip"]
# Get IPv6 # Get IPv6
if protocol in ("ipv6", None): if protocol in ("ipv6", None):
@ -397,7 +389,6 @@ class Container(_lxc.Container):
count += 1 count += 1
os.remove(path)
return ips return ips
def get_keys(self, key=None): def get_keys(self, key=None):