自定义 Anaconda


Red Hat Enterprise Linux 10

更改安装程序外观,并在 Red Hat Enterprise Linux 上创建自定义附加组件

摘要

Anaconda 是 Red Hat Enterprise Linux 使用的安装程序。当在您的环境中安装 RHEL 时,您可以自定义 Anaconda 来扩展功能。

第 1 章 Anaconda 自定义介绍

Red Hat Enterprise Linux 和 Fedora 安装程序 Anaconda 对其最新版本进行了很多改进。这些改进之一就是提高了定制性。现在,您可以编写附加组件来扩展基本的安装程序功能,并更改图形用户界面的外观。

本文档将介绍如何自定义以下内容:

  • 引导菜单 - 预配置选项、颜色方案以及后台
  • 图形界面的外形 - 徽标、背景、产品名称
  • 安装程序功能 - 附加组件可通过在图形和文本用户界面中添加新的 Kickstart 命令和新屏幕来增强安装程序
重要

本书中描述的流程是为 Red Hat Enterprise Linux 10 或类似的系统编写的。在其他系统上,用于创建自定义 ISO 镜像的工具和应用程序(如 xorrisofs )可能有所不同,流程可能需要调整。

支持声明

红帽只支持自定义 Red Hat Enterprise Linux 安装介质和使用 Red Hat Enterprise Linux 镜像构建器的镜像。或者,您可以使用 Kickstart 在您的基础设施中部署一致的系统。

第 2 章 执行预自定义任务

2.1. 使用 ISO 镜像

在这个部分中,您将了解如何:

  • 提取 Red Hat Enterprise Linux ISO。
  • 创建包含自定义的新引导镜像。

2.2. 下载 RHEL 引导镜像

在开始自定义安装程序前,请下载红帽提供的引导镜像。您可以在登录到您的帐户后 从红帽客户门户网站获取 Red Hat Enterprise Linux 10 引导介质。

注意
  • 您的帐户必须有足够的权限来下载 Red Hat Enterprise Linux 10 镜像。
  • 您必须下载 Binary DVDBoot ISO 镜像。
  • 您不能使用其他可用的下载(如 KVM 客户机镜像或补充 DVD)自定义安装程序。

有关 Binary DVD 和 Boot ISO 下载的详情,请参考 产品下载

2.3. 提取 Red Hat Enterprise Linux 引导镜像

您可以提取引导镜像的内容。

流程

  1. 确保目录 /mnt/iso 存在,并且当前未在那里挂载任何内容。
  2. 挂载下载的镜像。

    # mount -t iso9660 -o loop path/to/image.iso /mnt/iso

    其中 path/to/image.iso 是下载的引导镜像的路径。

  3. 创建您要放置 ISO 镜像内容的工作目录。

    $ mkdir /tmp/ISO
  4. 将挂载镜像的所有内容复制到新工作目录中。确保使用 -p 选项来保留文件和目录的权限和所有权。

    # cp -pRf /mnt/iso /tmp/ISO
  5. 卸载镜像。

    # umount /mnt/iso

第 3 章 自定义引导菜单

您可以自定义引导菜单选项。有关下载和提取引导镜像的详情,请参考 提取 Red Hat Enterprise Linux 引导镜像

引导菜单自定义涉及以下高级别任务:

  1. 完成先决条件。
  2. 自定义引导菜单。
  3. 创建自定义引导镜像。

3.1. 自定义引导菜单

引导菜单是使用安装镜像引导系统后出现的菜单。通常,此菜单允许您在以下选项中选择:安装 Red Hat Enterprise Linux从本地驱动器引导拯救安装的系统。要自定义引导菜单,您可以:

  • 自定义默认选项。
  • 添加更多选项。
  • 改变视觉风格(颜色和背景)。

安装介质由 GRUB2 引导装载程序组成。GRUB2 引导装载程序用于带有 UEFI 固件的系统。

自定义引导菜单选项对 Kickstart 特别有用。在开始安装前,必须向安装程序提供 Kickstart 文件。通常,这可以通过手动编辑现有引导选项之一来添加 inst.ks= 引导选项来完成。如果编辑了介质中的引导装载程序配置文件,可以把这个选项添加到预先配置的条目之一。

3.2. 带有传统 BIOS 固件的系统

引导介质上的 boot/grub2/grub.cfg 配置文件包含预配置菜单条目的列表,以及其他控制外观和引导菜单功能的指令。在配置文件中,Red Hat Enterprise Linux 的默认菜单条目(Test this media & install Red Hat Enterprise Linux 10)在以下块中定义:

menuentry 'Install Red Hat Enterprise Linux 10.0' --class fedora --class gnu-linux --class gnu --class os {
	linux /images/pxeboot/vmlinuz inst.stage2=hd:LABEL=RHEL-10-0-BaseOS-x86_64 quiet
	initrd /images/pxeboot/initrd.img
}
menuentry 'Test this media & install Red Hat Enterprise Linux 10.0' --class fedora --class gnu-linux --class gnu --class os {
	linux /images/pxeboot/vmlinuz inst.stage2=hd:LABEL=RHEL-10-0-BaseOS-x86_64 rd.live.check quiet
	initrd /images/pxeboot/initrd.img
}

其中:

  • menuentry - 定义条目的标题。它使用单引号或双引号(' 或 ")进行指定。您可以使用 --class 选项将菜单条目分组到不同的类中,然后使用 GRUB2 主题进行不同的样式化。

    注意

    您必须将每个菜单条目定义包含在大括号({})中。

  • linux - 定义引导的内核(示例中为/images/pxeboot/vmlinuz ),以及其他额外的选项(如果有的话)。您可以自定义这些选项,来更改引导条目的行为。有关适用于 Anaconda 的选项的详情,请参阅 自动安装 RHEL。一个值得注意的选项是 inst.ks=,它允许您指定 Kickstart 文件的位置。您可以将 Kickstart 文件放在引导 ISO 镜像上,并使用 inst.ks= 选项指定它的位置;例如,您可以将 kickstart.ks 文件放在镜像的根目录中,并使用 inst.ks=hd:LABEL=RHEL-10-0-BaseOS-x86_64:/kickstart.ks。您还可以使用 dracut.cmdline (7) 手册页中列出的 dracut 选项。

    重要

    当使用磁盘标签引用某个驱动器时,例如 inst.stage2=hd:LABEL=RHEL-10-0-BaseOS-x86_64,使用 \x20 替换所有空格。

  • initrd - 要加载的初始 RAM 磁盘( initrd )镜像的位置。grub.cfg 配置文件中使用的其他选项有:

    • set timeout - 确定在自动使用默认菜单条目之前显示的引导菜单的时长。默认值为 60,这意味着菜单显示 60 秒。将此值设置为 -1,可完全禁用超时。

      注意

      在执行无头安装时,将 timeout 设置为 0 非常有用,因为此设置会立即激活默认引导条目。

  • submenu - 一个允许您在其下创建子菜单,并对一些条目进行分组的子菜单块,而不是在主菜单中显示它们。默认配置中的 Troubleshooting 子菜单包含用于救援现有系统的条目。条目的标题在单引号或双引号(' 或 ")中。如上所述,submenu 块包含一个或多个 menuentry 定义,整个块都使用大括号({})括起来。例如:

    submenu 'Submenu title' {
      menuentry 'Submenu option 1' {
        linux /images/pxeboot/vmlinuz inst.stage2=hd:LABEL=RHEL-10-0-BaseOS-x86_64 nomodeset quiet
        initrd /images/pxeboot/initrd.img
      }
      menuentry 'Submenu option 2' {
        linux /images/pxeboot/vmlinuz inst.stage2=hd:LABEL=RHEL-10-0-BaseOS-x86_64 inst.rescue quiet
        initrd /images/pxeboot/initrd.img
      }
    }
  • set default - 确定默认条目。条目号从 0 开始。如果要使第三个条目成为默认条目,请使用 set default=2 ,以此类推。
  • theme - 确定包含 GRUB2 主题文件的目录。您可以使用主题来定制引导装载程序的视觉方面 - 后台、字体和特定元素的颜色。

3.3. 带有 UEFI 固件的系统

引导介质上的 EFI/BOOT/grub.cfg 配置文件包含预配置的菜单条目列表,以及其他控制外观和引导菜单功能的指令。在配置文件中,Red Hat Enterprise Linux 的默认菜单条目(Test this media & install Red Hat Enterprise Linux 10)在以下块中定义:

menuentry 'Test this media & install Red Hat Enterprise Linux 10.0' --class fedora --class gnu-linux --class gnu --class os {
	linuxefi /images/pxeboot/vmlinuz inst.stage2=hd:LABEL=RHEL-10-0-BaseOS-x86_64 rd.live.check quiet
	initrdefi /images/pxeboot/initrd.img
}

其中:

  • menuentry - 定义条目的标题。它使用单引号或双引号('")指定。您可以使用 --class 选项将菜单条目分组到不同的 中,然后使用 GRUB2 主题进行不同的样式化。

    注意

    如上例所示,您必须将每个菜单条目定义包含在大括号({})中。

  • linuxefi - 定义引导的内核(示例中的/images/pxeboot/vmlinuz ),以及其他额外的选项(如果有的话)。

    您可以自定义这些选项,来更改引导条目的行为。有关适用于 Anaconda 的选项的详情,请参考 Kickstart 引导选项

    一个值得注意的选项是 inst.ks=,它允许您指定 Kickstart 文件的位置。您可以将 Kickstart 文件放在引导 ISO 镜像上,并使用 inst.ks= 选项指定它的位置;例如,您可以将 kickstart.ks 文件放在镜像的根目录中,并使用 inst.ks=hd:LABEL=RHEL-10-0-BaseOS-x86_64:/kickstart.ks

    您还可以使用您系统上 dracut.cmdline(7) 手册页中列出的 dracut 选项 。

    重要

    当使用磁盘标签引用某个驱动器时,例如 inst.stage2=hd:LABEL=RHEL-10-BaseOS-x86_64,使用 \x20 替换所有空格。

  • initrdefi - 要加载的初始 RAM 磁盘(initrd)镜像的位置。

grub.cfg 配置文件中使用的其他选项有:

  • set timeout - 确定在自动使用默认菜单条目之前显示的引导菜单的时长。默认值为 60,这意味着菜单显示 60 秒。将此值设置为 -1,可完全禁用超时。

    注意

    在执行无头安装时,将超时设为 0 非常有用,因为此设置会立即激活默认引导条目。

  • submenu - 一个允许您在其下创建子菜单,并对一些条目进行分组的 submenu 块,而不是在主菜单中显示它们。默认配置中的 Troubleshooting 子菜单包含用于拯救现有系统的条目。

    条目的标题在单引号或双引号('")中。

    如上所述,submenu 块包含一个或多个 menuentry 定义,整个块使用大括号({})括起来,例如:

    submenu 'Submenu title' {
      menuentry 'Submenu option 1' {
        linuxefi /images/pxeboot/vmlinuz inst.stage2=hd:LABEL=RHEL-10-0-BaseOS-x86_64 nomodeset quiet
        initrdefi /images/pxeboot/initrd.img
      }
      menuentry 'Submenu option 2' {
        linuxefi /images/pxeboot/vmlinuz inst.stage2=hd:LABEL=RHEL-10-0-BaseOS-x86_64 inst.rescue quiet
        initrdefi /images/pxeboot/initrd.img
      }
    }
  • set default - 确定默认条目。条目号从 0 开始。如果要使 第三个 条目成为默认条目,请使用 set default=2 ,以此类推。
  • theme - 确定包含 GRUB2 主题文件的目录。您可以使用主题来定制引导装载程序的视觉方面 - 后台、字体和特定元素的颜色。

第 4 章 图形用户界面的品牌塑造和镀铬

Anaconda 用户界面的自定义可能包括自定义图形元素和自定义产品名称。

先决条件

  1. 您已下载并提取 ISO 镜像。
  2. 您已创建了自己的品牌资料。

有关下载和提取引导镜像的详情,请参考 提取 Red Hat Enterprise Linux 引导镜像

用户界面自定义涉及以下高级别任务:

  1. 完成先决条件。
  2. 创建自定义品牌资料(如果您计划自定义图形元素)。
  3. 自定义图形元素(如果您计划自定义它们)。
  4. 自定义产品名称(如果您计划自定义它)。
  5. 创建一个 product.img 文件。
  6. 创建一个自定义引导镜像。
注意

要创建自定义品牌塑造材料,首先参考默认的图形元素文件类型和尺寸。您可以相应地创建自定义资料。有关默认图形元素的详情,请查看 自定义图形元素 部分中提供的示例文件。

4.1. 定制图形元素

要自定义图形元素,您可以使用自定义的品牌材料修改或替换可自定义的元素,并更新容器文件。

安装程序的可自定义图形元素存储在安装程序运行时文件系统的 /usr/share/anaconda/pixmaps/ 目录中。该目录包括以下可定制文件:

pixmaps
├─ anaconda-password-show-off.svg
├─ anaconda-password-show-on.svg
├─ right-arrow-icon.png
├─ sidebar-bg.png
├─ sidebar-logo.png
└─ topbar-bg.png

此外,/usr/share/anaconda/ 目录包含一个名为 anaconda-gtk.css 的基本 CSS 风格表,它决定了主 UI 元素的文件名和参数 - 徽标以及侧边栏和顶部栏的徽标。特定于产品的风格表自定义位于一个单独的文件(/usr/share/anaconda/pixmaps/redhat.css)中,并覆盖 anaconda-gtk.css 文件中的默认值。对 CSS 自定义使用特定于产品的文件,因为它仅根据需要覆盖风格表的特定元素。

特定于产品的 redhat.css 文件具有以下内容,这些内容可根据您的要求进行自定义(关于完整的风格表规范,请查看 anaconda-gtk.css 文件的内容):

/* theme colors/images */

@define-color product_bg_color @redhat;

/* logo and sidebar classes */

.logo-sidebar {
   background-image: url('/usr/share/anaconda/pixmaps/sidebar-bg.png');
   background-color: @product_bg_color;
   background-repeat: no-repeat;
}

/* Add a logo to the sidebar */

.logo {
   background-image: url('/usr/share/anaconda/pixmaps/sidebar-logo.png');
   background-position: 50% 20px;
   background-repeat: no-repeat;
   background-color: transparent;
}

/* This is a placeholder to be filled by a product-specific logo. */

.product-logo {
   background-image: none;
   background-color: transparent;
}

AnacondaSpokeWindow #nav-box {
   background-color: @product_bg_color;
   background-image: url('/usr/share/anaconda/pixmaps/topbar-bg.png');
   background-repeat: no-repeat;
   color: white;
}

CSS 文件最重要的部分是根据分辨率处理缩放的方法。PNG 镜像背景无法扩展,它们始终以真实尺寸显示。相反,背景信息具有透明背景,样式表在 @define-color 行上定义了匹配的背景颜色。因此,背景 图像 “淡出”为背景 颜色,这意味着背景可以在所有分辨率下工作,而无需图像缩放。

您还可以更改 background-repeat 参数来平铺背景;或者,如果您确信将要安装的每个系统都具有相同的显示分辨率,则您可以使用填充整个栏的背景图像。

以上列出的任何文件都可以自定义。这样做之后,请按照 创建 product.img 文件 章节中的说明创建自己的带有自定义图形的 product.img,然后 创建自定义引导镜像 来创建一个包含您的更改的新的可引导 ISO 镜像。

4.2. 自定义产品名称

要自定义产品名称,您必须创建一个自定义 .buildstamp 文件。为此,请使用以下内容创建一个新的 .buildstamp 文件:

[Main]
Product=My Distribution
Version=10
BugURL=https://bugzilla.redhat.com/
IsFinal=True

My Distribution 更改为您要在安装程序中显示的名称。

创建自定义 .buildstamp 文件后,请按照 创建 product.img 文件 章节来创建一个包含您的自定义的新的 product.img 文件,按照 创建自定义引导镜像 章节来创建一个包含您的更改的新的可引导 ISO 文件。

4.3. 配置默认配置文件

您可以以 .ini 文件格式编写 Anaconda 配置文件。Anaconda 配置文件由section、options 和 comments 组成。每个部分都由一个 section 标头定义,注释以 # 字符开头,key = value 定义 options。生成的配置文件使用 configparser Python 配置文件解析器进行了处理。

默认配置文件位于 /etc/anaconda/anaconda.conf,包含支持的 sections 和 options 。该文件提供了安装程序的完整默认配置。您可以在 /etc/anaconda/conf.d/ 目录中创建自定义配置文件。

以下配置文件描述了默认配置:

# Anaconda configuration file.

[Anaconda]
# Run Anaconda in the debugging mode.
debug = False

# List of Anaconda DBus modules that can be activated.
# Supported patterns: MODULE.PREFIX., MODULE.NAME activatable_modules = org.fedoraproject.Anaconda.Modules.
    org.fedoraproject.Anaconda.Addons.*

# List of Anaconda DBus modules that are not allowed to run.
# Supported patterns: MODULE.PREFIX., MODULE.NAME forbidden_modules = # List of Anaconda DBus modules that can fail to run. # The installation won't be aborted because of them. # Supported patterns: MODULE.PREFIX., MODULE.NAME
optional_modules =
    org.fedoraproject.Anaconda.Modules.Subscription
    org.fedoraproject.Anaconda.Addons.*


[Installation System]
# Type of the installation system.
# FIXME: This is a temporary solution.
type = UNKNOWN

# Should the installer show a warning about enabled SMT?
can_detect_enabled_smt = False


[Installation Target]
# Type of the installation target.
type = HARDWARE

# A path to the physical root of the target.
physical_root = /mnt/sysimage

# A path to the system root of the target.
system_root = /mnt/sysroot

# Should we install the network configuration?
can_configure_network = True

# Should we copy input kickstart to target system?
can_copy_input_kickstart = True

# Should we save kickstart equivalent to installation settings to the new system?
can_save_output_kickstart = True

# Should we save logs from the installation to the new system?
can_save_installation_logs = True


[Network]
# Network device to be activated on boot if none was configured so.
# Valid values:
#
#   NONE                   No device
#   DEFAULT_ROUTE_DEVICE   A default route device
#   FIRST_WIRED_WITH_LINK  The first wired device with link
#
default_on_boot = NONE


[Payload]
# Default package environment.
default_environment =

# List of ignored packages.
ignored_packages =

# Names of repositories that provide latest updates.
updates_repositories =

# Names of repositories disabled by default.
# Supported patterns: REPO-NAME, PREFIX*, SUFFIX, *INFIX
disabled_repositories =
    source
    debuginfo
    updates-testing
    updates-testing-modular

# List of .treeinfo variant types to enable.
# Valid items:
#
#   addon
#   optional
#   variant
#
enabled_repositories_from_treeinfo = addon optional variant

# Enable installation from the closest mirror.
enable_closest_mirror = True

# Default installation source.
# Valid values:
#
#    CLOSEST_MIRROR  Use closest public repository mirror.
#    CDN             Use Content Delivery Network (CDN).
#
default_source = CLOSEST_MIRROR

# Enable ssl verification for all HTTP connection
verify_ssl = True

# GPG keys to import to RPM database by default.
# Specify paths on the installed system, each on a line.
# Substitutions for $releasever and $basearch happen automatically.
default_rpm_gpg_keys =

[Security]
# Enable SELinux usage in the installed system.
# Valid values:
#
#  -1  The value is not set.
#   0  SELinux is disabled.
#   1  SELinux is enabled.
#
selinux = -1


[Bootloader]
# Type of the bootloader.
# Supported values:
#
#   DEFAULT   Choose the type by platform.
#   EXTLINUX  Use extlinux as the bootloader.
#   SDBOOT    Use systemd-boot as the bootloader.
#
type = DEFAULT

# Name of the EFI directory.
efi_dir = default

# Hide the GRUB menu.
menu_auto_hide = False

# Are non-iBFT iSCSI disks allowed?
nonibft_iscsi_boot = False

# Arguments preserved from the installation system.
preserved_arguments =
    cio_ignore zfcp.allow_lun_scan
    speakup_synth apic noapic apm ide noht acpi video
    pci nodmraid nompath nomodeset noiswmd fips selinux
    biosdevname ipv6.disable net.ifnames net.ifnames.prefix
    nosmt vga


[Storage]
# Enable iBFT usage during the installation.
ibft = True

# Tell multipathd to use user friendly names when naming devices during the installation.
multipath_friendly_names = True

# Create GPT discoverable partition type IDs, if possible
gpt_discoverable_partitions = True

# Do you want to allow imperfect devices (for example, degraded mdraid array devices)?
allow_imperfect_devices = False

# Btrfs compression algorithm and level. e.g. zstd:1
btrfs_compression =

# Default disk label type.
# Valid values:
#
#    gpt  Prefer creation of GPT disk labels.
#    mbr  Prefer creation of MBR disk labels.
#
#    If not specified, use whatever Blivet uses by default.
#
disk_label_type =

# Default file system type. Use whatever Blivet uses by default.
file_system_type =

# Default partitioning.
# Specify a mount point and its attributes on each line.
#
# Valid attributes:
#
#   size <SIZE>    The size of the mount point.
#   min <MIN_SIZE> The size will grow from MIN_SIZE to MAX_SIZE.
#   max <MAX_SIZE> The max size is unlimited by default.
#   free <SIZE>    The required available space.
#   btrfs          The mount point will be created only for the Btrfs scheme
#
default_partitioning =
    /     (min 1 GiB, max 70 GiB)
    /home (min 500 MiB, free 50 GiB)

# Default partitioning scheme.
# Valid values:
#
#   PLAIN      Create standard partitions.
#   BTRFS      Use the Btrfs scheme.
#   LVM        Use the LVM scheme.
#   LVM_THINP  Use LVM Thin Provisioning.
#
default_scheme = LVM

# Default version of LUKS.
# Valid values:
#
#   luks1  Use version 1 by default.
#   luks2  Use version 2 by default.
#
luks_version = luks2


[Storage Constraints]

# Minimal size of the total memory.
min_ram = 320 MiB

# Minimal size of the available memory for LUKS2.
luks2_min_ram = 128 MiB

# Should we recommend to specify a swap partition?
swap_is_recommended = False

# Recommended minimal sizes of partitions.
# Specify a mount point and a size on each line.
min_partition_sizes =
    /      250 MiB
    /usr   250 MiB
    /tmp   50  MiB
    /var   384 MiB
    /home  100 MiB
    /boot  512 MiB

# Required minimal sizes of partitions.
# Specify a mount point and a size on each line.
req_partition_sizes =

# Allowed device types of the / partition if any.
# Valid values:
#
#   LVM        Allow LVM.
#   MD         Allow RAID.
#   PARTITION  Allow standard partitions.
#   BTRFS      Allow Btrfs.
#   DISK       Allow disks.
#   LVM_THINP  Allow LVM Thin Provisioning.
#
root_device_types =

# Mount points that must be on a linux file system.
# Specify a list of mount points.
must_be_on_linuxfs = / /var /tmp /usr /home /usr/share /usr/lib

# Paths that must be directories on the / file system.
# Specify a list of paths.
must_be_on_root = /bin /dev /sbin /etc /lib /root /mnt lost+found /proc

# Paths that must NOT be directories on the / file system.
# Specify a list of paths.
must_not_be_on_root =

# Mount points that are recommended to be reformatted.
#
# It will be recommended to create a new file system on a mount point
# that has an allowed prefix, but doesn't have a blocked one.
# Specify lists of mount points.
reformat_allowlist = /boot /var /tmp /usr
reformat_blocklist = /home /usr/local /opt /var/www


[User Interface]
# The path to a custom stylesheet.
custom_stylesheet =

# A list of spokes to hide in UI.
# FIXME: Use other identification then names of the spokes.
hidden_spokes =

# Should the UI allow to change the configured root account?
can_change_root = False

# Should the UI allow to change the configured user accounts?
can_change_users = False

# Define the default password policies.
# Specify a policy name and its attributes on each line.
#
# Valid attributes:
#
#   quality <NUMBER>  The minimum quality score (see libpwquality).
#   length <NUMBER>   The minimum length of the password.
#   empty             Allow an empty password.
#   strict            Require the minimum quality.
#
password_policies =
    root (quality 1, length 6)
    user (quality 1, length 6, empty)
    luks (quality 1, length 6)

# Should kernel options be shown in the software selection spoke?
show_kernel_options = True

[License]
# A path to EULA (if any)
#
# If the given distribution has an EULA & feels the need to
# tell the user about it fill in this variable by a path
# pointing to a file with the EULA on the installed system.
#
# This is currently used just to show the path to the file to
# the user at the end of the installation.
eula =


[Timezone]
# URL for geolocation data provider.
# This is used for automatic language and timezone detection.
#
# Known valid providers:
#
#   https://geoip.fedoraproject.org/city
#   https://api.hostip.info/get_json.php
#
# If left empty, geolocation does not run.
#
geolocation_provider = https://geoip.fedoraproject.org/city

[Localization]
# Should geolocation be used when setting the language ?
#
use_geolocation = True

conf.d 目录中文件的内容覆盖 anaconda.conf 中的默认值。文件以 <priority>-<config-description>.conf 格式命名,如 100-my-distribution.conf。具有最高优先级的文件最后应用,覆盖之前应用的所有配置文件。

以下是自定义配置文件内容的一个示例:

# Anaconda configuration file for My Distribution

[Profile]
 Define the profile.
profile_id = my_distribution

[Profile Detection]
 Match os-release values.
os_id = my_distribution

[Network]
default_on_boot = NONE

[Storage]
file_system_type = xfs
default_partitioning =
	/     (min 2 GiB, max 50 GiB)
	/home (min 20 GiB, free 10 GiB)
	/test (size 256 MiB)
	swap

[Storage Constraints]
swap_is_recommended = True

[User Interface]
custom_stylesheet = /usr/share/anaconda/pixmaps/my_distribution.css

第 5 章 开发安装程序附加组件

有关 Anaconda 及其架构的详情解释了其后端以及附加组件正常工作所需的各种插件。这些信息支持对特定要求定制的自定义附加组件的开发。

5.1. Anaconda 和附加组件介绍

Anaconda 是 Fedora、Red Hat Enterprise Linux 及其变体使用的操作系统安装程序。它是一组 Python 模块和脚本,以及一些额外的文件,如 Gtk widgets(用 C 编写的)、systemd 单元和 dracut 库。它们一起形成了一个允许用户设置结果(目标)系统参数的工具,然后在计算机上设置此系统。安装过程有四个主要步骤:

  1. 准备安装目的地(通常是磁盘分区)
  2. 安装软件包和数据
  3. 安装并配置引导装载程序
  4. 配置新安装的系统

使用 Anaconda 可让您使用以下三种方法安装 Fedora、Red Hat Enterprise Linux 及其变体:

使用图形用户界面(GUI):

这是最常用的安装方法。该界面允许用户在开始安装前,只需很少或无需配置即可以交互方式安装系统。这个方法涵盖了所有常见的用例,包括设置复杂的分区布局。

图形界面支持通过 RDP 进行远程访问,其允许即使在没有图形卡或附加监控器的系统上也可以使用 GUI。

使用文本用户界面(TUI):

TUI 的工作方式类似于单色行打印机,它允许在不支持光标移动、颜色和其他高级功能的串行控制台上工作。文本模式是有限的,它只允许您自定义最常用的选项,如网络设置、语言选项或安装(软件包)源;在此界面中没有手动分区等高级功能。

使用 Kickstart 文件:

Kickstart 文件是一个纯文本文件,它使用类似 shell 的语法,可包含驱动器安装过程的数据。Kickstart 文件允许您部分或完全自动化安装。要完全自动化安装,需要一组命令来配置所有必填区域。如果缺少一个或多个命令,则安装需要用户参与才能完成。

除了安装程序本身的自动化外,Kickstart 文件还可以包含安装过程中在特定时间运行的自定义脚本。

5.2. Anaconda 构架

Anaconda 是一组 Python 模块和脚本。它还使用几个外部软件包和程序库。这个工具组的主要组件包括以下软件包:

  • pykickstart - 解析并验证 Kickstart 文件。另外,提供存储安装值的数据结构。
  • dnf - 安装软件包并解决依赖项的软件包管理器
  • blivet - 处理与存储管理相关的所有活动
  • pyanaconda - 包含 Anaconda 的用户界面和模块,如键盘和时区选择、网络配置和用户创建。同时提供各种执行面向系统功能的工具
  • python-meh - 包含一个异常处理程序,它在崩溃时收集和存储额外的系统信息,并将这些信息传递给 libreport 库,后者本身是 ABRT 项目 的一部分
  • dasbus - 启用 D-Bus 库与 anaconda 模块和外部组件之间的通信
  • python-simpleline - 文本 UI 框架库,用于在 Anaconda 文本模式中管理用户交互
  • gtk - 用于创建和管理 GUI 的 Gnome 工具包库

除了上面提到的软件包划分外,Anaconda 在内部被分成用户界面和一组模块,其作为单独的进程运行,并使用 D-Bus 库进行通信。这些模块是:

  • Boss - 管理内部模块发现、生命周期和协调
  • Localization - 管理区域设置
  • Network - 处理网络
  • Payloads - 处理以不同格式的安装数据,如 rpmostreetar 和其他安装格式。有效负载管理安装的数据源 ; 源的格式可能会有所不同,比如 CD-ROM、HDD、NFS、URL 和其他来源
  • Security - 管理与安全相关的方面
  • Services - 处理服务
  • Storage - 使用 blivet管理存储
  • Subscription - 处理 subscription-manager 工具和洞察力。
  • Timezone - 处理时间、日期、区域和时间同步。
  • Users - 创建用户和组。

每个模块声明其处理 Kickstart 的哪些部分,并具方法来将配置从 Kickstart 应用到安装环境和安装的系统。

Anaconda 的 Python 代码部分(pyanaconda)作为拥有用户界面的"主"进程启动。您提供的任何 Kickstart 数据都使用 pykickstart 模块进行解析,且 Boss 模块已启动,它会发现所有其他模块并启动它们。然后主进程会根据其声明的功能将 Kickstart 数据发送到模块。模块处理数据,将配置应用到安装环境,UI 将验证是否已做了所有必要的选择。如果没有,您必须在互动安装模式中提供数据。完成所有必要的选择后,安装就可以开始 - 把数据写入安装系统的模块。

5.3. Anaconda 用户界面

Anaconda 用户界面(UI)有一个非线性结构,也称 hub 和 spoke 模型。

Anaconda hub 和 spoke 模型的优点是:

  • 进入安装程序屏幕的灵活性。
  • 保留默认设置的灵活性。
  • 提供对配置的值的概述信息。
  • 支持可扩展性。您可以添加 hub,而无需对任何内容重新排序,并可解决一些复杂的依赖关系。
  • 支持使用图形和文本模式安装。

下图显示了安装程序布局以及hubspoke之间可能的交互 (创建):

图 5.1. Hub 和 spoke 模型

hub 和 spoke

在图中,屏幕 2-13 称为 普通 spoke,屏幕 1 和 14 称为 独立 spoke 。独立 spoke 是可在独立 spoke 或 hub 之前或之后使用的屏幕。例如,安装开始时的 Welcome 屏幕提示您为剩余的安装选择语言。

注意
  • 安装概述 是 Anaconda 中唯一的 hub。它显示了在安装开始前配置的选项概述

每个 spoke 都具有以下预定义的属性来反映 hub。

  • ready - 说明您能否访问 spoke 。例如,当安装程序配置软件包源时,spoke 的颜色是灰色的,在配置完成后前您无法访问它。
  • completed - 标记 spoke 是否已完成 (已设置所有必需的值)。
  • mandatory - 决定在继续安装前是否 必须 访问 spoke ;例如,您必须访问 Installation Destination spoke,即使您想要使用自动磁盘分区
  • status - 提供在 spoke 中配置的值的简短概述(在 hub 的 spoke 名称下显示)

要使用户界面更清晰,可将 spoke 分组为不同的类别。例如,Localization 类别为键盘布局选择、语言支持和时区设置组合在一起。

每个 spoke 都包含显示和允许从一个或多个模块修改值的 UI 控制。这个行为也适用于附加组件提供的 spoke。在 Kickstart 安装过程中,一些 spoke 可能会保持隐藏状态,同时仍然自动处理其数据,而无需打开它们。

5.4. 跨 Anaconda 线程通信

有些您需要在安装过程中执行的动作可能需要很长时间。例如:扫描磁盘以了解现有分区或下载软件包元数据。为防止您等待并保持响应,Anaconda 在单独的线程中运行这些操作。

Gtk 工具包不支持多个线程的元素更改。Gtk 的主事件循环运行在 Anaconda 进程的主线程中。因此,所有与 GUI 相关的操作都必须在主线程中执行。为此,请使用 GLib.idle_add,这并不总是容易或需要的。pyanaconda.ui.gui.utils 模块中定义的几个帮助程序功能和 decorators 可能会造成困难。

@gtk_action_wait@gtk_action_nowait 修饰符以这样一种方式更改修饰函数或方法,即当调用此函数或方法时,它会自动排队到在主线程中运行的 Gtk 的主循环中。返回值要么返回给调用者,要么被丢弃。

在 spoke 和 hub 通讯中,一个 spoke 会声明何时就绪且不会被阻断。hubQ 消息队列处理此功能,并定期检查主事件循环。当 spoke 变为可访问时,它会向队列发送一条消息来宣布更改,并且该更改不应再被阻止。

当 spoke 需要刷新其状态或完成一个标志时,也是如此。Configuration and Progress hub 有一个名为 progressQ 的不同的队列,它充当传输安装进度更新的介质。

这些机制也用于文本界面。在文本模式中,没有主循环,但可能需要大量事件进行键盘输入。

5.5. Anaconda 模块和 D-Bus 库

Anaconda 模块作为独立进程运行。要通过其 D-Bus API 与这些进程进行通信,请使用 dasbus 库。

通过 D-Bus API 调用方法是异步的,但使用 dasbus 库,您可以在 Python 中将它们转换为同步方法调用。您还可以写入以下程序之一:

  • 带有异步调用和返回处理程序的程序
  • 调用者需要等待调用完成的程序。

另外,Anaconda 使用模块中运行的任务对象。任务具有 D-Bus API 和方法,可在其他线程中自动执行。要成功运行任务,请使用 sync_run_taskasync_run_task 助手函数。

5.6. Hello World addon 示例

Anaconda 开发人员发布了一个名为"Hello World"的示例附加组件,在 GitHub 上提供。这里重现了后续章节的描述。

5.7. Anaconda 附加组件结构

Anaconda 附加组件是 Python 软件包,其中包含含有 __init__.py 和其他源目录(子软件包)的目录。由于 Python 只允许您导入每个软件包名称一次,因此请为软件包顶级目录指定唯一的名称。您可以使用任意名称,因为附加组件都会被加载,而无论它们的名称是什么,唯一的要求是它们必须被放在特定的目录中。

对附加组件的建议的命名约定类似于 Java 软件包或 D-Bus 服务名称。

要使目录名称为 Python 软件包的唯一标识符,请使用使用下划线(_)而不是点,使用您机构的反向域名作为附加组件名称的前缀。例如,com_example_hello_world

重要

确保在每个目录中创建一个 __init__.py 文件。缺少这个文件的目录被视为无效的 Python 软件包。

在编写附加组件时,请确定以下几项:

  • 对每个界面(图形界面和文本界面)的支持可由单独的子软件包提供,对于图形界面,这些子软件包被命名为 gui ,对于基于文本的界面,这些子软件包被命名为 tui
  • guitui 软件包包含一个 spokes 子软件包。 [1]
  • 软件包中包含的模块有一个任意名称。
  • gui/tui/ 目录包含带有任何名称的 Python 模块。
  • 有的服务可以执行附加组件的实际工作。可使用 Python 或者其他任何语言编写该服务。
  • 该服务实现了对 D-Bus 和 Kickstart 的支持。
  • 这个附加组件包含启用自动启动该服务的文件。

以下是支持每个接口(Kickstart、GUI 和 TUI)的附加目录结构示例:

例 5.1. 附加组件结构示例

com_example_hello_world
├─ gui
│  ├─ init.py
│  └─ spokes
│     └─ init.py
└─ tui
   ├─ init.py
   └─ spokes
   └─ init.py

每个软件包必须至少包含一个带有任意名称的模块,该名称定义了从 API 中定义的一个或多个类继承的类。

注意

对于所有附加组件,请遵循 Python 的 PEP 8PEP 257 文档字符串约定指南。对于 Anaconda 中的文档字符串的实际内容格式没有共识,唯一的要求是它们是人类可读的。如果您计划对您的附加组件使用自动生成的文档,则文档字符串应遵循您用于完成此操作的工具包指南。

如果附加组件需要定义一个新类别,您可以包含类别子软件包,但不建议这样做。



[1] 如果附加组件需要定义一个新的类别,则 gui 软件包中可能还包含一个 类别 子软件包,但不建议这样做。

5.8. Anaconda 服务及配置文件

Anaconda 服务和配置文件包含在 data/ 目录中。这些文件是启动附加组件服务并配置 D-Bus 所需要的。

以下是 Anaconda Hello World 附加组件的一些示例:

例 5.2. addon-name.conf 示例:

<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
       <policy user="root">
               <allow own="org.fedoraproject.Anaconda.Addons.HelloWorld"/>
               <allow send_destination="org.fedoraproject.Anaconda.Addons.HelloWorld"/>
       </policy>
       <policy context="default">
               <deny own="org.fedoraproject.Anaconda.Addons.HelloWorld"/>
               <allow send_destination="org.fedoraproject.Anaconda.Addons.HelloWorld"/>
       </policy>
</busconfig>

此文件必须放在安装环境中的 /usr/share/anaconda/dbus/confs/ 目录中。字符串 org.fedoraproject.Anaconda.Addons.HelloWorld 必须与 D-Bus 上附加服务的位置相对应。

例 5.3. addon-name.service 示例:

[D-BUS Service]
# Start the org.fedoraproject.Anaconda.Addons.HelloWorld service.
# Runs org_fedora_hello_world/service/main.py
Name=org.fedoraproject.Anaconda.Addons.HelloWorld
Exec=/usr/libexec/anaconda/start-module org_fedora_hello_world.service
User=root

此文件必须放在安装环境中的 /usr/share/anaconda/dbus/services/ 目录中。字符串 org.fedoraproject.Anaconda.Addons.HelloWorld 必须与 D-Bus 上附加服务的位置相对应。以 Exec= 开头的行中的值必须是在安装环境中启动服务的有效命令。

5.9. GUI 附加组件基本特性

与附加组件中的 Kickstart 支持类似,GUI 支持要求附加组件的每一个部分都必须至少包含一个模块,并带有一个继承自 API 定义的特定类的类定义。对于图形附加组件支持,您唯一应添加的类是 NormalSpoke 类,该类在 pyanaconda.ui.gui.spokes 中定义,作为屏幕普通spoke 类型的一个类。

要实现继承自 NormalSpoke 的新类,您必须定义 API 所需的以下类属性:

  • builderObjects - 列出了来自 spoke 的 .glade 文件中的所有顶级对象,它们应与其子对象(递归方式)一起暴露给 spoke。如果所有内容都应暴露给 spoke,则列表应为空。
  • mainWidgetName - 包含 .glade 文件中定义的主窗口小部件(Add Link)的 id。
  • uiFile - 包含 .glade 文件的名称。
  • category - 包含 spoke 所属类别的类。
  • icon - 包含用于 hub 上 spoke 的图标的标识符。
  • title - 定义要用于 hub 上的 spoke 的标题。

5.10. 对附加图形用户界面(GUI)添加支持

您可以通过执行以下高级步骤,向您的附加组件的图形用户界面(GUI)添加支持:

  1. 定义 Normalspoke 类所需的属性
  2. 定义 __init__initialize 方法
  3. 定义 refreshapplyexecute 方法
  4. 定义 statusreadycompletedmandatory 属性

先决条件

  • 您的附加组件包括对 Kickstart 的支持。请参阅 Anaconda 附加组件结构
  • 安装 anaconda-widgets 和 anaconda-widgets-devel 软件包,其中包含特定于 Anaconda 的 Gtk 小部件,如 SpokeWindow

流程

  • 根据以下示例,创建带有所有必要的定义的以下模块,来添加对 Add-on 图形用户界面(GUI)的支持。

例 5.4. 定义 Normalspoke 类所需的属性:

# will never be translated
_ = lambda x: x
N_ = lambda x: x

# the path to addons is in sys.path so we can import things from org_fedora_hello_world
from org_fedora_hello_world.gui.categories.hello_world import HelloWorldCategory
from pyanaconda.ui.gui.spokes import NormalSpoke

# export only the spoke, no helper functions, classes or constants
all = ["HelloWorldSpoke"]

class HelloWorldSpoke(FirstbootSpokeMixIn, NormalSpoke):
    """
    Class for the Hello world spoke. This spoke will be in the Hello world
    category and thus on the Summary hub. It is a very simple example of a unit
    for the Anaconda's graphical user interface. Since it is also inherited form
    the FirstbootSpokeMixIn, it will also appear in the Initial Setup (successor
    of the Firstboot tool).

    :see: pyanaconda.ui.common.UIObject
    :see: pyanaconda.ui.common.Spoke
    :see: pyanaconda.ui.gui.GUIObject
    :see: pyanaconda.ui.common.FirstbootSpokeMixIn
    :see: pyanaconda.ui.gui.spokes.NormalSpoke

    """

    # class attributes defined by API #

    # list all top-level objects from the .glade file that should be exposed
    # to the spoke or leave empty to extract everything
    builderObjects = ["helloWorldSpokeWindow", "buttonImage"]

    # the name of the main window widget
    mainWidgetName = "helloWorldSpokeWindow"

    # name of the .glade file in the same directory as this source
    uiFile = "hello_world.glade"

    # category this spoke belongs to
    category = HelloWorldCategory

    # spoke icon (will be displayed on the hub)
    # preferred are the -symbolic icons as these are used in Anaconda's spokes
    icon = "face-cool-symbolic"

    # title of the spoke (will be displayed on the hub)
    title = N_("_HELLO WORLD")

__all__ 属性导出 spoke 类,后跟其定义的第一行,包括前面在 GUI 附加组件基本功能 中提到的属性定义。这些属性值引用 com_example_hello_world/gui/spokes/hello.glade 文件中定义的小部件。还有两个值得注意的属性:

  • category,它的值从 com_example_hello_world.gui.gui.categories 模块的 HelloWorldCategory 类导入。附加组件路径 HelloWorldCategory 位于 sys.path 中,因此值可以从 com_example_hello_world 软件包导入。category 属性是 N_function 名称的一部分,用于标记要转换的字符串;但会返回字符串的非转换版本,因为转换发生在后续阶段。
  • title,其定义中包含一个下划线。title 属性下划线标记标题本身的开头,并使用 Alt+H 键盘快捷键使 spoke 可访问。

通常在类定义标头和类 属性 定义后面是初始化类实例的构造器。如果是 Anaconda 图形界面对象,有两初始化新实例的种方法:__init__ 方法和 initialize 方法。

这两个函数背后的原因是,GUI 对象可以一次在内存中创建 ,并在不同时间完全初始化,而 spoke 初始化可能会很耗时。因此,__init__ 方法应只调用父类的 __init__ 方法,例如初始化非 GUI 属性。另一方面,安装程序图形用户界面初始化时调用的 initialize 方法应该完成 spoke 的整个初始化过程。

Hello World add-on 示例中,定义了如下两种方法:注意传给 __init__ 方法的编号和描述参数。

例 5.5. 定义 __init__ 和初始化方法:

def __init__(self, data, storage, payload):
    """
    :see: pyanaconda.ui.common.Spoke.init
    :param data: data object passed to every spoke to load/store data
    from/to it
    :type data: pykickstart.base.BaseHandler
    :param storage: object storing storage-related information
    (disks, partitioning, boot loader, etc.)
    :type storage: blivet.Blivet
    :param payload: object storing packaging-related information
    :type payload: pyanaconda.packaging.Payload

    """

    NormalSpoke.init(self, data, storage, payload)
    self._hello_world_module = HELLO_WORLD.get_proxy()

def initialize(self):
    """
    The initialize method that is called after the instance is created.
    The difference between init and this method is that this may take
    a long time and thus could be called in a separate thread.
    :see: pyanaconda.ui.common.UIObject.initialize
    """
    NormalSpoke.initialize(self)
    self._entry = self.builder.get_object("textLines")
    self._reverse = self.builder.get_object("reverseCheckButton")

传给 __init__ 方法的数据参数是存储所有数据的 Kickstart 文件的内存树状表示。在祖先的一个 __init__ 方法中,它存储在 self.data 属性中,这个属性允许类中的所有其他方法读取和修改结构。

注意

从 RHEL10 开始,存储对象 不再可用。如果您的附加组件需要与存储配置进行交互,请使用 Storage DBus 模块。

由于 HelloWorldData 类已在 Hello World 附加组件示例 中定义,因此此附加组件的 self.data 中已有一个子树。它的根(一个类的实例)作为 self.data.addons.com_example_hello_world 提供。

祖先的 __init__ 的所做的另一个操作是使用 spoke 的 .glade 文件初始化 GtkBuilder 的实例,并将它存储为 self.builderinitialize 方法使用这个来获取用于显示和修改 Kickstart 文件的 %addon 部分中文本的 GtkTextEntry

在创建 spoke 时,__init__initialize 方法都很重要。但是,spoke 的主要作用是通过希望更改或查看 spoke 的值显示和设置的用户访问。要启用此功能,可以使用其他三种方法:

  • refresh - 在要访问 spoke 时调用 ;此方法会刷新 spoke 的状态,主要是它的 UI 元素,以确保显示的数据与内部数据结构相匹配,并通过它来确保显示 self.data 结构中存储的当前值。
  • apply - 当 spoke 离开时调用,用于将 UI 元素的值存储回 self.data 结构。
  • execute - 当用户离开 spoke 时调用,用于根据 spoke 的新状态来执行任何运行时更改。

这些功能在 Hello World 附加组件示例中以以下方式实现:

例 5.6. 定义 refresh 、apply 和 execute 方法

def refresh(self):
    """
    The refresh method that is called every time the spoke is displayed.
    It should update the UI elements according to the contents of
    internal data structures.
    :see: pyanaconda.ui.common.UIObject.refresh
    """
    lines = self._hello_world_module.Lines
    self._entry.get_buffer().set_text("".join(lines))
    reverse = self._hello_world_module.Reverse
    self._reverse.set_active(reverse)

def apply(self):
    """
    The apply method that is called when user leaves the spoke. It should
    update the D-Bus service with values set in the GUI elements.
    """
    buf = self._entry.get_buffer()
    text = buf.get_text(buf.get_start_iter(),
                        buf.get_end_iter(),
                        True)
    lines = text.splitlines(True)
    self._hello_world_module.SetLines(lines)

    self._hello_world_module.SetReverse(self._reverse.get_active())

def execute(self):
  """
  The execute method that is called when the spoke is exited. It is
  supposed to do all changes to the runtime environment according to
  the values set in the GUI elements.

  """

  # nothing to do here
  pass

您可以使用几个额外的方法来控制 spoke 的状态:

  • ready - 确定 spoke 是否准备好被访问;如果值为"False",则不能访问 spoke,例如,在配置软件包源之前的 Package Selection spoke。
  • completed - 确定 spoke 是否已完成。
  • mandatory - 确定 spoke 是强制还是非强制的,例如,Installation Destination spoke,其必须一直被访问,即使您想使用自动分区。

所有这些属性都需要根据安装过程的当前状态动态确定。

以下是在 Hello World 附加组件中实现这些方法的示例,这需要在 HelloWorldData 类的文本属性中设置一个特定的值:

例 5.7. 定义 ready 、completed 和 mandatory 方法

@property
def ready(self):
    """
    The ready property reports whether the spoke is ready, that is, can be visited
    or not. The spoke is made (in)sensitive based on the returned value of the ready
    property.

    :rtype: bool

    """

    # this spoke is always ready
    return True


@property
def mandatory(self):
    """
    The mandatory property that tells whether the spoke is mandatory to be
    completed to continue in the installation process.

    :rtype: bool

    """

    # this is an optional spoke that is not mandatory to be completed
    return False

在定义了这些属性后,spoke 可以控制其可访问性和完整性,但不能提供其中配置的值的摘要 - 您必须访问 spoke 以查看它是如何配置的,这可能不是必需的。因此,存在名为 status 的额外属性。此属性包含一文本行,并带有已配置值的简短摘要,然后其可显示在 spoke 标题下的 hub 中。

status 属性定义在 Hello World 示例附加组件中,如下所示:

例 5.8. 定义 status 属性

@property
def status(self):
    """
    The status property that is a brief string describing the state of the
    spoke. It should describe whether all values are set and if possible
    also the values themselves. The returned value will appear on the hub
    below the spoke's title.
    :rtype: str
    """
    lines = self._hello_world_module.Lines
    if not lines:
        return _("No text added")
    elif self._hello_world_module.Reverse:
        return _("Text set with {} lines to reverse").format(len(lines))
    else:
        return _("Text set with {} lines").format(len(lines))

在定义了示例中描述的所有属性后,附加组件完全支持显示图形用户界面(GUI)以及 Kickstart。

注意

此处演示的示例非常简单,不包含任何控制;需要掌握 Python Gtk 编程知识才能在 GUI 中开发功能性的、交互式 spoke 。

一个值得注意的限制是每个 spoke 都必须有自己的主窗口,即 SpokeWindow 小部件的一个实例。此小部件以及其他特定于 Anaconda 的小部件可在 anaconda-widgets 软件包中找到。您可以在 anaconda-widgets-devel 软件包中找到使用 GUI 支持开发附加组件所需的其他文件,如 Glade 定义。

一旦图形界面支持模块包含所有必要的方法,您就可以继续使用以下部分来添加对基于文本的用户界面的支持,

5.11. 附加组件 GUI 高级功能

pyanaconda 软件包包含多个帮助程序和工具函数,以及用于 hub 和 spoke 的结构。其中大多数位于 pyanaconda.ui.gui.utils 软件包中。

Hello World 附加组件示例演示了englightbox 内容管理器的用法,Anaconda 也使用它。此内容管理器可以将窗口置于 lightbox 中,以提高其可见性并聚焦它,以防止用户与底层窗口进行交互。为了演示此功能,示例附加组件包含一个打开新对话框窗口的按钮;对话框本身是一个继承于 GUIObject 类的特殊 HelloWorldDialog,其在 pyanaconda.ui.gui.init 中定义。

对话框类定义运行和销毁可通过 self.window 属性访问的内部 Gtk 对话框的 run 方法,该对话框使用具有同样含义的 mainWidgetName 类属性填充。因此,定义对话框的代码非常简单,如下例所示:

例 5.9. 定义 englightbox 对话框

        # every GUIObject gets ksdata in init
        dialog = HelloWorldDialog(self.data)

        # show dialog above the lightbox
        with self.main_window.enlightbox(dialog.window):
            dialog.run()

定义 englightbox 对话框 示例代码会创建一个对话框实例,然后使用 enlightbox 内容管理器在 lightbox 中运行对话框。上下文管理器有一个对 spoke 窗口的引用,只需要对话框的窗口来实例化对话框的 lightbox 。

5.12. TUI 附加组件基本特性

Anaconda 还支持基于文本的界面(TUI)。这个界面在功能方面有更多限制,但在某些系统上,它可能是交互式安装的唯一选择。有关基于文本的界面和图形界面与 TUI 的限制的更多信息,请参阅 Anaconda 简介和附加组件

注意

要在附加组件中添加对文本接口的支持,请在 tui 目录下创建新的子软件包集合,如 Anaconda 附加组件结构所述。

安装程序中的文本模式支持是基于 simpleline 库,该库仅允许非常简单的用户交互。文本模式界面:

  • 不支持光标移动 - 相反,其行为类似一台行打印机。
  • 不支持任何视觉增强,例如使用不同的颜色或字体。

在内部,simpleline 工具包有三个主要类: AppUIScreenWidget。Widget 是包含要在屏幕上打印的信息的单元。它们被放在被 App 类的一个实例切换的 UIScreens 上。在基本元素之上,hub spoke 和对话框 都 以类似图形界面的方式包含各种小部件。

  • title - 决定 spoke 的标题,类似于 GUI 中的 title 参数。
  • category - 确定作为字符串的 spoke 类;类名称不在任何地方显示,它仅用于分组。
注意

TUI 处理类的方式与 GUI 不同。将预先存在的类别分配给您的新 spoke。创建新类需要修补 Anaconda,这不会带来任何好处。

每个 spoke 也预计覆盖多种方法,即 initinitializerefreshapplyexecuteinputprompt 和属性(readycompletedmandatorystatus)。

5.13. 定义一个简单的 TUI Spoke

以下示例演示了在 Hello World 示例附加组件中一个简单的文本用户界面(TUI) spoke 的实现:

先决条件

流程

  • 根据以下示例,创建带有所有必要定义的模块来添加对附加文本用户界面(TUI)的支持:

例 5.10. 定义一个简单的 TUI Spoke

class HelloWorldSpoke(NormalTUISpoke):
    # category this spoke belongs to
    category = HelloWorldCategory

def __init__(self, *args, **kwargs):
    """
    Create the representation of the spoke.

    :see: simpleline.render.screen.UIScreen
    """
    super().__init__(*args, **kwargs)
    self.title = N_("Hello World")
    self._hello_world_module = HELLO_WORLD.get_proxy()
    self._container = None
    self._reverse = False
    self._lines = ""

def initialize(self):
    """
    The initialize method that is called after the instance is created.
    The difference between __init__ and this method is that this may take
    a long time and thus could be called in a separated thread.

    :see: pyanaconda.ui.common.UIObject.initialize
    """
    # nothing to do here
    super().initialize()

def setup(self, args=None):
    """
    The setup method that is called right before the spoke is entered.
    It should update its state according to the contents of DBus modules.

    :see: simpleline.render.screen.UIScreen.setup
    """
    super().setup(args)

    self._reverse = self._hello_world_module.Reverse
    self._lines = self._hello_world_module.Lines

    return True

def refresh(self, args=None):
    """
    The refresh method that is called every time the spoke is displayed.
    It should generate the UI elements according to its state.

    :see: pyanaconda.ui.common.UIObject.refresh
    :see: simpleline.render.screen.UIScreen.refresh
    """
    super().refresh(args)

    self._container = ListColumnContainer(
        columns=1
    )
    self._container.add(
        CheckboxWidget(
            title="Reverse",
            completed=self._reverse
        ),
        callback=self._change_reverse
    )
    self._container.add(
        EntryWidget(
            title="Hello world text",
            value="".join(self._lines)
        ),
        callback=self._change_lines
    )

    self.window.add_with_separator(self._container)

def _change_reverse(self, data):
    """
    Callback when user wants to switch checkbox.
    Flip state of the "reverse" parameter which is boolean.
    """
    self._reverse = not self._reverse

def _change_lines(self, data):
    """
    Callback when user wants to input new lines.
    Show a dialog and save the provided lines.
    """
    dialog = Dialog("Lines")
    result = dialog.run()
    self._lines = result.splitlines(True)

def input(self, args, key):
    """
    The input method that is called by the main loop on user's input.

    * If the input should not be handled here, return it.
    * If the input is invalid, return InputState.DISCARDED.
    * If the input is handled and the current screen should be refreshed,
      return InputState.PROCESSED_AND_REDRAW.
    * If the input is handled and the current screen should be closed,
      return InputState.PROCESSED_AND_CLOSE.

    :see: simpleline.render.screen.UIScreen.input
    """
    if self._container.process_user_input(key):
        return InputState.PROCESSED_AND_REDRAW

    if key.lower() == Prompt.CONTINUE:
        self.apply()
        self.execute()
        return InputState.PROCESSED_AND_CLOSE

    return super().input(args, key)

def apply(self):
    """
    The apply method is not called automatically for TUI. It should be called
    in input() if required. It should update the contents of internal data
    structures with values set in the spoke.
    """
    self._hello_world_module.SetReverse(self._reverse)
    self._hello_world_module.SetLines(self._lines)

def execute(self):
    """
    The execute method is not called automatically for TUI. It should be called
    in input() if required. It is supposed to do all changes to the runtime
    environment according to the values set in the spoke.
    """
    # nothing to do here
    pass

如需了解更多详细信息和最新的代码,请参阅 Hello World Anaconda Addon - GitHub 仓库

注意

如果仅调用祖先的 init,则不需要覆盖 init 方法,但示例中的注释描述了以可理解的方式传递给 spoke 类构造器的参数。

在上例中:

  • setup 方法为每个条目上的 spoke 的内部属性设置一个默认值,该属性随后由 refresh 方法显示,由 input 方法更新,并由 apply 方法用来更新内部数据结构。
  • execute 方法与 GUI 中的等效方法具有相同的目的;在这种情况下,该方法没有任何效果。
  • input 方法特定于文本界面;在 Kickstart 或 GUI 中没有等效的方法。input 方法负责用户交互。
  • input 方法处理输入的字符串,并根据其类型和值采取措施。上例要求输入任何值,然后将它存储为内部属性(密钥)。在更复杂的附加组件中,您通常需要执行一些不平凡的操作,如将字母解析为操作、将数字转换为整数、显示额外的屏幕或切换布尔值。
  • 输入类的 返回 值必须是 InputState 枚举或 input 字符串本身,如果此 input 应该由不同的屏幕处理。与图形模式不同,在离开 spoke 时不会自动调用 applyexecute 方法,必须从 input 方法显式调用它们。同样适用于关闭(隐藏) spoke 屏幕:必须从 close 方法显式调用它。

若要显示另一个屏幕,例如,您需要在不同的 spoke 中输入的附加信息,您可以实例化另一个 TUIObject ,并使用 ScreenHandler.push_screen_modal() 来显示它。

由于基于文本的界面的限制,TUI spoke 往往具有非常相似的结构,由用户应选中或取消选中并填充的复选框或条目列表组成。

5.14. 使用 NormalTUISpoke 来定义文本接口 Spoke

重新定义简单 TUI Spoke 示例演示了一种实现 TUI spoke 的方法,其中它的方法处理打印和处理可用的和提供的数据。但是,使用 pyanaconda.ui.tui.spokes 软件包中的 NormalTUISpoke 类可以以不同的方式实现这一点。通过继承此类,您只需指定应在其中设置的字段和属性,就可实现典型的 TUI spoke 。以下示例演示了这一点:

先决条件

流程

  • 根据以下示例,创建带有所有必要定义的模块,来对 Add-on 文本用户界面(TUI)添加支持。

例 5.11. 使用 NormalTUISpoke 来定义文本接口 Spoke

class HelloWorldEditSpoke(NormalTUISpoke):
    """Example class demonstrating usage of editing in TUI"""
    category = HelloWorldCategory

    def init(self, data, storage, payload):
        """
        :see: simpleline.render.screen.UIScreen
        :param data: data object passed to every spoke to load/store data
                     from/to it
        :type data: pykickstart.base.BaseHandler
        :param storage: object storing storage-related information
                        (disks, partitioning, boot loader, etc.)
        :type storage: blivet.Blivet
        :param payload: object storing packaging-related information
        :type payload: pyanaconda.packaging.Payload
        """
        super().init(self, *args, **Kwargs)

        self.title = N_("Hello World Edit")
        self._container = None

        # values for user to set
        self._checked = False
        self._unconditional_input = ""
        self._conditional_input = ""

    def refresh(self, args=None):
        """
        The refresh method that is called every time the spoke is displayed.
        It should update the UI elements according to the contents of self.data.
        :see: pyanaconda.ui.common.UIObject.refresh
        :see: simpleline.render.screen.UIScreen.refresh
        :param args: optional argument that may be used when the screen is
                     scheduled
        :type args: anything
        """
        super().refresh(args)
        self._container = ListColumnContainer(columns=1)

        # add ListColumnContainer to window (main window container)
        # this will automatically add numbering and will call callbacks when required
        self.window.add(self._container)

        self._container.add(CheckboxWidget(title="Simple checkbox", completed=self._checked),
                            callback=self._checkbox_called)
        self._container.add(EntryWidget(title="Unconditional text input",
                                        value=self._unconditional_input),
                            callback=self._get_unconditional_input)

        # show conditional input only if the checkbox is checked
        if self._checked:
            self._container.add(EntryWidget(title="Conditional password input",
                                            value="Password set" if self._conditional_input
                                            else ""),
                                callback=self._get_conditional_input)

        self.window.add_with_separator(self._container)

    def _checkbox_called(self, data):  # pylint: disable=unused-argument
        """Callback when user wants to switch checkbox.

        :param data: can be passed when adding callback in container (not used here)
        :type data: anything
        """
        self._checked = not self._checked

    def _get_unconditional_input(self, data):  # pylint: disable=unused-argument
        """Callback when the user wants to set unconditional input.

        :param data: can be passed when adding callback in container (not used here)
        :type data: anything
        """
        dialog = Dialog(
            "Unconditional input",
            conditions=[self._check_user_input]
        )
        self._unconditional_input = dialog.run()

    def _get_conditional_input(self, data):  # pylint: disable=unused-argument
        """Callback when the user wants to set conditional input.

        :param data: can be passed when adding callback in container (not used here)
        :type data: anything
        """
        dialog = PasswordDialog(
            "Unconditional password input",
            policy_name=PASSWORD_POLICY_ROOT
        )
        self._conditional_input = dialog.run()

    def _check_user_input(self, user_input, report_func):
        """Check if the user has written a valid value.

        :param user_input: user input for validation
        :type user_input: str

        :param report_func: function for reporting errors on user input
        :type report_func: func with one param
        """
        if re.match(r'^\w+$', user_input):
            return True
        else:
            report_func("You must set at least one word")
            return False

    def input(self, args, key):
        """
        The input method that is called by the main loop on user's input.

        :param args: optional argument that may be used when the screen is
                     scheduled
        :type args: anything
        :param key: user's input
        :type key: unicode
        :return: if the input should not be handled here, return it, otherwise
                 return InputState.PROCESSED or InputState.DISCARDED if the input was
                 processed successfully or not respectively
        :rtype: enum InputState
        """
        if self._container.process_user_input(key):
            return InputState.PROCESSED_AND_REDRAW
        else:
            return super().input(args, key)


    @property
    def completed(self):
        # completed if user entered something non-empty to the Conditioned input
        return bool(self._conditional_input)

    @property
    def status(self):
        return "Hidden input %s" % ("entered" if self._conditional_input
                                    else "not entered")

    def apply(self):
        # nothing needed here, values are set in the self.args tree
        pass

如需了解更多详细信息和最新的代码,请参阅 Hello World NormalTUISpoke - GitHub Repository

5.15. 部署和测试 Anaconda 附加组件

您可以在安装环境中部署并测试您自己的 Anaconda 附加组件。要做到这一点,请按照以下步骤执行:

先决条件

  • 您创建了附加组件。
  • 您有权访问您的 D-Bus 文件。
  • 您已安装了 lorax 软件包。

流程

  1. 在您喜欢的位置创建一个 DIR 目录。
  2. Add-on python 文件添加到 DIR/usr/share/anaconda/addons/ 中。
  3. 将您的 D-Bus 服务文件复制到 DIR/usr/share/anaconda/dbus/services/ 中。
  4. 将您的 D-Bus 服务配置文件复制到 /usr/share/anaconda/dbus/confs/
  5. 创建 updates 镜像。

    访问 DIR 目录:

    cd DIR

    查找 updates 镜像。

    find . | cpio -c -o | pigz -9cv > DIR/updates.img
  6. 使用 mkksiso 工具将 updates 镜像包含在 ISO 引导镜像中:

    sudo mkksiso -u updates.img boot.iso new_boot.iso
  7. 引导生成的 new_boot.iso

    它自动将嵌入的 updates 镜像与附加组件一起应用,从而导致附加组件在安装期间被使用。

有关解包现有引导镜像、创建 product.img 文件并重新打包镜像的具体步骤,请参阅 提取 Red Hat Enterprise Linux 引导镜像

第 6 章 完成自定义后的任务

要完成自定义配置,请执行以下任务:

  • 创建 product.img 文件(仅适用于图形化定制)。
  • 创建自定义引导镜像。

这部分提供有关如何创建 product.img 镜像文件以及创建自定义引导镜像的信息。

6.1. 创建 product.img 文件

product.img 镜像文件是包含新安装程序文件的存档文件,这些安装程序文可在运行时取代现有的安装程序文件。

在系统启动期间,Anaconda 将从引导介质上的 images/ 目录中加载 product.img 文件。然后,它会使用此目录中的文件替换安装程序文件系统中同名的文件。替换时的文件会自定义安装程序(例如,将默认镜像替换为自定义镜像)。

注意: product.img 镜像必须包含与安装程序相同的目录结构。有关安装程序目录结构的更多信息,请参阅下表。

Expand
表 6.1. 安装程序目录结构和自定义内容
自定义内容的类型文件系统位置

Pixmaps(logo、sidebar、top bar 等)

/usr/share/anaconda/pixmaps/

GUI 样式表

/usr/share/anaconda/anaconda-gtk.css

Anaconda 附加组件

/usr/share/anaconda/addons/

Profile 配置文件

/etc/anaconda/profile.d/

自定义配置文件

/etc/anaconda/conf.d/

Anaconda DBus 服务配置文件

/usr/share/anaconda/dbus/confs/

Anaconda DBus 服务文件

/usr/share/anaconda/dbus/services/

以下流程解释了如何创建 product.img 文件。

流程

  1. 导航到工作目录,如 /tmp ,创建名为 product/ 的子目录:

    $ cd /tmp
  2. 创建一个子目录 product/

    $ mkdir product/
  3. 创建一个与您要替换的文件位置相同的目录结构。例如,如果要测试安装系统上 /usr/share/anaconda/addons 目录中的附加组件,请在您的工作目录中创建同样的结构:

    $ mkdir -p product/usr/share/anaconda/addons
    注意

    要查看安装程序的运行时文件系统,请启动安装并切换到虚拟控制台 1 (Ctrl+Alt+F1),然后切换到第二个 tmux 窗口(Ctrl+b+2)。可用来浏览文件系统打开的 shell 提示符。

  4. 将自定义文件(在这个示例中, Anaconda 的自定义附加组件)放在新创建的目录中:

    $ cp -r ~/path/to/custom/addon/ product/usr/share/anaconda/addons/
  5. 重复步骤 3 和 4(为您要添加到安装程序的每个文件创建目录结构并将自定义文件放在其中)。
  6. 在目录的根目录中创建一个 .buildstamp 文件。.buildstamp 文件描述了系统版本、产品和其他几个参数。以下是 Red Hat Enterprise Linux 8.4 中的 .buildstamp 文件示例:

    [Main]
    Product=Red Hat Enterprise Linux
    Version=8.4
    BugURL=https://bugzilla.redhat.com/
    IsFinal=True
    UUID=202007011344.x86_64
    [Compose]
    Lorax=28.14.49-1

    IsFinal 参数指定镜像是否是产品的发行版本(GA)版本(True),还是预发布,如 Alpha、Beta 版还是一个内部里程碑(False)。

  7. 进到 product/ 目录,创建 product.img 归档文件:

    $ cd product
    $ find . | cpio -c -o | gzip -9cv > ../product.img

    这会在 product/ 目录的上一级目录中创建 product.img 文件。

  8. product.img 文件移到提取的 ISO 映像的 images/ 目录。

现在 product.img 文件已创建好,您要做的自定义内容被放在各自的目录中。

注意

不是在引导介质上添加 product.img 文件,您可以将此文件放在不同的位置,并在引导菜单中使用 inst.updates= 引导选项加载它。在这种情况下,只要可以从安装系统访问该位置,镜像文件可以是任何名称,并可放在任意位置(USB 闪存驱动器、硬盘、HTTP、FTP 或者 NFS 服务器)。

6.2. 创建自定义引导镜像

自定义引导镜像和 GUI 布局后,创建一个新镜像,其中包含您所做的更改。

要创建自定义引导镜像,请按照以下步骤操作。

流程

  1. 请确定您的所有更改都包含在工作目录中。例如,如果您要测试附加组件,请确保将 product.img 放在 images/ 目录中。
  2. 确保您的当前工作目录是提取的 ISO 镜像的顶级目录,例如 /tmp/ISO/iso/
  3. 安装以下软件包:isomd5sum,xorriso,lorax:

    # dnf install isomd5sum xorriso lorax
  4. 使用 mkefiboot 制作一个 EFI 引导镜像:

    # mkefiboot --label=ANACONDA /tmp/ISO/iso/EFI/BOOT/ /tmp/ISO/iso/images/efiboot.img
  5. 使用 xorrisofs 创建一个新的 ISO 镜像:

    # xorrisofs -o ../NEWISO.iso -R -J -V RHEL-10-0-BaseOS-x86_64 --grub2-mbr /usr/lib/grub/i386-pc/boot_hybrid.img -partition_offset 16 -appended_part_as_gpt -append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B /tmp/ISO/iso/images/efiboot.img -iso_mbr_part_type EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 -c boot.cat --boot-catalog-hide -b images/eltorito.img -no-emul-boot -boot-load-size 4 -boot-info-table --grub2-boot-info -eltorito-alt-boot -e --interval:appended_partition_2:all:: -no-emul-boot -graft-points .discinfo=/tmp/ISO/iso/.discinfo images/install.img=/tmp/ISO/iso/images/install.img images/pxeboot=/tmp/ISO/iso/images/pxeboot boot/grub2=/tmp/ISO/iso/boot/grub2 boot/grub2/i386-pc=/usr/lib/grub/i386-pc images/eltorito.img=/tmp/ISO/iso/images/eltorito.img EFI/BOOT=/tmp/ISO/iso/EFI/BOOT

    在上例中:

    • 如果您对需要一个位置来在同一磁盘上加载文件的选项使用 LABEL= 指令,请确保 -V 选项的值与镜像的引导装载程序配置匹配。如果您的引导装载程序配置(用于 BIOS 的boot/grub2/grub.cfg 和用于 UEFI 的 EFI/BOOT/grub.cfg )使用 inst.stage2=LABEL=disk_label 节来从同一磁盘加载安装程序的第二阶段,则磁盘标签必须匹配。

      重要

      在引导装载程序配置文件中,将磁盘标签中的所有空格替换为 \x20。例如,如果您创建了一个带有 RHEL 10.0 标签的 ISO 镜像,则引导装载程序配置应使用 RHEL\x2010.0

    • -o 选项(-o ../NEWISO.iso)的值替换为新镜像的文件名。示例中的值在当前目录 上面的 目录中创建 NEWISO.iso 文件。有关这个命令的详情请参考您系统上的 xorrisofs (1) 手册页。
  6. 在镜像中省略 MD5 checksum。请注意,如果没有 MD5 检验和,镜像验证检查可能会失败(引导装载程序配置中的 rd.live.check 选项),安装可能会挂起。

    # implantisomd5 ../NEWISO.iso

    在上例中, 使用文件名和您在上一步中创建的 ISO 镜像位置替换 ../NEWISO.iso

    现在,您可以将新 ISO 镜像写入到物理介质或网络服务器,来在物理硬件上启动它,或者您可以使用它来开始安装虚拟机。

Red Hat logoGithubredditYoutubeTwitter

学习

尝试、购买和销售

社区

關於紅帽

我们提供强化的解决方案,使企业能够更轻松地跨平台和环境(从核心数据中心到网络边缘)工作。

让开源更具包容性

红帽致力于替换我们的代码、文档和 Web 属性中存在问题的语言。欲了解更多详情,请参阅红帽博客.

关于红帽文档

Legal Notice

Theme

© 2026 Red Hat
返回顶部