7

I have an Ubuntu 14.04 server installation which acts as an LXC host. It has two users: user1 and user2.

user1 owns an unprivileged LXC container, which uses a directory (inside /home/user1/.local/...) as backing store.

How do I make a full copy of the container for user2? I can't just copy the files because they are mapped with owners ranging from 100000 to 100000+something, which are bound to user1.

Also, which I believe is basically the same question, how can I safely make a backup of my user1's LXC container to restore it later on another machine and/or user?

1
  • use cp instead of mv with the appropriate flags to preserve all the properties you need to preserve in my script. Dec 22, 2014 at 2:21

3 Answers 3

2

I know now how to do this. If you can't follow this explanation, please ask back, but also make sure you have read up on userns in the readings I am giving at the bottom

Preliminary assumptions

I'll stick with the following assumptions, extended from what I have from your question:

  1. host has a user1 and a user2, if an information isn't specific to one, we'll use userX
  2. the container will be named by a variable which we will render as $container
  3. home folders for user1 and user2 will be given in the notation known from Bash as ~user1 and ~user2.
  4. we'll assume the subordinate UID and GID ranges to be 100000..165536 for user1 and 200000..265536 for user2 just for brevity
  5. the root FS folder for $container will be rendered as $rootfs, regardless of where it will end up (~userX/.local/share/lxc/$container/rootfs)
  6. container configuration is by default in ~userX/.local/share/lxc/$container/config

Moving the container

There are two relevant pieces of data that govern the userns containers:

  1. owner and group for the files/folders of the folders comprising the $container
  2. the subordinate UIDs and GIDs assigned in two places: /etc/sub{uid,gid} for the user account (manipulated via usermod --{add,del}-sub-{uid,gid}s) and lxc.id_map in the $container configuration (~userX/.local/share/lxc/$container/config) respectively
    • I don't know for certain whether it is possible to define different ranges in the container configuration for each container. E.g. if the host user userX has 65536 subordinate GIDs and UIDs, it might be possible to assign 5000 to 65 different containers, but I haven't tested that hypothesis.
    • it is certain, though, that this setting communicates to LXC which are the valid ranges for GID and UID in the child namespace.

So the gist is really that you need to make sure that the file/folder owner and group for the container match the configuration, which in turn has to be a valid subset of the host subordinate GIDs/UIDs assigned to user1 and user2 respectively.

If you're using Bash, for example, you can use $((expression)) for arithmetic expressions and let to assign arithmetic expressions to variables. This is mighty useful if you know a base value (100000 and 200000 respectively) and the GID/UID for the "inside" users.

The main points are:

  1. it's possible
  2. either the capability CAP_CHOWN or superuser rights are required

Here's a script that will probably need some more honing (example: migration from root-created container to unprivileged), but it works for me for the purpose:

#!/usr/bin/env bash

function syntax
{
    echo "SYNTAX: ${0##*/} <from-user> <to-user> <container-name>"
    [[ -n "$1" ]] && echo -e "\nERROR: ${1}."
    exit 1
}

# Checks
[[ -n "$1" ]] || syntax "<from-user> is not set"
[[ -n "$2" ]] || syntax "<to-user> is not set"
[[ -n "$3" ]] || syntax "<container-name> is not set"
[[ "$UID" -eq "0" ]] || syntax "${0##*/}" "You must be superuser to make use of this script"
# Constants with stuff we need
readonly USERFROM=$1
readonly USERTO=$2
shift; shift
readonly CONTAINER=${1:-*}
LXCLOCAL=".local/share/lxc"
readonly HOMEFROM=$(eval echo ~$USERFROM)
readonly HOMETO=$(eval echo ~$USERTO)
readonly LXCFROM="$HOMEFROM/$LXCLOCAL"
readonly LXCTO="$HOMETO/$LXCLOCAL"
readonly GIDBASEFROM=$(awk -F : "\$1 ~/$USERFROM/ {print \$2}" /etc/subgid)
readonly UIDBASEFROM=$(awk -F : "\$1 ~/$USERFROM/ {print \$2}" /etc/subuid)
readonly GIDSIZEFROM=$(awk -F : "\$1 ~/$USERFROM/ {print \$3}" /etc/subgid)
readonly UIDSIZEFROM=$(awk -F : "\$1 ~/$USERFROM/ {print \$3}" /etc/subuid)
readonly GIDBASETO=$(awk -F : "\$1 ~/$USERTO/ {print \$2}" /etc/subgid)
readonly UIDBASETO=$(awk -F : "\$1 ~/$USERTO/ {print \$2}" /etc/subuid)
readonly GIDSIZETO=$(awk -F : "\$1 ~/$USERTO/ {print \$3}" /etc/subgid)
readonly UIDSIZETO=$(awk -F : "\$1 ~/$USERTO/ {print \$3}" /etc/subuid)
unset LXCLOCAL
# More checks
[[ -d "$LXCFROM" ]] || syntax "Could not locate '$LXCFROM'. It is not a directory as expected"
[[ -e "$LXCTO" ]] && syntax "Destination '$LXCTO' already exists. However, it must not"
for i in GIDBASEFROM UIDBASEFROM GIDBASETO UIDBASETO; do
    (($i > 0)) || syntax "Could not determine base/offset of subordinate UID/GID range"
done
for i in GIDSIZEFROM UIDSIZEFROM GIDSIZETO UIDSIZETO; do
    (($i > 0)) || syntax "Could not determine length of subordinate UID/GID range"
done

echo "Going to migrate container: $CONTAINER"
echo -e "\tfrom user $USERFROM ($HOMEFROM): subUID=${UIDBASEFROM}..$((UIDBASEFROM+UIDSIZEFROM)); subGID=${GIDBASEFROM}..$((GIDBASEFROM+GIDSIZEFROM))"
echo -e "\tto user $USERTO ($HOMETO): subUID=${UIDBASETO}..$((UIDBASETO+UIDSIZETO)); subGID=${GIDBASETO}..$((GIDBASETO+GIDSIZETO))"
while read -p "Do you want to continue? (y/N) "; do
    case ${REPLY:0:1} in
        y|Y)
            break;
            ;;
        *)
            echo "User asked to abort."
            exit 1
            ;;
    esac
done

# Find the UIDs and GIDs in use in the container
readonly SUBGIDSFROM=$(find -H "$LXCFROM" -printf '%G\n'|sort -u)
readonly SUBUIDSFROM=$(find -H "$LXCFROM" -printf '%U\n'|sort -u)

# Change group
for gid in $SUBGIDSFROM; do
    let GIDTO=$(id -g "$USERTO")
    if ((gid == $(id -g "$USERFROM"))); then
        echo "Changing group from $USERFROM ($gid) to $USERTO ($GIDTO)"
        find -H "$LXCFROM/$CONTAINER" -gid $gid -exec chgrp $GIDTO {} +
    elif ((gid >= GIDBASEFROM )) && ((gid <= GIDBASEFROM+GIDSIZEFROM)); then
        let GIDTO=$((gid-GIDBASEFROM+GIDBASETO))
        echo "Changing group $gid -> $GIDTO"
        find -H "$LXCFROM/$CONTAINER" -gid $gid -exec chgrp $GIDTO {} +
    else
        echo "ERROR: Some file/folder inside '$LXCFROM/$CONTAINER' has a group not assigned to $USERFROM (assigned subordinate GIDs)."
        echo -e "Use:\n\tfind -H '$LXCFROM/$CONTAINER' -gid $gid\nto list those files/folders."
        exit 1
    fi
done

# Change owner
for uid in $SUBUIDSFROM; do
    let UIDTO=$(id -u "$USERTO")
    if ((uid == $(id -u "$USERFROM"))); then
        echo "Changing owner from $USERFROM ($uid) to $USERTO ($UIDTO)"
        find -H "$LXCFROM/$CONTAINER" -uid $uid -exec chown $UIDTO {} +
    elif ((uid >= UIDBASEFROM )) && ((uid <= UIDBASEFROM+UIDSIZEFROM)); then
        let UIDTO=$((uid-UIDBASEFROM+UIDBASETO))
        echo "Changing owner $uid -> $UIDTO"
        find -H "$LXCFROM/$CONTAINER" -uid $uid -exec chown $UIDTO {} +
    else
        echo "ERROR: Some file/folder inside '$LXCFROM/$CONTAINER' has an owner not assigned to $USERFROM (assigned subordinate UIDs)."
        echo -e "Use:\n\tfind -H '$LXCFROM/$CONTAINER' -uid $uid\nto list those files/folders."
        exit 1
    fi
done
mv "$LXCFROM/$CONTAINER" "$LXCTO/" || { echo "ERROR: failed to move to destination: ${LXCTO}/${CONTAINER}."; exit 1; }

In addition to the license terms of the StackExchange network, I am putting this into the public domain. So reuse and modify for whatever purpose, but it comes without any warranty and I must not be held liable for its use or abuse.

Usage
SYNTAX: lxc-reassign-userns.sh <from-user> <to-user> <container-name>

It assumes find, sort, uniq, awk (mawk and gawk should work), id, bash, chown, chmod and so on to be available and to understand all the command line switches it is using. For Bash readonly and let and arithmetic expressions are assumed to be understood. For find is assumes + is a valid terminator for the -exec action.

This list is probably not complete.

Backups

Yes, you can make backups and restore them elsewhere, as long as you also adjust the file owner and group accordingly.

However, assuming you use something like tar, there's a caveat: tar will ignore sockets, so $rootfs/dev/log will pose an issue - others may also create a similar issue.

Resources:

2
  • Honestly, I was hoping for a built-in, ready-to-use command. It's strange to me that the lxc package doesn't ship one. However, thanks a lot for the time and effort you put into writing such a detailed answer, and for sharing your code. I will test it, then I will give you feedback.
    – agdev84
    Jan 7, 2015 at 11:19
  • @agdev84: I am currently rewriting it a bit in order to also allow moving it from a privileged to an unprivileged userns based container. Might end up doing it in Python or so, though. Jan 7, 2015 at 11:46
2

fuidshift has been created for this purpose. It seems to be part of LXD.

http://manpages.ubuntu.com/manpages/xenial/man1/fuidshift.1.html

2

Edit: fuidshift is the best way to do it. In Ubuntu, since LXD was converted from a DEB package to a snap, fuidshift is not shipped anymore and this is also not going to happen. You need to compile fuidshift by yourself.

However, this can be done quite easily (download of the source code and compilation is done almost automatically), see https://github.com/lxc/lxc/issues/3186

--

You could just copy the directory containing the LXC container from user1 to user2 and use this python code to shift UIDs and GIDs:

#!/usr/bin/python3

import os
import sys

uidmap_start = 100000
uidmap_size = 65536

gidmap_start = 100000
gidmap_size = 65536


def changeUidGidRecursive(path):
  changeUidGid(path)
  if os.path.isdir(path) and not os.path.islink(path):
    for filename in os.listdir(path):
      sub_path = os.path.join(path, filename)
      changeUidGidRecursive(sub_path)

def changeUidGid(path):
  stat_info = os.lstat(path)
  uid = stat_info.st_uid
  gid = stat_info.st_gid
  new_uid = uid + uidmap_start
  new_gid = gid + gidmap_start
  if (new_uid > uidmap_end):
    print("Info: New UID %d for \"%s\" would be out of range. Not changing UID." % (new_uid, path))
    new_uid = uid
  if (new_gid > gidmap_end):
    print("Info: New GID %d for \"%s\" would be out of range. Not changing GID." % (new_gid, path))
    new_gid = gid
  if (new_uid != uid or new_gid != gid):
    mode = stat_info.st_mode
    os.chown(path, new_uid, new_gid, follow_symlinks=False)
    new_mode = os.lstat(path).st_mode
    # If necessary, restore old mode
    if (new_mode != mode):
      os.chmod(path, mode)

if __name__ == '__main__':
  uidmap_end = uidmap_start + uidmap_size
  gidmap_end = gidmap_start + gidmap_size

  base_path = ''
  if len(sys.argv) > 1:
    base_path = sys.argv[1]
  else:
    print("Usage: %s <path>" % (sys.argv[0]))
    sys.exit(1)

  if not os.path.exists(base_path):
    print("Error: Path \"%s\" does not exist" % (base_path))
    print("Exiting")
    sys.exit(1)
  changeUidGidRecursive(base_path)
  sys.exit(0)

You will need to adapt uidmap_start, gidmap_size and possibly also uidmap_size and gidmap_size to your needs.

I used this python code to migrate privileged LXC containers to unprivileged ones. The python code runs faster than a shell script.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .