lxc-ls: Fix support of --nesting for unpriv

This reworks the way lxc-ls works in nesting mode. In the past it'd use
attach_wait's subprocess function to call itself in the container's
namespace, carefully only attaching to the namespaces it needed.

This works great for system containers but not so much as soon as you
also need to attach to userns. Instead this fix moves all of the
container listing code into a get_containers function (hence the massive
diff, sorry), this function is then called recursively.

For running containers, the function is called through attach_wait
inside the container's namespace, for stopped container, the function is
simply called recursively with a base path (container's rootfs) in an
attempt to find containers that way.
Communication between the parent lxc-ls and the child lxc-ls is done
through a temporary fd and serialized state using json (similar to what
was done using stdout in the previous implementation).

As get_global_config_item unfortunately caches the values, there's no
easy way to figure out what the lxcpath should be for a root container
when running as non-root, so just use @LXCPATH@ for now and have
python do the parsing itself.

As a result, the following things now work as expected:
 - listing nested unprivileged containers (root containers inside unpriv)
 - listing nested containers when they're not running
 - filtering containers in nesting mode (only the first level is filtered)
 - copy with invalid config (used to traceback)

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 2014-03-04 13:20:10 -05:00
parent 891c180ab1
commit 5674a5bf39
2 changed files with 201 additions and 149 deletions

View File

@ -696,6 +696,7 @@ AC_CONFIG_FILES([
src/Makefile src/Makefile
src/lxc/Makefile src/lxc/Makefile
src/lxc/lxc-checkconfig src/lxc/lxc-checkconfig
src/lxc/lxc-ls
src/lxc/lxc-start-ephemeral src/lxc/lxc-start-ephemeral
src/lxc/legacy/lxc-ls src/lxc/legacy/lxc-ls
src/lxc/lxc.functions src/lxc/lxc.functions

View File

@ -31,12 +31,17 @@ import json
import lxc import lxc
import os import os
import re import re
import shutil
import tempfile import tempfile
import sys import sys
_ = gettext.gettext _ = gettext.gettext
gettext.textdomain("lxc-ls") gettext.textdomain("lxc-ls")
# Constants
LXCPATH = "@LXCPATH@"
RUNTIME_PATH = "@RUNTIME_PATH@"
# Functions used later on # Functions used later on
def batch(iterable, cols=1): def batch(iterable, cols=1):
@ -54,7 +59,7 @@ def batch(iterable, cols=1):
yield fields yield fields
def getTerminalSize(): def get_terminal_size():
import os import os
env = os.environ env = os.environ
@ -84,27 +89,17 @@ def getTerminalSize():
return int(cr[1]), int(cr[0]) return int(cr[1]), int(cr[0])
def getSubContainers(container): def get_root_path(path):
with open(os.devnull, "w") as fd: lxc_path = LXCPATH
fdnum, path = tempfile.mkstemp() global_conf = "%s/etc/lxc/lxc.conf" % path
os.remove(path) if os.path.exists(global_conf):
with open(global_conf, "r") as fd:
for line in fd:
if line.startswith("lxc.lxcpath"):
lxc_path = line.split("=")[-1].strip()
break
return lxc_path
fd = os.fdopen(fdnum)
container.attach_wait(
lxc.attach_run_command, [sys.argv[0], "--nesting"],
attach_flags=(lxc.LXC_ATTACH_REMOUNT_PROC_SYS),
namespaces=(lxc.CLONE_NEWNET + lxc.CLONE_NEWPID),
extra_env_vars=["NESTED=/proc/1/root/%s" %
lxc.default_config_path],
stdout=fd)
fd.seek(0)
out = fd.read()
fd.close()
if out:
return json.loads(out)
return None
# Constants # Constants
FIELDS = ("name", "state", "ipv4", "ipv6", "autostart", "pid", FIELDS = ("name", "state", "ipv4", "ipv6", "autostart", "pid",
@ -158,11 +153,6 @@ 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
# This value will contain the full path for a nested containers
# use args.lxcpath if you need the value relative to the container
nest_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(",")
@ -193,141 +183,202 @@ if args.nesting:
parser.error(_("Showing nested containers requires setns to the " parser.error(_("Showing nested containers requires setns to the "
"PID namespace which your kernel doesn't support.")) "PID namespace which your kernel doesn't support."))
# Set the actual lxcpath value
if not args.lxcpath:
args.lxcpath = lxc.default_config_path
# List of containers, stored as dictionaries # List of containers, stored as dictionaries
containers = [] def get_containers(fd=None, base="/", root=False):
for container_name in lxc.list_containers(config_path=nest_lxcpath): containers = []
entry = {}
entry['name'] = container_name
# Apply filter paths = [args.lxcpath]
if args.filter and not re.match(args.filter, container_name):
continue
# Return before grabbing the object (non-root) if not root:
if not args.state and not args.fancy and not args.nesting: paths.append(get_root_path(base))
containers.append(entry)
continue
container = lxc.Container(container_name, args.lxcpath) # Generate a unique list of valid paths
paths = set([os.path.normpath("%s/%s" % (base, path)) for path in paths])
if 'NESTED' in os.environ: for path in paths:
container.load_config(os.path.join(nest_lxcpath, container_name, if not os.access(path, os.R_OK):
"config")) continue
if container.controllable: for container_name in lxc.list_containers(config_path=path):
state = container.state entry = {}
else: entry['name'] = container_name
state = 'UNKNOWN'
# Filter by status # Apply filter
if args.state and state not in args.state: if root and args.filter and \
continue not re.match(args.filter, container_name):
# Nothing more is needed if we're not printing some fancy output
if not args.fancy and not args.nesting:
containers.append(entry)
continue
# Some extra field we may want
if 'state' in args.fancy_format or args.nesting:
entry['state'] = state
if 'pid' in args.fancy_format or args.nesting:
entry['pid'] = "-"
if state == 'UNKNOWN':
entry['pid'] = state
elif container.init_pid != -1:
entry['pid'] = str(container.init_pid)
if 'autostart' in args.fancy_format or args.nesting:
entry['autostart'] = "NO"
try:
if container.get_config_item("lxc.start.auto") == "1":
entry['autostart'] = "YES"
groups = container.get_config_item("lxc.group")
if len(groups) > 0:
entry['autostart'] = "YES (%s)" % ", ".join(groups)
except KeyError:
pass
if 'memory' in args.fancy_format or \
'ram' in args.fancy_format or \
'swap' in args.fancy_format:
if container.running:
try:
memory_total = int(container.get_cgroup_item(
"memory.usage_in_bytes"))
except:
memory_total = 0
try:
memory_swap = int(container.get_cgroup_item(
"memory.memsw.usage_in_bytes"))
except:
memory_swap = 0
else:
memory_total = 0
memory_swap = 0
if 'memory' in args.fancy_format:
if container.running:
entry['memory'] = "%sMB" % round(memory_total / 1048576, 2)
else:
entry['memory'] = "-"
if 'ram' in args.fancy_format:
if container.running:
entry['ram'] = "%sMB" % round(
(memory_total - memory_swap) / 1048576, 2)
else:
entry['ram'] = "-"
if 'swap' in args.fancy_format:
if container.running:
entry['swap'] = "%sMB" % round(memory_swap / 1048576, 2)
else:
entry['swap'] = "-"
# Get the IPs
for family, protocol in {'inet': 'ipv4', 'inet6': 'ipv6'}.items():
if protocol in args.fancy_format or args.nesting:
entry[protocol] = "-"
if state == 'UNKNOWN':
entry[protocol] = state
continue continue
if container.running: # Return before grabbing the object (non-root)
if not SUPPORT_SETNS_NET: if not args.state and not args.fancy and not args.nesting:
entry[protocol] = 'UNKNOWN' containers.append(entry)
continue continue
ips = container.get_ips(family=family) try:
if ips: container = lxc.Container(container_name, path)
entry[protocol] = ", ".join(ips) except:
continue
# Append the container if container.controllable:
containers.append(entry) state = container.state
else:
state = 'UNKNOWN'
# Nested containers # Filter by status
if args.nesting and container.state == "RUNNING": if args.state and state not in args.state:
sub = getSubContainers(container) continue
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: # Nothing more is needed if we're not printing some fancy output
if 'NESTED' in os.environ: if not args.fancy and not args.nesting:
print(json.dumps(containers)) containers.append(entry)
sys.exit(0) continue
# Some extra field we may want
if 'state' in args.fancy_format or args.nesting:
entry['state'] = state
if 'pid' in args.fancy_format or args.nesting:
entry['pid'] = "-"
if state == 'UNKNOWN':
entry['pid'] = state
elif container.init_pid != -1:
entry['pid'] = str(container.init_pid)
if 'autostart' in args.fancy_format or args.nesting:
entry['autostart'] = "NO"
try:
if container.get_config_item("lxc.start.auto") == "1":
entry['autostart'] = "YES"
groups = container.get_config_item("lxc.group")
if len(groups) > 0:
entry['autostart'] = "YES (%s)" % ", ".join(groups)
except KeyError:
pass
if 'memory' in args.fancy_format or \
'ram' in args.fancy_format or \
'swap' in args.fancy_format:
if container.running:
try:
memory_total = int(container.get_cgroup_item(
"memory.usage_in_bytes"))
except:
memory_total = 0
try:
memory_swap = int(container.get_cgroup_item(
"memory.memsw.usage_in_bytes"))
except:
memory_swap = 0
else:
memory_total = 0
memory_swap = 0
if 'memory' in args.fancy_format:
if container.running:
entry['memory'] = "%sMB" % round(memory_total / 1048576, 2)
else:
entry['memory'] = "-"
if 'ram' in args.fancy_format:
if container.running:
entry['ram'] = "%sMB" % round(
(memory_total - memory_swap) / 1048576, 2)
else:
entry['ram'] = "-"
if 'swap' in args.fancy_format:
if container.running:
entry['swap'] = "%sMB" % round(memory_swap / 1048576, 2)
else:
entry['swap'] = "-"
# Get the IPs
for family, protocol in {'inet': 'ipv4', 'inet6': 'ipv6'}.items():
if protocol in args.fancy_format or args.nesting:
entry[protocol] = "-"
if state == 'UNKNOWN':
entry[protocol] = state
continue
if container.running:
if not SUPPORT_SETNS_NET:
entry[protocol] = 'UNKNOWN'
continue
ips = container.get_ips(family=family)
if ips:
entry[protocol] = ", ".join(ips)
# Nested containers
if args.nesting:
if container.running:
# Recursive call in container namespace
temp_fd, temp_file = tempfile.mkstemp()
os.remove(temp_file)
container.attach_wait(get_containers, temp_fd,
attach_flags=0)
json_file = os.fdopen(temp_fd, "r")
json_file.seek(0)
try:
sub_containers = json.loads(json_file.read())
except:
sub_containers = []
json_file.close()
else:
def clear_lock():
try:
lock_path = "%s/lock/lxc/%s/%s" % (RUNTIME_PATH,
path,
entry['name'])
if os.path.exists(lock_path):
if os.path.isdir(lock_path):
shutil.rmtree(lock_path)
else:
os.remove(lock_path)
except:
pass
clear_lock()
# Recursive call using container rootfs
sub_containers = get_containers(
base="%s/%s" % (
base, container.get_config_item("lxc.rootfs")))
clear_lock()
for sub in sub_containers:
if 'nesting_parent' not in sub:
sub['nesting_parent'] = []
sub['nesting_parent'].insert(0, entry['name'])
sub['nesting_real_name'] = sub.get('nesting_real_name',
sub['name'])
sub['name'] = "%s/%s" % (entry['name'], sub['name'])
containers.append(sub)
# Append the container
containers.append(entry)
if fd:
json_file = os.fdopen(fd, "w+")
json_file.write(json.dumps(containers))
return
return containers
containers = get_containers(root=True)
# Print the list # Print the list
## Standard list with one entry per line ## Standard list with one entry per line
@ -348,7 +399,7 @@ if not args.fancy and not args.one:
container_names.append(container['name']) container_names.append(container['name'])
# Figure out how many we can put per line # Figure out how many we can put per line
width = getTerminalSize()[0] width = get_terminal_size()[0]
entries = int(width / (field_maxlength + 2)) entries = int(width / (field_maxlength + 2))
if entries == 0: if entries == 0: