在 OpenWrt 上玩转 LXC 容器

文章更新

20190625 初次成文

写在前面

LXC (LinuX Contains) 是操作系统级别的虚拟化技术,它可以提供轻量级的虚拟化、以便隔离进程和资源。容器有效地将操作系统管理的资源划分到独立的组中,并把各个独立的组进行隔离,可以让各自的组占用独立的资源,完成自己独立的任务。

LXC 容器已经成为 OpenWrt 项目的一部分,简单来说,LXC 允许你在 OpenWrt 中运行多个不同的系统,比如 Ubuntu、CentOS、Alpine Linux 等。

如此一来,很多 OpenWrt 上跑不起来或者暂时未适配的应用现在就都能跑啦~

LXC 的 Luci APP 源码托管在:

https://github.com/openwrt/luci/tree/master/applications/luci-app-lxc

在小苏的“自编译 OpenWrt 固件”中也加入了 LXC 及其 Luci APP 的支持,但是在国内范围内介绍 OpenWrt LXC 特性的文章不是很多,而 LXC 自身又是极其强大的一个东西,所以值得 鸽子王 小苏特地写一篇文章来介绍~

以下小苏以自己编译的适用于树莓派的 OpenWrt 固件为例:

分区新建及格式化

在使用 LXC 容器之前,我们需要做一些准备工作。因为 LXC 容器的建立过程中需要从网络上下载模板资源(包含容器自身的 RootFS 以及一些配置文件)并解压到机身存储中,不论是在容器的建立还是容器的运行过程中都会占用大量存储空间(几十至数百兆)。而运行 OpenWrt 的设备的存储根分区往往不足以承载这些“大文件”,怎么办呢?最简单的方法就是将 U 盘插入运行 OpenWrt 的设备,将 LXC 容器涉及到的文件“转移到 U 盘中”。在这篇文章中,小苏使用以 SD 卡为存储介质的树莓派做演示,因为 SD 卡的总容量完全满足 LXC 对于存储空间的要求,所以不必使用 U 盘来“转移文件”。

默认情况下,写入小苏编译的 OpenWrt 固件的 SD 卡除了 50M 的引导分区和 500M 的根分区之外,剩余存储空间皆为“空闲空间”。

Partition Guru 分区详情

虽然总空间很大,但因为小苏编译的 OpenWrt 固件根分区只有 500M ,不满足运行 LXC 容器的需求,所以小苏需要将 SD 卡的“空闲空间”新建一个分区,接着把新建好的分区挂载到 LXC 容器所在目录,来他个“狸猫换太子”~

首先我们使用 fdisk命令查看 SD 卡目前的分区状况:

root@OpenWrt:~# fdisk -l /dev/mmcblk0
Disk /dev/mmcblk0: 14.9 GiB, 15931539456 bytes, 31116288 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x5452574f

Device Boot Start End Sectors Size Id Type
/dev/mmcblk0p1 * 8192 110591 102400 50M c W95 FAT32 (LBA)
/dev/mmcblk0p2 114688 1138687 1024000 500M 83 Linux

分区情况与我们刚才在 Partition Guru 中看到的结果一致。另外我们需要记住设备的最后一个分区的终止扇区,在上面 fdisk返回的结果中,我们注意到最后一个分区是 mmcblk0p2,这个分区的终止扇区是 1138687。

接下来我们把 SD 卡的“空闲空间”利用起来,用 fdisk 命令在这部分空间上新建一个分区:

root@OpenWrt:~# fdisk /dev/mmcblk0 # 【进入 fdisk 分区工具】
Welcome to fdisk (util-linux 2.33).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Command (m for help): n # 【输入 n 新建分区】
Partition type
p primary (2 primary, 0 extended, 2 free)
e extended (container for logical partitions)


Select (default p): p # 【输入 p 选择新建主分区】
Partition number (3,4, default 3): 3 #【输入 3 选择新建分区号为3的主分区】
First sector (2048-31116287, default 2048): 1140000 # 【下文会讲到】
Last sector, +/-sectors or +/-size{K,M,G,T,P} (1140000-31116287, default 31116287): # 【直接按回车】

Created a new partition 3 of type 'Linux' and of size 14.3 GiB.

Command (m for help): p # 【输入 p 打印当前分区信息,效果等同于 fdisk -l 命令】
Disk /dev/mmcblk0: 14.9 GiB, 15931539456 bytes, 31116288 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x5452574f

Device Boot Start End Sectors Size Id Type
/dev/mmcblk0p1 * 8192 110591 102400 50M c W95 FAT32 (LBA)
/dev/mmcblk0p2 114688 1138687 1024000 500M 83 Linux
/dev/mmcblk0p3 1140000 31116287 29976288 14.3G 83 Linux

Command (m for help): w # 【提交对分区表的修改】
The partition table has been altered.
Syncing disks.

在这一步中:

First sector (2048-31116287, default 2048): 1140000 

小苏输入了 1140000,这个数值需要根据之前最后一个分区的终止扇区来决定,理论上可以输入大于最后一个分区终止扇区(1138687)而小于存储设备的终止扇区(31116287)之间的任意数值,但是为了节省空间,小苏建议输入一个略大于最后一个分区终止扇区的值,在上文中小苏选择了 1140000,当然这个值是区间内任意的,哪怕输入 1138688 也可以~

接下来我们把新建的分区格式化为 ext4 格式:

root@OpenWrt:~# mkfs.ext4 /dev/mmcblk0p3 # 【使用 mkfs.ext4 命令格式化刚刚新建的分区】
mke2fs 1.44.3 (10-July-2018)
Discarding device blocks: done
Creating filesystem with 3747036 4k blocks and 938400 inodes
Filesystem UUID: e07a04f3-933c-4a37-9f23-ed98de55e1f0
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208

Allocating group tables: done
Writing inode tables: done
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done

如上信息出现时,代表分区格式化成功~

分区挂载及目录设定

接下来我们需要将刚刚格式化好的分区挂载到 LXC 相关目录上,在 OpenWrt 中,与 LXC 相关的目录都在 /srv文件夹下,如果根目录下没有这个文件夹,我们需要先新建(若文件夹已存在可忽略此步)。

mkdir /srv

接着将 SD 卡的第三分区挂载到 /srv下:

mount -v -t ext4 -o rw /dev/mmcblk0p3 /srv

/srv 目录下新建 lxc 文件夹:

mkdir /srv/lxc

至此准备工作完成~

镜像源设定

进入 OpenWrt 的控制面板,在 “服务 - LXC Containers”我们可以进入 LXC 容器的控制界面。之前小苏提到过新建容器过程中需要从网络下载大量数据,为了提高下载速度,我们可以把 “Containers URL” 中的软件源替换成清华大学的 “LXC Images” 源:

mirrors.tuna.tsinghua.edu.cn/lxc-images

更换镜像源

“保存&应用”之后,刷新界面等一两秒,在“Create New Container”处的 “Template”下拉框中便可以加载出可用的 LXC 模板。

容器新建

在“Create New Container”的“共享名”处填入你想要给容器起的名字,在“Template”处选择容器模板,点击右侧的“Create”按钮即可启动容器新建任务,以 Ubuntu Trusty 模板为例:

新建 LXC 容器

稍等片刻,大概 5 分钟左右的时间(视网络而定),在 “Available Containers”即可出现新建完成的容器。

容器新建成功

注意:

不是所有模板创建的容器都可成功运行!

目前测试成功的模板为:

Alpine Linux 全系列

CentOS (有一些小 Bug*)

Devuan ASCII

Ubuntu Trusty

*CentOS 容器启动后可能会遇到网络无法连接的问题,可以参照 这篇文章 解决。

存在问题的模板为:

ArchLinux:无法启动

Debian 全系列:高版本发行版无法启动,低版本发行版无法联网

Ubuntu 高版本(除 Trusty):无法启动

待测试:

Fedora、Gentoo、Kali

网络配置

依次点击“MORE - CONFIGURE”打开容器配置界面,注释此行:

lxc.net.0.type = empty # 注释此行

并且在在末尾添加以下内容:

lxc.net.0.type = veth
lxc.net.0.flags = up
lxc.net.0.link = br-lan
lxc.net.0.hwaddr = 26:06:06:94:e6:5b

# Init
lxc.init.cmd = /sbin/init

最后点击输入框右下角“CONFIRM”按钮提交(按钮不太显眼,可能需要向右拖动指示条)

LXC 容器网络配置

提交后输入框下方会有“LXC configuration updated”的提示。

启动容器

顾名思义,点击容器“状态指示灯”右侧的 “启动按钮”可以启动容器,启动后,指示灯会由红色转为绿色,代表容器启动成功。

接下来我们可以在 SSH 或者 TTYD 终端中输入以下命令进入容器:

lxc-attach -n 容器名称

root@OpenWrt:~# lxc-attach -n Ubuntu
root@Ubuntu:~#

当命令指示符从root@OpenWrt: 变为 root@Ubuntu:时,代表我们成功进入容器。

除了使用 lxc-attach 的方式进入容器外,我们还可以使用 lxc-console的方式进入容器。这种进入方式更接近于实机交互,需要验证用户名和密码才可进入,所以在使用lxc-console前需要事先使用lxc-attach进入容器并设置一个用户密码。

lxc-console -n 容器名称 -t 1

其中,-t 参数后面的数字代表控制台编号,如果 -t 后跟随的数字是 1 ,那么命令执行后将会进入第一个控制台。进入控制台后,按下 Ctrl+a 后再按下 q 即可暂时离开控制台(而不退出),再次进入相同编号的控制台后可以恢复之前的会话,如果想要在终端中输入 Ctrl+a,那么按下两次 Ctrl+a 即可。

root@OpenWrt:~# lxc-console -n Ubuntu -t 1

Connected to tty 1
Type <Ctrl+a q> to exit the console, <Ctrl+a Ctrl+a> to enter Ctrl+a itself

Ubuntu 14.04.6 LTS Ubuntu tty1

Ubuntu login: root
Password:
Welcome to Ubuntu 14.04.6 LTS (GNU/Linux 4.14.127 armv7l)

* Documentation: https://help.ubuntu.com/

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@Ubuntu:~#

启动容器后,我们可以在“状态 - 概况”中看到刚刚创建的容器名称:

容器 IP

这是因为,容器中系统的虚拟网卡可以直接从 OpenWrt 的 DHCP 服务器中获取 IP,也就是说,容器当前的网络状态和一台跑着 Linux 系统并且接入 OpenWrt 所在网络的真机没有什么两样。这样的好处在于,无需在 OpenWrt 与容器间建立复杂的端口映射(Docker:??),在内网下访问容器内的服务十分方便,而即使是在外网,也只需要再做一个简单的端口映射就可以。

其他设置

因为 SD 卡的第三分区是我们手动新建的,并且为了使 LXC 容器正常运行,第三分区需要挂载到指定挂载点,所以为了使 LXC 容器“可持续运行”,我们需要在设备启动时做些文章。

在“系统 - 启动项”本地启动脚本输入框中输入以下内容以实现 SD 卡第三分区开机自动挂载:

# Umount /dev/mmcblk0p3 from system generated mount point
umount /dev/mmcblk0p3

# Mount /dev/mmcblk0p3 to /srv
mount -v -t ext4 -o rw /dev/mmcblk0p3 /srv

同时因为 LXC 容器默认不会在开机时启动,所以我们还可以在开机挂载代码后输入以下内容实现容器开机启动:

lxc-start -n Ubuntu

其中,Ubuntu 为容器名称。

同时应该注意,所有代码应插入在 exit 0之前。

开机启动脚本

参考资料

【LEDE】x86 软路由之路 - 10 - 都能用 Docker 了,LXC 还远吗? - CSDN

https://blog.csdn.net/wang805447391/article/details/83542599

容器技术(一)LXC 安装及使用 - leon 的博客

https://blog.leonshadow.com/763482/774.html