如何制作OpenStack Linux镜像

在上一篇文章构建OpenStack镜像的环境搭建中我们介绍了如何在自己的笔记本或台式机上搭建一个可以用于制作OpenStack镜像的环境,本文我们将利用这个环境,以制作CentOS7.9镜像为例,介绍手动制作OpenStack Linux镜像的详细步骤。

手动制作OpenStack Linux镜像


下载镜像

访问镜像下载地址,选择版本为7.9.2009,在isos目录下载x86_64的Minimal镜像,并上传到/tvm下。

创建虚拟机

首先创建一个raw格式镜像文件,作为虚拟机的系统盘,大小50G。

说明
镜像文件大小可根据实际情况自行调整,本文以50G为例

1
2
cd /tvm/
qemu-img create -f raw CentOS7.9.raw 50G

修改/tvm/templates.xml,在下图红色方框处指定虚拟机镜像文件绝对路径ISO文件绝对路径

启动虚拟机

1
2
cd /tvm/
virsh create templates.xml

启动后,使用VNC Viewer连接到虚拟机控制台

连接地址 VNC密码
宿主机IP:22000 123456

安装OS

安装系统的过程按正常进行,此处就不再赘述,以下是一些安装系统时通用的配置:

  • 系统语言统一为英语English(United States)
  • 时区统一为“Asia/Shanghai”,不使用UTC时间
  • 磁盘选择手动配置分区,分区规划可参考以下方案,分区类型统一为标准分区(Standard Partition),不采用LVM,文件系统为EXT4XFS
    • /boot 500M
    • swap 4096M
    • / 剩余空间
  • 主机名自定义,例如template
  • root用户密码自定义,部分Linux发行版安装时必须创建一个普通用户,root用户默认禁止登录。需安装完成后使用安装系统时创建的用户登录,然后修改root密码并设置允许root登录

完成以上配置后会正式开始安装,最后待安装完成后单击右下角的reboot重启虚拟机(部分Linux发行版会自动重启)

说明
此时你会发现虚拟机重启后又再次进入系统安装引导界面,因此需要先执行以下命令关闭虚拟机

1
2
3
4
5
6
7
[root@koenli tvm]# virsh list
Id Name State
----------------------------------------------------
3 koenli running

[root@koenli tvm]# virsh destroy koenli
Domain koenli destroyed

然后修改/tvm/templates.xml,将文件中指定的ISO文件路径清空

最后再次启动虚拟机,此时就会直接从硬盘中启动系统了。

1
2
cd /tvm/
virsh create templates.xml

配置OS

配置网卡配置文件

配置网卡配置文件ifcfg-eth0ifcfg-eth1,IP为DHCP获取,开机自启动

CentOS6.X/RedHat6.X

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
cat > /etc/sysconfig/network-scripts/ifcfg-eth0 << EOF
DEVICE=eth0
TYPE=Ethernet
ONBOOT=yes
NM_CONTROLLED=yes
BOOTPROTO=dhcp
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=yes
IPV6_ADDR_GEN_MODE=stable-privacy
EOF

cat > /etc/sysconfig/network-scripts/ifcfg-eth1 << EOF
DEVICE=eth1
TYPE=Ethernet
ONBOOT=yes
NM_CONTROLLED=yes
BOOTPROTO=dhcp
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=yes
IPV6_ADDR_GEN_MODE=stable-privacy
EOF

mv /etc/udev/rules.d/{70-persistent-net.rules,70-persistent-net.rules.bak}
ln -s /dev/null /etc/udev/rules.d/70-persistent-net.rules

service NetworkManager stop
chkconfig NetworkManager off
/etc/init.d/network restart

CentOS7.X/RedHat7.X

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
cat > /etc/sysconfig/network-scripts/ifcfg-eth0 << EOF
TYPE=Ethernet
BOOTPROTO=dhcp
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=yes
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=eth0
DEVICE=eth0
ONBOOT=yes
EOF

cat > /etc/sysconfig/network-scripts/ifcfg-eth1 << EOF
TYPE=Ethernet
BOOTPROTO=dhcp
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=yes
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=eth1
DEVICE=eth1
ONBOOT=yes
EOF

systemctl stop NetworkManager
systemctl disable NetworkManager
systemctl restart network

配置SSHD服务

关闭SSHD服务的UseDNSGSSAPIAuthentication选项,防止出现每次登录SSH时总是要停顿等待一下才能连接上的问题

CentOS6.X/RedHat6.X

1
2
3
sed -i 's/#UseDNS yes/UseDNS no/g' /etc/ssh/sshd_config
sed -i 's/GSSAPIAuthentication yes/GSSAPIAuthentication no/g' /etc/ssh/sshd_config
/etc/init.d/sshd restart

CentOS7.X/RedHat7.X

1
2
3
sed -i 's/#UseDNS yes/UseDNS no/g' /etc/ssh/sshd_config
sed -i 's/GSSAPIAuthentication yes/GSSAPIAuthentication no/g' /etc/ssh/sshd_config
systemctl restart sshd

配置防火墙和SELinux

关闭防火墙和SELinux,并设置开机不自启

说明
关闭SELinux需重启生效

CentOS6.X/RedHat6.X

1
2
3
4
5
/etc/init.d/iptables stop
chkconfig iptables off
setenforce 0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
reboot

CentOS7.X/RedHat7.X

1
2
3
4
5
systemctl stop firewalld
systemctl disable firewalld
setenforce 0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
reboot

系统参数优化(可选)

优化系统文件句柄数限制和进程数限制参数

说明
需重启生效

1
2
3
4
5
6
7
cat >> /etc/security/limits.conf << EOF
* soft nproc 655360
* hard nproc 655360
* soft nofile 655360
* hard nofile 655360
EOF
reboot

内核参数优化(可选)

CentOS6.X/RedHat6.X

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cp -a /etc/sysctl.conf /etc/sysctl.conf.bak
cat > /etc/sysctl.conf << EOF
net.ipv4.ip_forward = 1
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.accept_source_route = 0
kernel.sysrq = 0
kernel.core_uses_pid = 1
net.ipv4.tcp_syncookies = 1
kernel.msgmnb = 65536
kernel.msgmax = 65536
kernel.shmmax = 68719476736
kernel.shmall = 4294967296
EOF
sysctl --system

CentOS7.X/RedHat7.X

1
2
3
4
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
echo "net.ipv4.conf.default.rp_filter=0" >> /etc/sysctl.conf
echo "net.ipv4.conf.all.rp_filter=0" >> /etc/sysctl.conf
sysctl --system

配置console log

当操作系统内核崩溃时会报出内核crash错误信息,通常启动的时候一闪而过,而此时系统还没有起来,不能通过远程工具(比如SSH)进入系统查看,我们可以通过配置grub把这些日志重定向到Serial Console中,这样我们就可以通过Serial console来查看错误信息,以供分析和排错使用。

CentOS6.X/RedHat6.X

编辑/boot/grub/grub.conf文件,在kernel /vmlinuz...行末尾添加console=tty0 console=ttyS0,115200参数并保存退出

1
2
3
...
kernel /vmlinuz-...... rhgb quiet console=tty0 console=ttyS0,115200
...

重启系统使配置生效

1
reboot

CentOS7.X/RedHat7.X

编辑/etc/default/grub文件,在GRUB_CMDLINE_LINUX行添加console=tty0 console=ttyS0,115200参数并保存退出

1
2
3
...
GRUB_CMDLINE_LINUX="crashkernel=auto rhgb quiet console=tty0 console=ttyS0,115200"
...

更新GRUB配置并重启系统

1
2
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot

设置history历史命令显示格式(可选)

说明
调整历史命令显示格式,新增执行时间、执行用户的信息,有助于问题溯源

1
2
echo 'HISTTIMEFORMAT="[%Y-%m-%d %H:%M:%S $USER] "' >> /etc/profile
source /etc/profile

上传iso文件配置本地YUM源(可选)

说明
正常使用默认YUM源即可,但是如果镜像要应用于无法访问互联网的云环境,建议在制作镜像时就配置好本地YUM源,防止后面要逐台手动配置

说明
CentOS6默认官方源已下线,建议参考此文档切换到centos-vault源

1
2
3
4
5
cd /etc/yum.repos.d/
mkdir backup
mv * backup/
curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-6.10.repo
yum clean all

本地YUM源配置可参考CentOS6.X/CentOS7.X配置本地YUM源教程一文,此处不再赘述。

安装基础软件包

1
yum install vim net-tools lrzsz unzip dos2unix telnet sysstat iotop pciutils lsof tcpdump wget strace ipmitool psmisc tree bc -y

安装acpid服务

acpid是一个用户空间的服务进程,用来处理电源相关事件,比如将kernel中的电源事件转发给应用程序,告诉应用程序安全的退出,防止应用程序异常退出导致数据损坏。libvirt可以通过向guest虚拟机发送acpid事件触发电源操作,使虚拟机安全关机、重启等操作,相对于强制执行关闭电源操作更安全。通过acpid事件发送开关机信号即我们经常所说的软重启或者软关机。

CentOS6.X/RedHat6.X

1
2
yum install acpid -y
chkconfig acpid on

CentOS7.X/RedHat7.X

1
2
yum install acpid -y
systemctl enable acpid

安装epel源

https://fedoraproject.org/wiki/EPEL/zh-cn下载对应操作系统版本的epel-release包的最新版本,然后上传到虚拟机的/root目录下,然后按照以下步骤进行安装

CentOS6.X/RedHat6.X

说明
EPEL6已经结束支持,最新只有到6.8的版本

1
2
3
cd /root/
rpm -ivh epel-release-6-8.noarch.rpm
rm -rf epel-release-6-8.noarch.rpm

CentOS7.X/RedHat7.X

1
2
3
cd /root/
rpm -ivh epel-release-latest-7.noarch.rpm
rm -rf epel-release-latest-7.noarch.rpm

安装cloud-utils-growpart

安装cloud-utils-growpart工具,实现根分区自动扩展

CentOS6.X/RedHat6.X

1
2
yum install cloud-utils-growpart dracut-modules-growroot -y
dracut -f

说明
重新生成initramfs映像需要一点时间,请耐心等待

CentOS7.X/RedHat7.X

1
yum install cloud-utils-growpart -y

安装qemu-guest-agent

CentOS6.X/RedHat6.X

1
2
yum install qemu-guest-agent -y
chkconfig qemu-ga on

CentOS7.X/RedHat7.X

1
2
yum install qemu-guest-agent -y
systemctl enable qemu-guest-agent

禁用zeroconf

zeroconf是一种古老的自动网络配置技术,在没有DHCP服务的年代,所有服务器都需要网管手动配置IP、hostname等,非常麻烦,zeroconf正好解决了这个问题,不过目前通常都通过DHCP获取地址了。不过一些操作系统仍然会开启这个服务,当DHCP获取IP失败时,会尝试通过zeroconf配置。

zeroconf启动时会自动创建一条路由169.254.0.0/16,而虚拟机访问metadata服务的地址正好是169.254.169.254,如果启动了zeroconf服务,由于路由冲突,虚拟机不能通过169.254.169.254路由到网络节点的metadata服务了。OpenStack虚拟机通常都是通过DHCP获取IP的,因此我们并不需要zeroconf服务。为了虚拟机能够访问metadata服务,我们必须禁止zeroconf服务,关于该问题的更详细讨论可参考bug#983611

1
echo "NOZEROCONF=yes" >> /etc/sysconfig/network

安装cloud-init

说明
本文介绍安装的为社区版的cloud-init,部分云厂商会提供针对自身云平台进行优化的cloud-init版本,可以更好地支持平台服务,如果镜像要在对应云厂商上使用,建议安装云厂商提供的优化过的cloud-init。

CentOS6.X/RedHat6.X

说明
CentOS6.X/RedHat6.X官方YUM源里的cloud-init最新版本为0.7.5,而cloud-init最新版本截止目前已经更新到23.4.2,并且依赖Python3。本文将以安装cloud-init 19.4版本为例介绍具体的安装过程。至于为啥选择这个版本,原因是CentOS7.X/RedHat7.X官网YUM源中提供的cloud-init最新版本为19.4,与之保持一致。

先进行Python3的安装,版本我们选择Python3.6.0,以下为下载地址:

说明
建议下载Gzipped source tarball格式的源码包,下载完上传到/root目录下

安装编译工具和依赖库

1
yum install gcc gcc-c++ make zlib-devel openssl-devel -y

解压Python源码包

1
2
cd /root/
tar zxf Python-3.6.0.tgz

编译安装Python

1
2
3
cd Python-3.6.0
./configure --prefix=/usr/local/python-3.6.0
make && make install

建立软链接

1
2
ln -s /usr/local/python-3.6.0/bin/python3.6 /usr/bin/python3
ln -s /usr/local/python-3.6.0/bin/pip3 /usr/bin/pip3

下载cloud-init 19.4,以下为下载地址,下载完上传到/root目录下

解压cloud-init源码包

1
2
cd /root/
tar zxf cloud-init-19.4.tar.gz

安装cloud-init

1
2
3
cd cloud-init-19.4
pip3 install -r requirements.txt
python3 setup.py install --init-system sysvinit --install-scripts /usr/bin/

说明
如果安装时出现以下错误,参照以下方法解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Traceback (most recent call last):
File "setup.py", line 149, in <module>
for f in glob('systemd/*') if is_f(f) and is_generator(f)],
File "setup.py", line 149, in <listcomp>
for f in glob('systemd/*') if is_f(f) and is_generator(f)],
File "setup.py", line 123, in render_tmpl
tiny_p([sys.executable, './tools/render-cloudcfg', template, fpath])
File "setup.py", line 50, in tiny_p
(cmd, ret, out, err))
RuntimeError: Failed running ['/usr/bin/python3', './tools/render-cloudcfg', 'systemd/cloud-init-generator.tmpl', 'RENDERED_TEMPDckib_okx/cloud-init-generator'] [rc=1] (, Traceback (most recent call last):
File "./tools/render-cloudcfg", line 45, in <module>
main()
File "./tools/render-cloudcfg", line 37, in main
contents = (templater.render_string(contents, tpl_params)).rstrip() + "\n"
File "/root/cloud-init-19.4/cloudinit/templater.py", line 173, in render_string
return renderer(content, params)
File "/root/cloud-init-19.4/cloudinit/templater.py", line 95, in basic_render
return BASIC_MATCHER.sub(replacer, content)
File "/root/cloud-init-19.4/cloudinit/templater.py", line 93, in replacer
return str(selected_params[key])
KeyError: 'LOG_D'
)

原因分析
网上并没做找到有人遇到类似的问题,个人怀疑是因为根据默认的requirements.txt文件安装Python模块时会自动安装最新版本,模块之前的兼容性存在问题导致。通过环境对比调整模块的版本后最终安装成功了,这应该也是验证了我的猜想吧。
解决方法
备份requirements.txt,并生成新的requirements.txt,指定所有模块的版本

1
2
3
4
5
6
7
8
9
10
11
12
cd /root/cloud-init-19.4
cp requirements.txt requirements.txt.bak
cat > requirements.txt << EOF
jinja2==2.11.3
oauthlib==3.1.0
configobj==5.0.6
pyyaml
requests==2.25.1
jsonpatch==1.32
jsonschema==3.2.0
six==1.15.0
EOF

重新安装指定版本的模块并安装cloud-init

1
2
pip3 install -r requirements.txt
python3 setup.py install --init-system sysvinit --install-scripts /usr/bin/

编辑/etc/cloud/cloud.cfg,将disable_root设为0(允许root用户ssh远程登录),ssh_pwauth设为1(允许password认证),保存退出

1
2
3
4
5
6
7
vim /etc/cloud/cloud.cfg
users:
- default

disable_root: 0
ssh_pwauth: 1
...

创建并更新/etc/cloud/cloud.cfg.d/90_datasource.cfg,配置数据源

说明
cloud-init支持多种数据源类型,本文以ConfigDrive为例,更多数据源配置可以参考官方文档

1
2
3
cat > /etc/cloud/cloud.cfg.d/90_datasource.cfg << EOF
datasource_list: [ConfigDrive, None]
EOF

设置cloud-init开机自启

说明
只需要设置开机自启动,不要启动cloud-init

1
2
3
4
5
6
7
8
chkconfig --add cloud-init-local 
chkconfig cloud-init-local on
chkconfig --add cloud-config
chkconfig cloud-config on
chkconfig --add cloud-final
chkconfig cloud-final on
chkconfig --add cloud-init
chkconfig cloud-init on

清理安装包

1
2
3
cd /root/
rm -rf cloud-init-*
rm -rf Python-*

CentOS7.X/RedHat7.X

执行以下命令安装cloud-init

1
yum install cloud-init -y

编辑/etc/cloud/cloud.cfg,将disable_root设为0(允许root用户ssh远程登录),ssh_pwauth设为1(允许password认证),保存退出

1
2
3
4
5
6
7
vim /etc/cloud/cloud.cfg
users:
- default

disable_root: 0
ssh_pwauth: 1
...

创建并更新/etc/cloud/cloud.cfg.d/90_datasource.cfg,配置数据源

说明
cloud-init支持多种数据源类型,本文以ConfigDrive为例,更多数据源配置可以参考官方文档

1
2
3
cat > /etc/cloud/cloud.cfg.d/90_datasource.cfg << EOF
datasource_list: [ConfigDrive, None]
EOF

设置cloud-init开机自启

说明
只需要设置开机自启动,不要启动cloud-init

1
systemctl enable cloud-init

清理/var/lib/cloud/,因为其下有信号量,cloudinit检测到信号量则不再执行

1
rm -rf /var/lib/cloud/*

清理文件

1
2
rpm -e `rpm -qa |grep epel`
yum clean all

清理命令历史记录并关机

1
2
3
4
5
echo > /var/log/wtmp
echo > /var/log/btmp
echo > ~/.bash_history
history -c
shutdown -h now

说明
以上为比较常见通用的配置项,更多自定义配置请根据自己的云环境情况进行调整,例如安装对应云环境的监控agent、安全加固配置、安装防暴力破解工具等等

转换镜像格式

将raw格式镜像文件转换成qcow2格式镜像文件,压缩空间便于传输。

1
qemu-img convert -c -f raw CentOS7.9.raw -O qcow2 CentOS7.9.qcow2

上传镜像


使用glance命令上传镜像

OpenStack Linux镜像制作完成后,将最终生成的qcow2镜像文件上传到OpenStack控制节点,先转成raw镜像文件,然后通过glance命令上传镜像,最后设置镜像的hw_qemu_guest_agent属性为yes即可。相关命令如下:

1
2
3
qemu-img convert -f qcow2 CentOS7.9.qcow2 -O raw CentOS7.9.raw
glance image-create --name "CentOS7.9" --file /tvm/CentOS7.9.raw --disk-format raw --container-format bare --is-public True --progress
glance image-update --property hw_qemu_guest_agent=yes ${IMAGE_ID}

通过rbd直接导入镜像

由于镜像通常比较大,上传时如果使用Glance API,则通过HTTP上传,由于HTTP协议的限制,导致上传非常慢,非常耗时,并且可能会超时导致上传失败。如果Glance使用Ceph作为存储后端,可以通过rbd直接导入(import)的方式上传到Ceph中,速度会大幅度提高。

首先需要把镜像转为raw格式

1
qemu-img convert -f qcow2 CentOS7.9.qcow2 -O raw CentOS7.9.raw

创建一个空镜像,并记录${IMAGE_ID}(不需要指定文件路径,只是占个坑)

1
glance image-create --name "CentOS7.9" --disk-format raw --container-format bare  --is-public True

使用rbd命令直接导入镜像并创建快照

1
2
3
4
rbd --user ${USER_ID} -k ${Keyring} -p ${POOL_NAME} import CentOS7.9.raw
rbd --user ${USER_ID} -k ${Keyring} -p ${POOL_NAME} rename CentOS7.9.raw ${IMAGE_ID}
rbd --user ${USER_ID} -k ${Keyring} -p ${POOL_NAME} snap create ${IMAGE_ID}@snap
rbd --user ${USER_ID} -k ${Keyring} -p ${POOL_NAME} snap protect ${IMAGE_ID}@snap

更新镜像的locations参数

1
glance location-add --url rbd://${FS_ID}/${POOL_NAME}/${IMAGE_ID}/snap $IMAGE_ID [--metadata '{"backend": "${BACKEND_NAME}"}']

设置hw_qemu_guest_agent属性为yes

1
glance image-update --property hw_qemu_guest_agent=yes ${IMAGE_ID}

参考文档