Customizing Anaconda Red Hat Enterprise Linux 9 Changing the installer appearance and creating custom add-ons on Red Hat Enterprise Linux Red Hat Customer Content Services

Commands:
mount -t iso9660 -o loop path/to/image.iso /mnt/iso
mkdir /tmp/ISO
cp -pRf /mnt/iso /tmp/ISO
umount /mnt/iso
label check menu label Test this ^media & install Red Hat Enterprise Linux 9. menu default kernel vmlinuz append initrd=initrd.img inst.stage2=hd:LABEL=RHEL-9-BaseOS-x86_64 rd.live.check quiet
menu begin ^Troubleshooting menu title Troubleshooting label rescue menu label ^Rescue a Red Hat Enterprise Linux system kernel vmlinuz append initrd=initrd.img inst.stage2=hd:LABEL=RHEL-9-BaseOS-x86_64 rescue quiet menu separator label returntomain menu label Return to ^main menu menu exit menu end
menu color element ansi foreground background shadow
menuentry 'Test this media & install Red Hat Enterprise Linux 9' --class fedora --class gnu-linux --class gnu --class os { linuxefi /images/pxeboot/vmlinuz inst.stage2=hd:LABEL=RHEL-9-BaseOS-x86_64 rd.live.check quiet initrdefi /images/pxeboot/initrd.img }
submenu 'Submenu title' { menuentry 'Submenu option 1' { linuxefi /images/vmlinuz inst.stage2=hd:LABEL=RHEL-9-BaseOS-x86_64 xdriver=vesa nomodeset quiet initrdefi /images/pxeboot/initrd.img } menuentry 'Submenu option 2' { linuxefi /images/vmlinuz inst.stage2=hd:LABEL=RHEL-9-BaseOS-x86_64 rescue quiet initrdefi /images/initrd.img } }
pixmaps ├─ anaconda-password-show-off.svg ├─ anaconda-password-show-on.svg ├─ right-arrow-icon.png ├─ sidebar-bg.png ├─ sidebar-logo.png └─ topbar-bg.png
/* 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; }
[Main] Product=My Distribution Version=9 BugURL=https://bugzilla.redhat.com/ IsFinal=True UUID=202007011344.x86_64 [Compose] Lorax=28.14.49-1
Run Anaconda in the debugging mode. debug = False Enable Anaconda addons. This option is deprecated and will be removed in the future. addons_enabled = True List of enabled Anaconda DBus modules. This option is deprecated and will be removed in the future. kickstart_modules = 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.* Should the installer show a warning about enabled SMT? can_detect_enabled_smt = False 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 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 Default package environment. default_environment = List of ignored packages. ignored_packages = Names of repositories that provide latest updates. updates_repositories = 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 USDreleasever and USDbasearch happen automatically. default_rpm_gpg_keys = 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 Type of the boot loader. Supported values: # DEFAULT Choose the type by platform. EXTLINUX Use extlinux as the boot loader. # 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 rd.znet rd_ZNET 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 Enable dmraid usage during the installation. dmraid = True Enable iBFT usage during the installation. ibft = True Do you prefer creation of GPT disk labels? gpt = False Tell multipathd to use user friendly names when naming devices during the installation. multipath_friendly_names = True Do you want to allow imperfect devices (for example, degraded mdraid array devices)? allow_imperfect_devices = False 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. # 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 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 200 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 does not have a blocked one. Specify lists of mount points. reformat_allowlist = /boot /var /tmp /usr reformat_blocklist = /home /usr/local /opt /var/www The path to a custom stylesheet. custom_stylesheet = The path to a directory with help files. help_directory = /usr/share/anaconda/help 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) 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 =
Anaconda configuration file for Red Hat Enterprise Linux. [Product] product_name = Red Hat Enterprise Linux Show a warning if SMT is enabled. can_detect_enabled_smt = True [Network] default_on_boot = DEFAULT_ROUTE_DEVICE [Payload] ignored_packages = ntfsprogs btrfs-progs dmraid enable_closest_mirror = False default_source = CDN [Boot loader] efi_dir = redhat [Storage] file_system_type = xfs default_partitioning = / (min 1 GiB, max 70 GiB) /home (min 500 MiB, free 50 GiB) swap [Storage Constraints] swap_is_recommended = True [User Interface] help_directory = /usr/share/anaconda/help/rhel [License] eula = /usr/share/redhat-release/EULA
Anaconda configuration file for Red Hat Virtualization. [Product] product_name = Red Hat Virtualization (RHVH) [Base Product] product_name = Red Hat Enterprise Linux [Storage] default_scheme = LVM_THINP default_partitioning = / (min 6 GiB) /home (size 1 GiB) /tmp (size 1 GiB) /var (size 15 GiB) /var/crash (size 10 GiB) /var/log (size 8 GiB) /var/log/audit (size 2 GiB) swap [Storage Constraints] root_device_types = LVM_THINP must_not_be_on_root = /var req_partition_sizes = /var 10 GiB /boot 1 GiB
com_example_hello_world ├─ gui │ ├─ init .py │ └─ spokes │ └─ init .py └─ tui ├─ init .py └─ spokes └─ init .py
<!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>
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
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")
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")
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
@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
@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))
every GUIObject gets ksdata in init dialog = HelloWorldDialog(self.data) # show dialog above the lightbox with self.main_window.enlightbox(dialog.window): dialog.run()
@classmethod def should_run(cls, environment, data): """Run this spoke for Anaconda and Initial Setup""" return True
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
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+USD', 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
cd DIR
find . | cpio -c -o | pigz -9cv > DIR / updates .img
inst.updates=http://your-server/whatever/updates.img to boot options.
cd /tmp
mkdir product/
mkdir -p product/usr/share/anaconda/addons
cp -r ~/path/to/custom/addon/ product/usr/share/anaconda/addons/
[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
cd product
find . | cpio -c -o | gzip -9cv > ../product.img
genisoimage -U -r -v -T -J -joliet-long -V "RHEL-9 Server.x86_64" -volset "RHEL-9 Server.x86_64" -A "RHEL-9 Server.x86_64" -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -eltorito-alt-boot -e images/efiboot.img -no-emul-boot -o ../NEWISO.iso .
implantisomd5 ../NEWISO.iso