整理工作中移植 OpenStack Victoria 版本核心组件到龙芯 lemote Fedora 28 系统上的过程。

工作需求

现在需要移植 OpenStack 核心组件到 lemote Fedora 28 系统上,索性选取了当前最新的稳定分支 Victoria 版本。

移植所有组件并测试是不现实的,目前只考虑几个核心组件,包括 Nova、Cinder、Neutron、Horizo​​n、Keystone、Glance 和 Placement。

考虑到 OpenStack 是用 Python 编写的,而 Python 环境和大部分系统工具 lemote Fedora 28 系统上已经存在,而且接口是与 x86 一致的,所以不少组件应该是能直接运行的。龙芯平台的 libvirt 和 QEMU/LVM 是修改过的,因此最有可能需要修改的是 OpenStack Nova 组件的代码(事实证明,确实只需要修改这一个组件)。

为了方便部署测试,直接选择使用官方的 DevStack 自动部署脚本。

移植 DevStack

显然,DevStack 不经过修改是无法在龙芯平台上跑通的。修改过的 DevStack 代码见 https://github.com/FreeFlyingSheep/devstack

探测版本信息

该部分代码位于 functions-common

为了能在多个发行版上运行,DevStack 首先借助 lsb_release 命令探测发行版信息。而 lemote 系统上并没有提供 lsb_release 软件包,为此,通过检查 /proc/version 中的信息,发现是 lemote 系统直接返回,来绕过安装 lsb_release

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Make a *best effort* attempt to install lsb_release packages for the
# user if not available.  Note can't use generic install_package*
# because they depend on this!
function _ensure_lsb_release {
    if [[ -x $(command -v lsb_release 2>/dev/null) ]]; then
        return
    fi

    # lemote
    if grep -q "lemote" /proc/version; then
        return
    fi

    if [[ -x $(command -v apt-get 2>/dev/null) ]]; then
        sudo apt-get install -y lsb-release
    elif [[ -x $(command -v zypper 2>/dev/null) ]]; then
        sudo zypper -n install lsb-release
    elif [[ -x $(command -v dnf 2>/dev/null) ]]; then
        sudo dnf install -y redhat-lsb-core
    else
        die $LINENO "Unable to find or auto-install lsb_release"
    fi
}

只是绕过安装并没解决问题,还需要手动指定 lemote 系统的发行版系统,为了和其他发行版统一,借助 /etc/os-release 文件来获取相关信息。

 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
# GetOSVersion
#  Set the following variables:
#  - os_RELEASE
#  - os_CODENAME
#  - os_VENDOR
#  - os_PACKAGE
function GetOSVersion {
    # We only support distros that provide a sane lsb_release
    _ensure_lsb_release

    if ! grep -q "lemote" /proc/version; then
        os_RELEASE=$(lsb_release -r -s)
        os_CODENAME=$(lsb_release -c -s)
        os_VENDOR=$(lsb_release -i -s)
    else # lemote
        os_RELEASE=$(grep "^VERSION_ID=" /etc/os-release | cut -d '=' -f 2)
        os_CODENAME=$(grep "^VERSION=" /etc/os-release | cut -d '(' -f 2 | cut -d ')' -f 1)
        os_VENDOR=$(grep "^NAME=" /etc/os-release | cut -d '=' -f 2)
    fi

    if [[ $os_VENDOR =~ (Debian|Ubuntu|LinuxMint) ]]; then
        os_PACKAGE="deb"
    else
        os_PACKAGE="rpm"
    fi

    typeset -xr os_VENDOR
    typeset -xr os_RELEASE
    typeset -xr os_PACKAGE
    typeset -xr os_CODENAME
}

同时,为了方便后续判断系统信息,参考脚本提供的 is_suse 函数,加上 is_lemote 函数:

1
2
3
function is_lemote {
    is_fedora && grep -q "lemote" /proc/version
}

由于而脚本已经放弃了对 Fedora 28 系统的支持,所以运行时需要加上 FORCE=yes,索性将其添加到 local.conf 中。

编译 ETCD

脚本会自动根据系统版本信息来从官方下载 ETCD 包,而官方并未提供 MIPS 版本的 ETCD,所以需要手动编译。

ETCD 的编译比较简单,直接从官方仓库克隆,切换到相应版本(为了与脚本统一,选择了 3.3.12 版本),编译并参考其他体系结构的压缩文件打包为 etcd-v3.3.12-linux-mips64.tar.gz,之后在脚本中添加 MIPS 体系结构的信息。

该部分代码位于 stackrc

 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
38
39
40
41
42
# etcd3 defaults
ETCD_VERSION=${ETCD_VERSION:-v3.3.12}
ETCD_SHA256_AMD64=${ETCD_SHA256_AMD64:-"dc5d82df095dae0a2970e4d870b6929590689dd707ae3d33e7b86da0f7f211b6"}
ETCD_SHA256_ARM64=${ETCD_SHA256_ARM64:-"170b848ac1a071fe7d495d404a868a2c0090750b2944f8a260ef1c6125b2b4f4"}
ETCD_SHA256_PPC64=${ETCD_SHA256_PPC64:-"77f807b1b51abbf51e020bb05bdb8ce088cb58260fcd22749ea32eee710463d3"}
# etcd v3.2.x doesn't have anything for s390x
ETCD_SHA256_S390X=${ETCD_SHA256_S390X:-""}
# etcd v3.2.x doesn't have anything for mips64
ETCD_SHA256_MIPS64=${ETCD_SHA256_MIPS64:-"5632a3ce5d376701f87887ff5caac5d41a74ec6c1e691a661dc6ce1f1d2effab"}
# Make sure etcd3 downloads the correct architecture
if is_arch "x86_64"; then
    ETCD_ARCH="amd64"
    ETCD_SHA256=${ETCD_SHA256:-$ETCD_SHA256_AMD64}
elif is_arch "aarch64"; then
    ETCD_ARCH="arm64"
    ETCD_SHA256=${ETCD_SHA256:-$ETCD_SHA256_ARM64}
elif is_arch "ppc64le"; then
    ETCD_ARCH="ppc64le"
    ETCD_SHA256=${ETCD_SHA256:-$ETCD_SHA256_PPC64}
elif is_arch "s390x"; then
    # An etcd3 binary for s390x is not available on github like it is
    # for other arches. Only continue if a custom download URL was
    # provided.
    if [[ -n "${ETCD_DOWNLOAD_URL}" ]]; then
        ETCD_ARCH="s390x"
        ETCD_SHA256=${ETCD_SHA256:-$ETCD_SHA256_S390X}
    else
        exit_distro_not_supported "etcd3. No custom ETCD_DOWNLOAD_URL provided."
    fi
elif is_arch "mips64"; then
    # An etcd3 binary for mips64 is not available on github like it is
    # for other arches. Only continue if a custom download URL was
    # provided.
    if [[ -n "${ETCD_DOWNLOAD_URL}" ]]; then
        ETCD_ARCH="mips64"
        ETCD_SHA256=${ETCD_SHA256:-$ETCD_SHA256_MIPS64}
    else
        exit_distro_not_supported "etcd3. No custom ETCD_DOWNLOAD_URL provided."
    fi
else
    exit_distro_not_supported "invalid hardware type - $ETCD_ARCH"
fi

脚本的下载辅助函数会判断文件是否已经存在,若存在则不下载,因此拷贝 etcd-v3.3.12-linux-mips64.tar.gzfiles 目录即可,同时在 local.conf 中添加 ETCD_DOWNLOAD_URL=https://fake-url

编译好的 ETCD 无法直接运行,因为官方没在 MIPS 上测试过,所以需要添加环境变量 ETCD_UNSUPPORTED_ARCH=mips64le。参考脚本 aarch64 体系结构部分的代码,直接把该环境变量添加到 .service 文件中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# start_etcd3() - Starts to run the etcd process
function start_etcd3 {
    ...
    local unitfile="$SYSTEMD_DIR/$ETCD_SYSTEMD_SERVICE"
    write_user_unit_file $ETCD_SYSTEMD_SERVICE "$cmd" "" "root"

    iniset -sudo $unitfile "Unit" "After" "network.target"
    iniset -sudo $unitfile "Service" "Type" "notify"
    iniset -sudo $unitfile "Service" "Restart" "on-failure"
    iniset -sudo $unitfile "Service" "LimitNOFILE" "65536"
    if is_arch "aarch64"; then
        iniset -sudo $unitfile "Service" "Environment" "ETCD_UNSUPPORTED_ARCH=arm64"
    fi
    if is_arch "mips64"; then
        iniset -sudo $unitfile "Service" "Environment" "ETCD_UNSUPPORTED_ARCH=mips64le"
    fi

    $SYSTEMCTL daemon-reload
    $SYSTEMCTL enable $ETCD_SYSTEMD_SERVICE
    $SYSTEMCTL start $ETCD_SYSTEMD_SERVICE
}

修复已知问题

已知 lemote Fedora 28 系统上存在以下问题:

  • 缺少 dstat 软件包,且该包并不会在脚本运行中自动安装,使用 dnf 安装即可。
  • 系统自带的 virtualenv 版本过低,使用 pip 升级。
  • 缺少 uwsgi 软件包,dnf 源中不存在该软件包,使用 pip 安装。
  • 系统自带的 wrapt 会阻碍脚本自动升级该软件包,提前把它强制升级。

参考 fixup_fedora 函数,添加 fixup_lemote 函数,并在 fixup_fedora 函数中调用它。该部分代码位于 tools/fixup_stuff.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function fixup_lemote {
    if ! is_lemote; then
        return
    fi

    # Install missing tools
    install_package dstat
    pip_install -U virtualenv
    pip_install -U uwsgi
    pip_install -UI wrapt
}

function fixup_fedora {
    ...
    fixup_lemote
}

创建虚拟环境

系统下的 Python3 不认识 python3 -m venv 命令,将其改为 python3 -m virtualenv。该部分代码位于 stackrc

1
2
3
4
5
6
7
# Create a virtualenv with this
# Use the built-in venv to avoid more dependencies
if ! is_lemote; then
    export VIRTUALENV_CMD="python3 -m venv"
else # lemote
    export VIRTUALENV_CMD="python3 -m virtualenv"
fi

设置服务自启

系统重启后,DevStack 无法正常运行,检查日志和服务信息发现 httpdmemcached 服务没有设置自启。

检查脚本发现没有设置服务自启的封装函数(可能是我没找仔细?),参考 start_service 函数定义 _enable_service 函数 (注意已经存在 enable_service 函数了)。该部分代码位于 functions-common

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Service wrapper to enable services
# Note that we already have enable_service above
# _enable_service service-name
function _enable_service {
    if [ -x /bin/systemctl ]; then
        sudo /bin/systemctl enable $1
    else
        sudo service $1 enable
    fi
}

之后,在对应的服务处加上自启。对于 httpd,脚本对 apache 服务又进行了一层封装,添加相应内容。代码位于 lib/apache

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# lib/apache exports the following functions:
# ...
# - enable_apache_server

# install_apache_wsgi() - Install Apache server and wsgi module
function install_apache_wsgi {
    ...
    enable_apache_server
}

# enable_apache_server() - Enabling apache server
function enable_apache_server {
    _enable_service $APACHE_NAME
}

对于 memcached,在安装处添加自启即可。代码位于 lib/keystone

1
2
3
4
5
# start_keystone() - Start running processes
function start_keystone {
    ...
    _enable_service memcached
}

虚拟化

对于 lemote Fedora 28 系统,必须确保 libvirt-pythonpython3-libvirt 软件包的版本一致,且高于 6.6.0,索性直接在代码中写死。该部分代码位于 lib/nova_plugins/functions-libvirt

 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
38
39
40
41
42
43
44
# Installs required distro-specific libvirt packages.
function install_libvirt {

    if is_ubuntu; then
        install_package qemu-system libvirt-clients libvirt-daemon-system libvirt-dev
        if is_arch "aarch64"; then
            install_package qemu-efi
        fi
        # uninstall in case the libvirt version changed
        pip_uninstall libvirt-python
        pip_install_gr libvirt-python
        #pip_install_gr <there-si-no-guestfs-in-pypi>
    elif is_fedora || is_suse; then

        # Optionally enable the virt-preview repo when on Fedora
        if [[ $DISTRO =~ f[0-9][0-9] ]] && [[ ${ENABLE_FEDORA_VIRT_PREVIEW_REPO} == "True" ]]; then
            # https://copr.fedorainfracloud.org/coprs/g/virtmaint-sig/virt-preview/
            sudo dnf copr enable -y @virtmaint-sig/virt-preview
        fi

        # Note that in CentOS/RHEL this needs to come from the RDO
        # repositories (qemu-kvm-ev ... which provides this package)
        # as the base system version is too old.  We should have
        # pre-installed these
        install_package qemu-kvm

        install_package libvirt libvirt-devel
        if is_arch "aarch64"; then
            install_package edk2.git-aarch64
        fi

        if ! is_lemote; then
            pip_uninstall libvirt-python
            pip_install_gr libvirt-python
        else # lemote
            install_package libvirt-6.6.0
            install_package python3-libvirt-6.6.0
        fi
    fi

    if [[ $DEBUG_LIBVIRT_COREDUMPS == True ]]; then
        _enable_coredump
    fi
}

同时,系统只支持 custom 模式,模拟 Loongson-3A4000(见修改 CPU 模式部分)。默认的脚本只提供 nonehost-modelhost-passthrough 模式,参考 aarch64 体系结构,添加相关代码。该部分代码位于 lib/nova_plugins/hypervisor-libvirt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# configure_nova_hypervisor - Set config files, create data dirs, etc
function configure_nova_hypervisor {
    ...
    # arm64-specific configuration
    if is_arch "aarch64"; then
        iniset $NOVA_CONF libvirt cpu_mode "host-passthrough"
    fi

    # mips64-specific configuration
    if is_arch "mips64"; then
        iniset $NOVA_CONF libvirt cpu_mode "custom"
        iniset $NOVA_CONF libvirt cpu_models "Loongson-3A4000"
    fi
    ...
}

移植 Nova

由于龙芯平台的 libvirt 和 QEMU/KVM 的实现还不完全,nova-compute 不经过修改无法在龙芯平台上正常运行。修改过的 Nova 代码见 https://github.com/FreeFlyingSheep/Nova

修改 CPU 模式

在未修改 CPU 模式时,运行 DevStack,尝试创建虚拟机时报错,查看日志显示 libvirt 内部错误,未知 CPU 类型 qemu64

查看 nova-compute 配置文件发现 DevStack 默认 CPU 模式设置为 none,尝试改为 host-modelhost-passthrough,依然报 libvirt 内部错误,但错误内容变为不支持这两种模式。

查看 libvirt 源码,定位报错的位置,发现龙芯平台上的 libvirt 实现并不完全,去把这些接口都实现了显然不现实。考虑到龙芯平台通过修改过的 virt-manager 是能正常创建虚拟机的,就想到比较龙芯平台的 virt-manager 创建虚拟机的参数和 nova-compute 默认创建的参数(主要比较生成的给 libvirt 使用的 xml 文件)。

对比发现,virt-manager 生成的 xml 文件中,使用的是 custom 模式,模拟 Loongson-3A4000。修改 nova-compute 的配置文件,设置这两个参数,错误变为了没有使用 PCIe。

修改 PCIe 配置

查看 Nova 源码,直接强制检查 PCIe 的函数对于 mips64el 架构返回 True。该部分代码位于 virt/libvirt/driver.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def _guest_needs_pcie(self, guest, caps):
    """Check for prerequisites for adding PCIe root port
    controllers
    """
    ...
    if caps.host.cpu.arch == fields.Architecture.MIPS64EL:
        return True
    if not CONF.libvirt.num_pcie_ports:
        return False
    if (caps.host.cpu.arch == fields.Architecture.AARCH64 and
            guest.os_mach_type.startswith('virt')):
        return True
    if (caps.host.cpu.arch == fields.Architecture.X86_64 and
            guest.os_mach_type is not None and
            'q35' in guest.os_mach_type):
        return True
    return False

重启 Nova 服务,发现虚拟机创建成功,但无法运行,错误变为了没法加载 BIOS。

添加其他参数

继续比较 xml 文件,发现 virt-manager 生成的 xml 会指定 BIOS 文件和其他相关参数,查看 Nova 源码,将这部分内容写到对应位置。代码位于 virt/libvirt/driver.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def _configure_guest_by_virt_type(self, guest, virt_type, caps, instance,
                                  image_meta, flavor, root_device_name,
                                  sev_enabled):
    if virt_type == "xen":
        ...
    elif virt_type in ("kvm", "qemu"):
        if caps.host.cpu.arch in (fields.Architecture.I686,
                                    fields.Architecture.X86_64):
            guest.sysinfo = self._get_guest_config_sysinfo(instance)
            guest.os_smbios = vconfig.LibvirtConfigGuestSMBIOS()
        hw_firmware_type = image_meta.properties.get('hw_firmware_type')
        ...
        if image_meta.properties.get('hw_boot_menu') is None:
            guest.os_bootmenu = strutils.bool_from_string(
                flavor.extra_specs.get('hw:boot_menu', 'no'))
        else:
            guest.os_bootmenu = image_meta.properties.hw_boot_menu
        if caps.host.cpu.arch == fields.Architecture.MIPS64EL:
            guest.os_loader = "/usr/share/qemu/bios_loongson3.bin"
            guest.os_loader_type = "rom"
            guest.os_mach_type = "loongson3-virt"

    elif virt_type == "lxc":
        ...

重启 Nova 服务,发现虚拟机成功创建,并运行,但控制台界面卡在了加载内核。

修改显示模式(后来又改回去了)

查看日志,发现并没有任何报错。思考了一会,推测是内核其实已经成功启动,但没正常显示。

继续对比 xml 文件,发现 nova-compute 默认使用的显卡设备模型为 cirrus,而 virt-manager 使用的显卡设备模型为 qxl。在 virt-manager 的选项中,没有找到 cirrus 显卡设备模型,怀疑不支持。

于是修改 nova-compute 相关代码,使它在 mips64 架构下默认使用 qxl。该部分代码位于 virt/libvirt/driver.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def _add_video_driver(self, guest, image_meta, flavor):
    ...
    if guest.os_type == fields.VMMode.XEN:
        video.type = 'xen'
    elif CONF.libvirt.virt_type == 'parallels':
        video.type = 'vga'
    elif guestarch in (fields.Architecture.PPC,
                        fields.Architecture.PPC64,
                        fields.Architecture.PPC64LE):
        # NOTE(ldbragst): PowerKVM doesn't support 'cirrus' be default
        # so use 'vga' instead when running on Power hardware.
        video.type = 'vga'
    elif guestarch == fields.Architecture.AARCH64:
        # NOTE(kevinz): Only virtio device type is supported by AARCH64
        # so use 'virtio' instead when running on AArch64 hardware.
        video.type = 'virtio'
    elif (CONF.spice.enabled) or (guestarch == fields.Architecture.MIPS64):
        video.type = 'qxl'
    if image_meta.properties.get('hw_video_model'):
        video.type = image_meta.properties.hw_video_model
        if not self._video_model_supported(video.type):
            raise exception.InvalidVideoMode(model=video.type)
    ...

重启 Nova 服务,这次控制台界面显示出了开机动画,但发现键盘没有反应。

添加键盘设备

鼠标能动但键盘没有反应,推测使没添加键盘,查看 xml 发现果然如此。

在 Nova 中添加键盘。该部分代码位于 virt/libvirt/driver.py

 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
def _get_guest_config(self, instance, network_info, image_meta,
                      disk_info, rescue=None, block_device_info=None,
                      context=None, mdevs=None, accel_info=None):
    """Get config data for parameters.

    :param rescue: optional dictionary that should contain the key
        'ramdisk_id' if a ramdisk is needed for the rescue image and
        'kernel_id' if a kernel is needed for the rescue image.

    :param mdevs: optional list of mediated devices to assign to the guest.
    :param accel_info: optional list of accelerator requests (ARQs)
    """
    ...
    self._guest_add_spice_channel(guest)

    if self._guest_add_video_device(guest):
        self._add_video_driver(guest, image_meta, flavor)

        # We want video == we want graphical console. Some architectures
        # do not have input devices attached in default configuration.
        # Let then add USB Host controller and USB keyboard.
        # x86(-64) and ppc64 have usb host controller and keyboard
        # s390x does not support USB
        if caps.host.cpu.arch in (fields.Architecture.AARCH64,
                                    fields.Architecture.MIPS64EL):
            self._guest_add_usb_host_keyboard(guest)

    # Some features are only supported 'qemu' and 'kvm' hypervisor
    if virt_type in ('qemu', 'kvm'):
        self._set_qemu_guest_agent(guest, flavor, instance, image_meta)
        self._add_rng_device(guest, flavor, image_meta)
        self._add_vtpm_device(guest, flavor, instance, image_meta)
    ...

重启 Nova 服务,键盘能正常使用了,但鼠标能移动却无法点击。

改用 spice

听取同事建议后,索性改用 spice 协议,而不是 vnc 协议。这样可以移除 Nova 中显示模式部分的修改,鼠标也能正常使用了。

对于 DevStack,只需要修改配置文件 local.conf 即可,添加两行内容:

1
2
enable_service n-spice n-sproxy
disable_service n-novnc n-xvnc

其他问题

创建虚拟机的过程中,一旦涉及 Cinder 服务,就会报错,因此暂时使用了 Nova 自带的创建卷功能。

成功把 Nova 跑通后,开始解决 Cinder 无法映射块设备的问题。查看日志报错信息,发现是创建 LVM 卷的时候出错了。

根据网上资料手动创建 LVM 卷,复现了报错信息,只要是使用 thin 模式,创建卷就会报错,这应该是系统中相关的软件包存在问题,暂时将 cinder 的 LVM 模式更换为 default,绕过了这个问题,成功创建了卷。

还有一些问题,但暂时没想到解决办法。比如内核没有输出日志信息,检查 xml 确定已经重定向到了 console.log,内核启动也添加了相关参数。还有从 ISO 文件创建虚拟机时会报找不到设备等问题(由于系统的 QEMU/KVM 只支持龙芯的镜像,所以必须使用提前准备好的镜像)。

对于 DevStack,最终的 local.conf 配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[[local|localrc]]
FORCE=yes

ADMIN_PASSWORD=123456
DATABASE_PASSWORD=$ADMIN_PASSWORD
RABBIT_PASSWORD=$ADMIN_PASSWORD
SERVICE_PASSWORD=$ADMIN_PASSWORD

GIT_BASE=https://github.com
NOVA_REPO=https://github.com/FreeFlyingSheep/nova.git

ETCD_DOWNLOAD_URL=https://fake-url

DOWNLOAD_DEFAULT_IMAGES=False
IMAGE_URLS=https://fake-url/fedora28.qcow2

CINDER_LVM_TYPE=default

enable_service n-spice n-sproxy
disable_service n-novnc n-xvnc

至此,OpenStack 的移植初步完成了,到了勉强能用的地步。因为暂时没有后续的需求,所以先到此为止了。